mdev / udev 捕获 uevent 事件,在 /dev 下创建或删除设备文件
每个设备都有一个 uevent 属性文件,里面有设备号和设备名,此文要讲的是 uevent 事件,非 uevent 属性文件
kobject有事件需要上报时,会发送uevent事件,可以通过两个途径把事件上报到用户空间:一种是通过kmod模块,直接调用用户空间的可执行文件;另一种是通过netlink通信机制,将事件从内核空间传递给用户空间。
其中:
-
netlink是一种socket,专门用来进行内核空间和用户空间的通信;
-
kmod是管理内核模块的工具集,类似busybox,我们熟悉的lsmod,insmod等是指向kmod的链接。
uevent事件的描述
struct kobj_uevent_env { char *argv[3]; char *envp[UEVENT_NUM_ENVP]; int envp_idx; char buf[UEVENT_BUFFER_SIZE]; int buflen; }; enum kobject_action { KOBJ_ADD, KOBJ_REMOVE, KOBJ_CHANGE, KOBJ_MOVE, KOBJ_ONLINE, KOBJ_OFFLINE, KOBJ_BIND, KOBJ_UNBIND, }; static const char *kobject_actions[] = { [KOBJ_ADD] = "add", [KOBJ_REMOVE] = "remove", [KOBJ_CHANGE] = "change", [KOBJ_MOVE] = "move", [KOBJ_ONLINE] = "online", [KOBJ_OFFLINE] = "offline", [KOBJ_BIND] = "bind", [KOBJ_UNBIND] = "unbind", };
buf: 记录了所有的键值对(也称环境变量)信息,比如 "ACTION=add DEVPATH=/sys/class/tty/console SUBSYSTEM=20"
buflen: buf已使用的长度
envp: 记录每个键值对在buf的内存首地址
envp_idx: 键值对的总数
发送uevent事件的操作其实是把 struct kobj_uevent_env 实例广播出去。
struct kset 成员变量 uevent_ops
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); };
filter
当任何kobject需要上报uevent时,可以通过它所属的kset过滤,阻止不希望上报的event,从而达到从整体上管理的目的。
name
该接口可以返回kset的名称。如果一个kset没有合法的名称,则其下的所有Kobject将不允许上报uvent。
uevent
当任何kobject需要上报uevent时,可以通过调用它所属的kset的uevent(),添加环境变量ACTION。因为很多时候上报uevent时的环境变量都是相同的,因此可以由kset统一处理,就不需要让每个kobject独自添加了。
设备文件的创建
udev是构建在linux的sysfs之上的,是一个用户程序,它能够根据系统中的硬件设备的状态动态更新设备文件。mdev是busybox自带的一个简化版的udev,它比udev占用的内存更小,因此更适合嵌入式系统的应用。
每个dev属性文件都可表示为/sys/class/<subsystem>/<device_name>/dev,如下图:
dev属性文件以”major:minor”
形式保存设备编号,因此mdev能够从该 dev 属性文件中获取到设备号;并以包含该dev属性文件的文件夹名称作为设备名 device_name
,即:包含dev属性文件的文件夹名称为device_name,而/sys/class
和device_name
之间的那部分目录称为 subsystem。
例如,cat /sys/class/tty/tty0/dev
会得到4:0
,subsystem
为tty
、device_name
为tty0
。
mdev通过由uevent事件传递给它的环境变量获取到:引起该uevent 事件的设备action
及该设备所在的路径device path
。
然后判断引起该uevent事件的action是什么:
- 若该action是add,即有新设备加入到系统中,不管该设备是虚拟设备还是实际物理设备,mdev都会通过device path路径下的dev属性文件获取到设备号,然后以包含该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\n
”形式输出设备编号。
mdev 的源码分析
来看一下 busybox的源码,版本:May 2021 -- BusyBox 1.33.1 (stable)
// 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 \n after * the data, so we don't need to worry about null terminating the string * because sscanf() will stop at the first nondigit, which \n 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 = '\0'; 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 (;;) */ }