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/classdevice_name之间的那部分目录称为 subsystem。

例如,cat /sys/class/tty/tty0/dev会得到4:0subsystemttydevice_nametty0

 

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 (;;) */
}

 

posted @ 2023-03-12 19:33  流水灯  阅读(599)  评论(0编辑  收藏  举报