zoukankan      html  css  js  c++  java
  • Linux 内核:设备驱动模型(4)uevent与热插拔

    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盘自动挂载。

    参考文章:

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

    也就是每个dev属性文件所在的路径都可表示为/sys/class/subsystem/<device_name>/dev

    在 /dev目录下创建相应的设备文件。

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

    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
    
    如果说我的文章对你有用,只不过是我站在巨人的肩膀上再继续努力罢了。
    若在页首无特别声明,本篇文章由 Schips 经过整理后发布。
    博客地址:https://www.cnblogs.com/schips/
  • 相关阅读:
    jQuery动态效果学习笔记
    jQuery HTML操作学习笔记
    学习编程有必要做笔记吗?
    2017阿里巴巴面试技术挑战赛试题泄露
    来自IOS开发工程师的零基础自学HTML5经验分享
    前端学习笔记之HTML DOM操作
    web前端攻城狮整理的收藏夹
    WEB前端开发初学者必看的学习路线(附思维导图)
    搜狗员工用百度算什么,谷歌员工当着老板的面用bing,结果悲剧了!
    常用站长工具软件汇总,有没有一款你在用?
  • 原文地址:https://www.cnblogs.com/schips/p/linux_device_model_4.html
Copyright © 2011-2022 走看看