Linux 内核:设备驱动模型(4)uevent与热插拔
背景
我们简单回顾一下Linux的设备驱动模型(Linux Device Driver Model,LDDM):
1、在《sysfs与kobject基类》中,kobject的3大功能中包括了用户空间事件投递
。
2、在《driver-bus-device与probe》中,我们知道在驱动/设备的添加或者移除事件时,会同步投递对应的事件到用户空间,而且这个动作是通过uevent
来完成的。
当时出于学习的需要,我们并没有详细的说明。现在我们就来分析:
1、uevent机制以及 mdev 如何自动创建设备节点
2、实现自己想要的一些功能,比如U盘自动挂载。
参考文章:
- https://blog.csdn.net/newdye/article/details/77774070
- http://www.wowotech.net/device_model/uevent.html
- https://blog.csdn.net/u012066426/article/details/51917369
- https://blog.csdn.net/sandwich125/article/details/80580597
- https://blog.csdn.net/lizuobin2/article/details/51534385
uevent
uevent是kobject的一部分,用于在kobject状态发生改变时,例如增加、移除等,通知用户空间程序。用户空间程序收到这样的事件后,会做相应的处理。
uevent( user space event)是 内核与用户空间的一种基于netlink机制通信机制,主要用于设备驱动模型,常用于设备的热插拔。
例如:
U盘插入后,USB相关的驱动软件会动态创建用于表示该U盘的device结构(相应的也包括其中的kobject),并告知用户空间程序,为该U盘动态的创建/dev/目录下的设备节点;
更进一步,可以通知其它的应用程序,将该U盘设备mount到系统中,从而动态的支持该设备。
uevent的机制是比较简单的,设备模型中任何设备有事件需要上报时,会触发uevent提供的接口。uevent模块准备好上报事件的格式后,可以通过两个途径把事件上报到用户空间:一种是通过kmod模块,直接调用用户空间的可执行文件;另一种是通过netlink通信机制,将事件从内核空间传递给用户空间。
其中:
-
netlink是一种socket,专门用来进行内核空间和用户空间的通信;
-
kmod是管理内核模块的工具集,类似busybox,我们熟悉的lsmod,insmod等是指向kmod的链接。
uevent有几个核心的数据结构,按照惯例,先独立分析各个核心类,然后通过类之间的关系全面了解uevent机制
核心结构
kobject_action与事件类型
// include/linux/kobject.h
/*
* The actions here must match the index to the string array
* in lib/kobject_uevent.c
*
* Do not add new actions here without checking with the driver-core
* maintainers. Action strings are not meant to express subsystem
* or device specific properties. In most cases you want to send a
* kobject_uevent_env(kobj, KOBJ_CHANGE, env) with additional event
* specific variables added to the event environment.
*/
enum kobject_action {
KOBJ_ADD,
KOBJ_REMOVE,
KOBJ_CHANGE,
KOBJ_MOVE,
KOBJ_ONLINE,
KOBJ_OFFLINE,
KOBJ_MAX
};
// lib/kobject_uevent.c
/* the strings here must match the enum in include/linux/kobject.h */
static const char *kobject_actions[] = {
[KOBJ_ADD] = "add",
[KOBJ_REMOVE] = "remove",
[KOBJ_CHANGE] = "change",
[KOBJ_MOVE] = "move",
[KOBJ_ONLINE] = "online",
[KOBJ_OFFLINE] = "offline",
};
kobject_action定义了event的类型,包括:
action | 意义 |
---|---|
ADD /REMOVE |
kobject(或上层数据结构)的添加/移除事件。 |
ONLINE /OFFLINE |
kobject(或上层数据结构)的上线/下线事件,其实是是否使能。 |
CHANGE |
kobject(或上层数据结构)的状态或者内容发生改变。 |
MOVE |
kobject(或上层数据结构)更改名称或者更改parent(意味着在sysfs中更改了目录结构)。 |
CHANGE |
如果设备驱动需要上报的事件不再上面事件的范围内,或者是自定义的事件,可以使用该event,并携带相应的参数 |
kobj_uevent_env与用户环境
// include/linux/kobject.h
#define UEVENT_NUM_ENVP 32 /* number of env pointers */
#define UEVENT_BUFFER_SIZE 2048 /* buffer for the variables */
struct kobj_uevent_env {
// 指针数组,用于保存每个环境变量
char *envp[UEVENT_NUM_ENVP];
// 用于访问 环境变量指针 数组下标
int envp_idx;
// 保存环境变量的buffer与长度
char buf[UEVENT_BUFFER_SIZE];
int buflen;
};
前面有提到过,在通过kmod向用户空间上报event事件时,会直接执行用户空间的可执行文件。
而在Linux系统中,可执行文件的执行,依赖于环境变量,因此kobj_uevent_env
用于组织此次事件上报时的环境变量。
argv,argv[0]存储uevent_helper的值,uevent_helper的内容是由内核配置项CONFIG_UEVENT_HELPER_PATH决定的,该配置项指定了一个用户空间程序(或者脚本),用于解析上报的uevent,例如"/sbin/hotplug”。
可以这样理解,uevent模块通过kmod上报Uevent时,会通过call_usermodehelper函数,调用用户空间的可执行文件(或者脚本,简称uevent helper )处理该event。而该uevent helper的路径保存在uevent_helper数组中。对于uevent_helper还有一点要注意,在编译内核时,通过CONFIG_UEVENT_HELPER_PATH配置项,静态指定uevent helper的方式,会为每个event fork一个进程,随着内核支持的设备数量的增多,这种方式在系统启动时将会是致命的(可以导致内存溢出等),现在内核不再推荐使用该方式。
因此内核编译时,需要把该配置项留空。在系统启动后,大部分的设备已经ready,可以根据需要,重新指定一个uevent helper,以便检测系统运行过程中的热拔插事件。这可以通过把helper的路径写入到"/sys/kernel/uevent_helper”文件中实现。
实际上,内核通过sysfs文件系统的形式,将uevent_helper数组开放到用户空间,供用户空间程序修改访问。argv[1]存储了本kobj_uevent_env的buf指针,argv[2]一般为NULL。
kset_uevent_ops与策略
// include/linux/kobject.h
struct kset_uevent_ops {
int (* const filter)(struct kset *kset, struct kobject *kobj);
const char *(* const name)(struct kset *kset, struct kobject *kobj);
int (* const uevent)(struct kset *kset, struct kobject *kobj,
struct kobj_uevent_env *env);
};
前面在分析kset的时候,有一个属性uevent_ops就是kobj_uevent_ops结构。
filter
当任何kobject需要上报uevent时,它所属的kset可以通过该接口过滤,阻止不希望上报的event,从而达到从整体上管理的目的。
name
该接口可以返回kset的名称。如果一个kset没有合法的名称,则其下的所有Kobject将不允许上报uvent
uevent
当任何kobject需要上报uevent时,它所属的kset可以通过该接口统一为这些event添加环境变量。
因为很多时候上报uevent时的环境变量都是相同的,因此可以由kset统一处理,就不需要让每个kobject独自添加了。
三者的关系
当设备加载或卸载时,是怎么通过这几个uevent的核心类通知用户空间的呢?
通过前面的分析,大家应该知道,设备加载或卸载最直观的体现在/sys下目录的变化,/sys下的目录和kobject是对应的,因此还得从kobject说起。
kobject_uevent(&class_dev->kobj, KOBJ_ADD);
kobject_uevent_env(kobj, action, NULL);
// action_string = "add";
action_string = action_to_string(action);
/* 分配、保存环境变量的内存 */
/* environment values */
buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL);
/* 设置环境变量 */
nvp [i++] = scratch;
scratch += sprintf(scratch, "ACTION=%s", action_string) + 1;
envp [i++] = scratch;
scratch += sprintf (scratch, "DEVPATH=%s", devpath) + 1;
envp [i++] = scratch;
scratch += sprintf(scratch, "SUBSYSTEM=%s", subsystem) + 1;
/* 调用应用程序:比如mdev */
/* 在/etc/init.d/rcS 中的echo /sbin/mdev > /proc/sys/kernel/hotplug指定了应用程序*/
argv [0] = uevent_helper; // = "/sbin/mdev"
argv [1] = (char *)subsystem;
argv [2] = NULL;
call_usermodehelper (argv[0], argv, envp, 0);
发送事件
我们以之前注册驱动的时候,发送的事件为例。
// drivers/base/driver.c
int driver_register(struct device_driver *drv)
{
// ...
// 将事件发送到用户空间
kobject_uevent(&drv->p->kobj, KOBJ_ADD);
return ret;
}
kobject
找到自己的kset
,通过kobject_uevent
函数将事件发送到用户空间,但是实际上发送事件的动作由调用函数kobject_uevent_env
实现。
// lib/kobject_uevent.c
/**
* kobject_uevent - notify userspace by sending an uevent
*
* @action: action that is happening
* @kobj: struct kobject that the action is happening to
*
* Returns 0 if kobject_uevent() is completed with success or the
* corresponding error when it fails.
*/
int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{
return kobject_uevent_env(kobj, action, NULL);
}
EXPORT_SYMBOL_GPL(kobject_uevent);
kobject_uevent_env
主要做了这些事情:
1、获取整理了与即将发送的事件相关的环境变量,如ACTION、DEVPATH和SUBSYSTE等。
2、发送事件到用户空间。
/**
* kobject_uevent_env - send an uevent with environmental data
*
* @action: action that is happening
* @kobj: struct kobject that the action is happening to
* @envp_ext: pointer to environmental data
*
* Returns 0 if kobject_uevent_env() is completed with success or the
* corresponding error when it fails.
*/
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
char *envp_ext[])
{
struct kobj_uevent_env *env;
const char *action_string = kobject_actions[action];
const char *devpath = NULL;
const char *subsystem;
struct kobject *top_kobj;
struct kset *kset;
const struct kset_uevent_ops *uevent_ops;
int i = 0;
int retval = 0;
#ifdef CONFIG_NET
struct uevent_sock *ue_sk;
#endif
/* 1、找到对应的对象 */
/* search the kset we belong to */
// ...
/* 2、判断是否要过跳过发送事件 */
/* skip the event, if uevent_suppress is set*/
// ...
/* skip the event, if the filter returns zero. */
// ...
/* originating subsystem */
/* 通过uevent_ops->name函数取得子系统名,如果uevent_ops->name为NULL,则使用kset.kobj.name做为子系统名。
事实上,一个kset就是一个所谓的“subsystem”。
*/
if (uevent_ops && uevent_ops->name)
subsystem = uevent_ops->name(kset, kobj);
else
subsystem = kobject_name(&kset->kobj);
/* 3、处理事件*/
/* environment buffer */
// ...
/* complete object path */
// ...
/* default keys */
// ...
/* keys passed in from the caller */
// ...
/* let the kset specific function add its stuff */
// ...
/*
* Mark "add" and "remove" events in the object to ensure proper
* events to userspace during automatic cleanup. ...
*/
// ...
/* we will send an event, so request a new sequence number */
/*4、与用户空间交互 */
#if defined(CONFIG_NET)
/* send netlink message */
// ...
#endif
/* call uevent_helper, usually only enabled during early boot */
// ...
/* 5、回收资源 */
exit:
kfree(devpath);
kfree(env);
return retval;
}
EXPORT_SYMBOL_GPL(kobject_uevent_env);
找到顶层的对象
找到对象所属的顶级集合(kset)、顶级父对象(top_kobj)、以及顶级对应的uevent_ops:
/* 如果kobject 不属于一个Kset,则向上查找到,直到找到一个属于kset的kobject为止 */
struct kobject *top_kobj;
struct kset *kset;
const struct kset_uevent_ops *uevent_ops;
/* search the kset we belong to */
top_kobj = kobj;
while (!top_kobj->kset && top_kobj->parent)
top_kobj = top_kobj->parent;
if (!top_kobj->kset) {
pr_debug("kobject: '%s' (%p): %s: attempted to send uevent "
"without kset!
", kobject_name(kobj), kobj,
__func__);
return -EINVAL;
}
// 找到 kobj 的 kset,并使用event的操作方法
kset = top_kobj->kset;
uevent_ops = kset->uevent_ops;
判断是否要过跳过发送事件
判断分为2个层次:
1、这个kobj 是否允许上报事件
/* skip the event, if uevent_suppress is set*/
if (kobj->uevent_suppress) {
pr_debug("kobject: '%s' (%p): %s: uevent_suppress "
"caused the event to drop!
",
kobject_name(kobj), kobj, __func__);
return 0;
}
2、kset是否允许发送事件(通过filter
)。这里以bus为例。
/* skip the event, if the filter returns zero. */
if (uevent_ops && uevent_ops->filter)
if (!uevent_ops->filter(kset, kobj)) {
// 说明kobj希望发送的uevent被顶层kset过滤掉了,不再发送
pr_debug("kobject: '%s' (%p): %s: filter function "
"caused the event to drop!
",
kobject_name(kobj), kobj, __func__);
return 0;
}
/////////////////////////////
// drivers/base/bus.c
static int bus_uevent_filter(struct kset *kset, struct kobject *kobj)
{
struct kobj_type *ktype = get_ktype(kobj);
if (ktype == &bus_ktype)
return 1;
return 0;
}
static const struct kset_uevent_ops bus_uevent_ops = {
.filter = bus_uevent_filter,
};
int __init buses_init(void)
{
bus_kset = kset_create_and_add("bus", &bus_uevent_ops, NULL);
// ...
return 0;
}
处理事件
/* environment buffer */
env = kzalloc(sizeof(struct kobj_uevent_env), GFP_KERNEL);
if (!env)
return -ENOMEM;
/* complete object path */
// 获取Path 也就是kobj的路径 /sys/devices/xxx
devpath = kobject_get_path(kobj, GFP_KERNEL);
/* default keys */
// 将ACTION、DEVPATH、SUBSYSTEM三个默认环境变量添加到env中
retval = add_uevent_var(env, "ACTION=%s", action_string);
retval = add_uevent_var(env, "DEVPATH=%s", devpath);
retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem);
/* keys passed in from the caller */
// 额外的变量信息(由调用者提供,在bus.c中是NULL)
if (envp_ext) {
for (i = 0; envp_ext[i]; i++) {
retval = add_uevent_var(env, "%s", envp_ext[i]);
}
}
/* let the kset specific function add its stuff */
// kset可以通过uevent_ops->uevent完成自己特定的功能
if (uevent_ops && uevent_ops->uevent) {
retval = uevent_ops->uevent(kset, kobj, env);
}
/*
* Mark "add" and "remove" events in the object to ensure proper
* events to userspace during automatic cleanup. If the object did
* send an "add" event, "remove" will automatically generated by
* the core, if not already done by the caller.
*/
// 如果action是KOBJ_ADD, 设置state_add_uevent_sent为1。
// 如果action是KOBJ_REMOVE,设置state_remove_uevent_sent为1。
// 作用:确保在自动清理期间向用户空间发送正确的事件。
if (action == KOBJ_ADD)
kobj->state_add_uevent_sent = 1;
else if (action == KOBJ_REMOVE)
kobj->state_remove_uevent_sent = 1;
// 将SEQNUM环境变量添加到env中,代表 热插拔事件的顺序号.
// 顺序号是一个 64-位 数, 它每次产生热插拔事件都递增.
// 这允许用户空间以内核产生它们的顺序来排序热插拔事件, 因为对一个用户空 间程序可能乱序运行.
/* we will send an event, so request a new sequence number */
retval = add_uevent_var(env, "SEQNUM=%llu", (unsigned long long)++uevent_seqnum);
与用户空间交互
热插拔(hotplug)是指当有设备插入或拨出系统时,内核可以检测到这种状态变化,并通知用户空间加载或移除该设备对应的驱动程序模块。
在Linux系统上内核有两种机制可以通知用户空间执行加载或移除操作,一种是udev,另一种是/sbin/hotplug;
在Linux发展的早期,只有/sbin/hotplug,实际上是基于内核中的call_usermodehelper
函数实现的,它能从内核空间启动一个用户空间程序。
随着内核的发展,出现了udev机制并逐渐取代了/sbin/hotplug。udev的实现基于内核中的网络机制,它通过创建标准的socket接口来监听来自内核的网络广播包,并对接收到的包进行分析处理。
在Linux中,有两种方式完成向用户空间广播当前kset对象中的uevent事件:
- 通过udev的方式向用户空间广播当前kset对象中的uevent事件。
- 另外一种方式是在内核空间启动一个用户空间进程/sbin/hotplug,通过给该进程传递内核设定的环境变量的方式来通知用户空间kset对象中的uevent事件
通过udev的方式
mutex_lock(&uevent_sock_mutex);
#if defined(CONFIG_NET)
/* send netlink message */
list_for_each_entry(ue_sk, &uevent_sock_list, list) {
struct sock *uevent_sock = ue_sk->sk;
struct sk_buff *skb;
size_t len;
if (!netlink_has_listeners(uevent_sock, 1))
continue;
/* allocate message with the maximum possible size */
len = strlen(action_string) + strlen(devpath) + 2;
skb = alloc_skb(len + env->buflen, GFP_KERNEL);
if (skb) {
char *scratch;
/* add header */
scratch = skb_put(skb, len);
sprintf(scratch, "%s@%s", action_string, devpath);
/* copy keys to our continuous event payload buffer */
for (i = 0; i < env->envp_idx; i++) {
len = strlen(env->envp[i]) + 1;
scratch = skb_put(skb, len);
strcpy(scratch, env->envp[i]);
}
NETLINK_CB(skb).dst_group = 1;
retval = netlink_broadcast_filtered(uevent_sock, skb,
0, 1, GFP_KERNEL,
kobj_bcast_filter,
kobj);
/* ENOBUFS should be handled in userspace */
if (retval == -ENOBUFS || retval == -ESRCH)
retval = 0;
} else
retval = -ENOMEM;
}
#endif
mutex_unlock(&uevent_sock_mutex);
通过设置并启动hotplug进程
/* call uevent_helper, usually only enabled during early boot */
if (uevent_helper[0] && !kobj_usermode_filter(kobj)) {
char *argv [3];
argv [0] = uevent_helper;
argv [1] = (char *)subsystem;
argv [2] = NULL;
retval = add_uevent_var(env, "HOME=/");
retval = add_uevent_var(env,
"PATH=/sbin:/bin:/usr/sbin:/usr/bin");
// 调用用户空间程序,程序名 argv[0], 并把环境变量当作参数传递过去
retval = call_usermodehelper(argv[0], argv,
env->envp, UMH_WAIT_EXEC);
}
如何在Linux内核中执行某些用户态程序或系统命令?
- 在用户态中,可以通过
execve()
实现; - 在内核态,则可以通过
call_usermodehelpere()
实现该功能。
如果您查阅了上述函数的源码实现,就可以发现
call_usermodehelper()
与execve
系统调用最终都会会执行do_execve()
。
uevent_helper常用环境变量:
环境变量 | 说明 |
---|---|
ACTION | 对应kobject_action定义的kobject动作,不过是将枚举转换成了字符串 |
DEVPATH | 被创建或删除的kobject在sysfs中的路径 |
SEQNUM | 热插拔事件,使程序可以区分热插拔事件 |
SUBSYSTEM | 描述子系统的字符串,与class中的name对应。 |
谁是uevent_helper
刚刚我们说了,kobject_uevent_env
会指定uevent_helper
来并执行。搜索源码以后发现uevent_helper
的默认值是/sbin/hotplug"
:
// lib/kobject_uevent.c
char uevent_helper[UEVENT_HELPER_PATH_LEN] = CONFIG_UEVENT_HELPER_PATH;
// .config (默认值)
CONFIG_UEVENT_HELPER_PATH="/sbin/hotplug"
但文件系统中找不到hotplug
。
通过在if (uevent_helper[0])
添加一句printk("uevent_helper is %s
", uevent_helper );
日志如下:
uevent_helper is /sbin/hotplug
uevent_helper is /sbin/hotplug
// ...
uevent_helper is /sbin/mdev
uevent_helper is /sbin/mdev
看到没,刚开始确实是/sbin/hotplug,但后来就变成了/sbin/mdev。
结论:在系统启动后,大部分的设备已经ready,可以根据需要,重新指定一个uevent helper,以便检测系统运行过程中的热拔插事件。
可以通过把helper的路径写入到/sys/kernel/uevent_helper
文件中实现。
有的资料说是将mdev加到
/proc/sys/kernel/hotplug_helper
,其实这两个是一样的,但为了确保proc
子系统只提供给进程使用,因此新的系统应该优先使用sys
子系统
例如:
# /etc/init.d/rcS
echo /sbin/mdev > /sys/kernel/hotplug # 重新指定了处理uevent的上层应用程序
实际上,内核通过sysfs文件系统的形式,将uevent_helper数组开放到用户空间,供用户空间程序修改访问。
在早期版本的内核中,
uevent helper
是通过CONFIG_UEVENT_HELPER_PATH
配置项来静态指定uevent helper。但这种方式会为每个event fork一个进程,随着内核支持的设备数量的增多,这种方式在系统启动时将会是致命的(存在内存溢出的风险等)。现在不推荐使用这种方式,因此内核编译时,需要把该配置项留空。
至于为什么用户空间能够修改uevent_helper
,实际上是由"kernel/ksysfs.c
”实现的,这里不再详细描述。
mdev
概述
熟悉linux驱动程序编写的人都知道,需要在/dev下建立设备文件,但是如果用LDDM来写驱动程序可能就看不到熟悉的mknod,modprobe等了,这些操作并非消失了,而是由其他机制代替人工做了。
大家都知道创建设备节点的工作是在用户空间进行的,为什么不能由驱动直接创建呢?
试想,如果创建设备由驱动程序来做,驱动位于内核层,如果由其负责这个任务,那么驱动就得知道它要创建的设备名。
简单的字符驱动还好,如果是USB等可插拔的设备,驱动怎么知道自己要创建什么设备名呢?
有人说可以写明一套规则。确实如此,但如果把这套规则放到应用层,由应用程序开发人员去明确这个规则(mdev正是这样做的),会不会更好?
因为是应用程序直接编程访问这个设备名对应的设备驱动的。所以设备驱动不应该直接负责设备文件的创建。
用户层创建设备文件也有两种方法:
- 用户在shell中使用mknod命令创建设备文件,同时传入设备名和设备号。这应该是大家最熟悉的一种方法,但是这种人工的做法,很不科学。它只是一种演示的方法,不适于作为工程方法。
- 利用设备驱动模型来辅助创建设备文件(这也是设备模型的作用之一)。
udev和mdev就是使用设备驱动模型来自动创建设备文件的。
- udev是构建在linux的sysfs之上的,是一个用户程序,它能够根据系统中的硬件设备的状态动态更新设备文件。
- mdev是busybox自带的一个简化版的udev,它比udev占用的内存更小,因此更适合嵌入式系统的应用。
udev和mdev都依赖uevent机制,个人理解,udev使用netlink机制,mdev使用kmod机制。
在分析kobj_uevent_env的argv成员是已经提到了,kmod最终会调用用户程序,即uevent_helper处理uevent消息,在嵌入式中,mdev通常就是uevent_helper
程序。
我们接下来介绍mdev,udev等的原理也是一样的。
mdev是busybox提供的一个工具,用在嵌入式系统中,相当于简化版的udev,作用是在系统启动和热插拔或动态加载驱动程序时, 自动创建设备节点。
在加载驱动过程中,根据驱动程序,在/dev
下自动创建设备节点。
文档说明
# docs/mdev.txt
Mdev has two primary uses: initial population and dynamic updates. Both
require sysfs support in the kernel and have it mounted at /sys. For dynamic
updates, you also need to have hotplugging enabled in your kernel.
Here's a typical code snippet from the init script:
[0] mount -t proc proc /proc
[1] mount -t sysfs sysfs /sys
[2] echo /sbin/mdev > /proc/sys/kernel/hotplug
[3] mdev -s
Alternatively, without procfs the above becomes:
[1] mount -t sysfs sysfs /sys
[2] sysctl -w kernel.hotplug=/sbin/mdev
[3] mdev -s
Of course, a more "full" setup would entail executing this before the previous
code snippet:
[4] mount -t tmpfs -o size=64k,mode=0755 tmpfs /dev
[5] mkdir /dev/pts
[6] mount -t devpts devpts /dev/pts
The simple explanation here is that [1] you need to have /sys mounted before
executing mdev. Then you [2] instruct the kernel to execute /sbin/mdev whenever
a device is added or removed so that the device node can be created or destroyed.
Then you [3] seed /dev with all the device nodes that were created while the system
was booting.
For the "full" setup, you want to [4] make sure /dev is a tmpfs filesystem
(assuming you're running out of flash). Then you want to [5] create the
/dev/pts mount point and finally [6] mount the devpts filesystem on it.
mdev -s
执行mdev -s命令时,mdev扫描/sys/class/block
(块设备保存在/sys/block
)目录下的dev属性文件。
从内核2.6.25版本以后,块设备不再保存于
/sys/block
目录下。mdev扫描/sys/block
是为了实现兼容历史版本的早期驱动。
由于dev属性文件以”major:minor”
形式保存设备编号,因此mdev能够从该dev 属性文件中获取到设备编号;
并以包含该dev属性文件的目录名称作为设备名 device_name
,即:包含dev属性文件的目录称为device_name,
而/sys/class
和device_name
之间的那部分目录称为 subsystem。
也就是每个dev属性文件所在的路径都可表示为
/sys/class/subsystem/<device_name>/dev
在 /dev目录下创建相应的设备文件。
例如,cat /sys/class/tty/tty0/dev
会得到4:0
,subsystem
为tty
、device_name
为tty0
。
uevent调用的mdev
当mdev因uevnet事件(以前叫hotplug事件)被调用时,mdev通过由uevent事件传递给它的环境变量获取到:引起该uevent 事件的设备action
及该设备所在的路径device path
。
然后判断引起该uevent事件的action是什么:
- 若该action是add,即有新设备加入到系统中,不管该设备是虚拟设备还是实际物理设备,mdev都会通过device path路径下的dev属性文件获取到设备编号,然后以device path路径最后一个目录(即包含该dev属性文件的目录)作为设备名,在/dev目录下创建相应的设备文件。
- 若该action是remove,即设备已从系统中移除,则删除/dev目录下以device path路径最后一个目录名称作为文件名的设备文件。
- 如果该action既不是add也不是remove,mdev则什么都不做。
由上面可知,如果我们想在设备加入到系统中或从系统中移除时,由mdev自动地创建和删除设备文件,那么就必须做到以下三点:
1、在/sys/class
的某一subsystem目录下,创建一个以设备名device_name作为名称的目录
2、并且在该device_name目录下还必须包含一个 dev属性文件,该dev属性文件以”major:minor
”形式输出设备编号。
那么,实际上,mdev做了什么呢?
来看一下 busybox的源码,版本:May 2021 -- BusyBox 1.33.1 (stable)
mdev_main
由于新版本的busybox比较复杂,我们看一个比较老的版本
// util-linux/mdev.c
int mdev_main(int argc UNUSED_PARAM, char **argv)
{
// ...
xchdir("/dev"); // 先把目录改变到/dev下
if (argv[1] && strcmp(argv[1], "-s") == 0) { // 在文件系统启动的时候会调用 mdev -s,创建所有驱动设备节点
putenv((char*)"ACTION=add"); // mdev -s 的动作是创建设备节点,所以为add
if (access("/sys/class/block", F_OK) != 0) { // 当/sys/class/block目录不存在时,才扫描/sys/block
/* Scan obsolete /sys/block only if /sys/class/block
* doesn't exist. Otherwise we'll have dupes.
* Also, do not complain if it doesn't exist.
* Some people configure kernel to have no blockdevs.
*/
recursive_action("/sys/block",
ACTION_RECURSE | ACTION_FOLLOWLINKS | ACTION_QUIET,
fileAction, dirAction, temp, 0);
}
/*
* 这个函数是递归函数,它会扫描/sys/class目录下的所有文件,如果发现dev文件,将按照
* /etc/mdev.conf文件进行相应的配置。如果没有配置文件,那么直接创建设备节点
* 最终调用的创建函数是 make_device
*/
recursive_action("/sys/class",
ACTION_RECURSE | ACTION_FOLLOWLINKS,
fileAction, dirAction, temp, 0);
} else{
// 获得环境变量,环境变量是内核在调用mdev之前设置的
env_devname = getenv("DEVNAME"); /* can be NULL */
G.subsystem = getenv("SUBSYSTEM");
action = getenv("ACTION");
env_devpath = getenv("DEVPATH");
snprintf(temp, PATH_MAX, "/sys%s", env_devpath);
make_device(env_devname, temp, op);
}
}
由以上代码分析可知,无论对于何种操作,最后都是调用make_device
make_device
make_device最终完成了创建/移除驱动节点并执行指定的命令的操作。
/* mknod in /dev based on a path like "/sys/block/hda/hda1"
* NB1: path parameter needs to have SCRATCH_SIZE scratch bytes
* after NUL, but we promise to not mangle it (IOW: to restore NUL if needed).
* NB2: "mdev -s" may call us many times, do not leak memory/fds!
*
* device_name = $DEVNAME (may be NULL)
* path = /sys/$DEVPATH
*/
static void make_device(char *device_name, char *path, int operation)
{
int major, minor, type, len;
//path_end指定path结尾处
char *path_end = path + strlen(path);
/* Try to read major/minor string. Note that the kernel puts
after
* the data, so we don't need to worry about null terminating the string
* because sscanf() will stop at the first nondigit, which
is.
* We also depend on path having writeable space after it.
*/
/* 读取 主/次设备号 */
major = -1;
if (operation == OP_add) {
// 往path结尾处拷贝“/dev”,这时path=/sys/class/test/test_dev/dev
strcpy(path_end, "/dev");
// 打开并读取/sys/class/test/test_dev/dev
len = open_read_close(path, path_end + 1, SCRATCH_SIZE - 1);
*path_end = ' ';
if (len < 1) {
if (!ENABLE_FEATURE_MDEV_EXEC)
return;
/* no "dev" file, but we can still run scripts
* based on device name */
// 通过sscanf从/sys/class/test/test_dev/dev获得主次设备号
// 因为 cat /sys/class/test/test_dev/dev 能够得到 '主设备号:次设备号' 这样子的结果
} else if (sscanf(path_end + 1, "%u:%u", &major, &minor) == 2) {
dbg1("dev %u,%u", major, minor);
} else {
major = -1;
}
}
/* else: for delete, -1 still deletes the node, but < -1 suppresses that */
/* Determine device name */
// ...
/* Determine device type */
// ...
#if ENABLE_FEATURE_MDEV_CONF
// 如果 /etc/mdev.conf 有这个配置文件的话,根据配置文件的规则来 创建设备节点 并执行一些命令
// ...
#endif
for (;;) {
const char *str_to_match;
regmatch_t off[1 + 9 * ENABLE_FEATURE_MDEV_RENAME_REGEXP];
char *command;
char *alias;
char aliaslink = aliaslink; /* for compiler */
char *node_name;
const struct rule *rule;
str_to_match = device_name;
rule = next_rule();
#if ENABLE_FEATURE_MDEV_CONF
// ...
#endif
/* Build alias name */
alias = NULL;
if (ENABLE_FEATURE_MDEV_RENAME && rule->ren_mov) {
// ...
}
dbg3("alias:'%s'", alias);
// 解析命令
command = NULL;
IF_FEATURE_MDEV_EXEC(command = rule->r_cmd;)
if (command) {
/* Are we running this command now?
* Run @cmd on create, $cmd on delete, *cmd on any
*/
if ((command[0] == '@' && operation == OP_add)
|| (command[0] == '$' && operation == OP_remove)
|| (command[0] == '*')
) {
command++;
} else {
command = NULL;
}
}
dbg3("command:'%s'", command);
// ...
// 如果动作是 ADD ,则在 /dev/ 中 创建节点
if (operation == OP_add && major >= 0) {
// ...
if (mknod(node_name, rule->mode | type, makedev(major, minor)) && errno != EEXIST)
bb_perror_msg("can't create '%s'", node_name);
// ...
}
// 如果命令存在,则 执行命令
if (ENABLE_FEATURE_MDEV_EXEC && command) {
/* setenv will leak memory, use putenv/unsetenv/free */
char *s = xasprintf("%s=%s", "MDEV", node_name);
putenv(s);
dbg1("running: %s", command);
if (system(command) == -1)
bb_perror_msg("can't run '%s'", command);
bb_unsetenv_and_free(s);
}
// 如果动作是REMOVE ,则在 /dev/ 中 移除节点
if (operation == OP_remove && major >= -1) {
if (ENABLE_FEATURE_MDEV_RENAME && alias) {
if (aliaslink == '>') {
dbg1("unlink: %s", device_name);
unlink(device_name);
}
}
dbg1("unlink: %s", node_name);
unlink(node_name);
}
/* We found matching line.
* Stop unless it was prefixed with '-'
*/
if (!ENABLE_FEATURE_MDEV_CONF || !rule->keep_matching)
break;
} /* for (;;) */
}
调试log
附上一次其他网友调试时打印出来的环境变量(基于platform device)。
env[0] ACTION=add
env[1] DEVPATH=/devices/platform/myled
env[2] SUBSYSTEM=platform
env[3] MAJOR=251
env[4] MINOR=0
env[5] DEVNAME=myled
env[6] MODALIAS=platform:myled
env[7] SEQNUM=642
env[8] HOME=/
env[9] PATH=/sbin:/bin:/usr/sbin:/usr/bin
附录:基于LDDM的设备
什么都不依赖的单纯设备,它的父设备是NULL,会在出现在 /sys/devices/
目录下
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <mach/regs-gpio.h>
#include <mach/hardware.h>
#include <linux/device.h>
static int first_drv_open(struct inode *inode, struct file *file)
{
return 0;
}
static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
return 0;
}
static struct file_operations first_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = first_drv_open,
.write = first_drv_write,
};
struct device dev = {
.init_name = "my_first_drv",
.devt = MKDEV(major, 0),
};
int major;
static int first_drv_init(void)
{
major = register_chrdev(0, "first", &first_drv_fops);
device_register(&dev);
return 0;
}
static void first_drv_exit(void)
{
unregister_chrdev(major, "first_drv"); // 卸载
iounmap(gpbcon);
}
module_init(first_drv_init);
module_exit(first_drv_exit);
MODULE_LICENSE("GPL");
测试:
# ls /sys/devices/
my_first_drv platform system virtual
# ls /sys/devices/my_first_drv/
dev uevent
附录:mdev.conf 文档
作者@韦东山, 介绍了如何 编写/etc/mdev.conf
。
格式
<device regex> <uid>:<gid> <octal permissions> [<@|$|*> <command>]
- device regex:正则表达式,表示哪一个设备
- uid: owner
- gid: 组ID
- octal permissions:以八进制表示的属性
- @:创建设备节点之后执行命令
- $:删除设备节点之前执行命令
- *: 创建设备节点之后 和 删除设备节点之前 执行命令
- command:要执行的命令
范例
前提:韦东山老师写了个驱动,有 led led1 led2 led3 这四个设备。
写法1
指定4个设备,全部设为 777权限
leds 0:0 777
led1 0:0 777
led2 0:0 777
led3 0:0 777
写法2
基于正则表达式
leds?[123]? 0:0 777
写法3
在2的基础上,指定在 设备创建后,执行脚本
leds?[123]? 0:0 777 @ echo create /dev/$MDEV > /dev/console
写法4
类似3,但是使用了环境变量$ACTION
leds?[123]? 0:0 777 * if [ $ACTION = "add" ]; then echo create /dev/$MDEV > /dev/console; else echo remove /dev/$MDEV > /dev/console; fi
写法5
将命令写到文件中执行
leds?[123]? 0:0 777 * /bin/add_remove_led.sh
脚本的内容是:
#!/bin/sh
if [ $ACTION = "add" ];
then
echo create /dev/$MDEV > /dev/console;
else
echo remove /dev/$MDEV > /dev/console;
fi