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!\n", 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!\n",
                 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!\n",
                 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\n", 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\n”形式输出设备编号。

那么,实际上,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 \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 (;;) */
}

调试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
posted @ 2021-06-20 13:26  schips  阅读(6297)  评论(0编辑  收藏  举报