[Linux 基础] 嵌入式 Linux ---- uevent 机制:uevent 原理分析(深度好文)

简介:

本文主要介绍 uevent 机制是什么,并通过代码分析使用 uevent 机制生成设备节点的过程。而本文将分为两部分,第一部分我们介绍一些预备知识和 uevent 的原理,而第二部分——通过代码介绍使用 uevent 机制创建设备节点。

Linux 内核:linux-2.6.22.6

所用开发板:JZ2440 V3(S2C2440A)

声明:

本文主要是看完韦东山老师视频并结合一些博客内容所写,因此文中可能会有其他文章中的内容,如果你觉得我的文章对你构成了侵犯,您可以告诉我,我会对文章进行改正,同时如果文中可能有不正确的地方,敬请指正。谢谢。

第一部分:预备知识和 uevent 的原理

下面我们开始讲解预备只是。在讲解之前,我们先来看一个介绍 uevent 机制的框架图:

                  该图片来自:Linux设备模型(3)_Uevent

我们很多人可能不清楚:uevent 机制是什么,uevent 机制到底做了什么工作?它的哪些方面是值得我们研究的?

我们在刚学习驱动程序的时候并没有使用 uevent 机制,也就是说我们在程序中没有用到 class_create 和 class_device_create 函数,来自动的在用户空间为设备驱动创建设备节点。那时候我们要手动的在用户空间使用 mknod 命令来创建设备节点。而当我们使用 class_create 和 class_device_create 函数后,它们会为我们在用户空间创建设备节点而不用我们再手动的去完成这项工作。而这就是我们对于 uevent 机制的宏观认识。而我们知道我们的设备节点是为设备驱动所创建的,而设备 device 和驱动 driver 都是以链表的形式连接在总线 bus 上的,而 设备——驱动——总线 的更上一层就是 sysfs 层。因此这就引出了我们就要介绍一个组合了:sysfs+mdev。而这对组合将为我们解释 uevent 机制的原理。我们先来了解 sysfs。

1.1、sysfs

sysfs 是一个基于内存的虚拟文件系统,有 kernel 提供,挂载到 /sys 目录下(用 mount 查看得到 sysfs on /sys type sysfs(rw,nosuid,nodev,noexec,relatime)),负责以设备树的形式向 user namespace 提供直观的设备和驱动信息。同时 sysfs 以不同的视角为我们展示当前系统接入的设备:

  • /sys/block 历史遗留问题,存放块设备,提供以设备名(如 sda)到 /sys/devices 的符号链接;
  • /sys/bus 按总线类型分类,在某个总线目录下可以找到连接该总线的设备的符号链接,指向 /sys/devices。某个总线目录之下的 drivers 目录抱哈了该总线所需的所有驱动的符号链接对应 kernel 中的 struct bus_type;
  • /sys/class 按设备功能分类,如输入设备在 /sys/class/input 之下,图形设备在 /sys/class/graphics 之下,是指向 /sys/devices 目录下对应设备的符号链接对应 kernel 中的 struct class;
  • /sys/dev  按设备驱动程序分层(字符设备/块设备),提供以 major:minor 为名到 /sys/devices 的符号链接 对应 kernel 中的 struct device_driver;
  • /sys/devices 包含所有被发现的注册在各种总线上的各种物理设备。所有的物理设备都按其在总线上的拓扑结构来显示,除了 platform devices 和 system devices。platform devices 一般是关在芯片内部告诉或者低速总线上的各种控制器和外设,能被 CPU 直接寻址。system devices 不是外设,它是吸盘内部的核心结构,比如 CPU,timer 等,它们一般没有相关的 driver,但是会有一些体系结构相关的代码来配置它们对应 kernel 中的 struct device。

上面展现了在 sys 目录下总线、设备、驱动和类所对应的文件,而它们的关系为

  • device 用于描述各种设备,其保存了所有的设备信息;
  • driver 用于驱动 device,其保存了所有能够被它所驱动的设备链表;
  • bus 是连接 CPU 和 device 的桥梁,其保存了所有挂载在它上面的设备链表和驱动这些设备的驱动链表;
  • class 用于描述一类 device,其保存了所有该类 device 的设备链表

下面我们介绍总线、设备、驱动和类更下面一层的结构体。sysfs 的功能基于 Linux 的统一设备模型,它有以下结构体构成:kobject、kset、ktype。同时我们从上面框图中可以看出 uevent 是在 kobject 结构体的基础上实现的。

kobject:统一设备模型中最基本的对象。

struct kobject {
 	const char *name;  //name,该Kobject的名称,同时也是sysfs中的目录名称。
			    //由于Kobject添加到Kernel时,需要根据名字注册到sysfs中,之后就不能再直接修改该字段。
			   //如果需要修改Kobject的名字,需要调用kobject_rename接口,该接口会主动处理sysfs的相关事宜。
        struct list_head    entry; //entry,用于将Kobject加入到Kset中的list_head。 
        struct kobject      *parent; //parent,指向parent kobject,以此形成层次结构(在sysfs就表现为目录结构)。
        struct kset     *kset; //kset,该kobject属于的Kset。可以为NULL。
				//如果存在,且没有指定parent,则会把Kset作为parent
				//(别忘了Kset是一个特殊的Kobject)。
        struct kobj_type    *ktype;  //ktype,该Kobject属于的kobj_type。每个Kobject必须有一个ktype,或者Kernel会提示错误。
        struct sysfs_dirent *sd;   //sd,该Kobject在sysfs中的表示。
 
        struct kref     kref;  //kref,"struct kref”类型(在include/linux/kref.h中定义)的变量,为一个可用于原子操作的引用计数。
        unsigned int state_initialized:1; //state_initialized,指示该Kobject是否已经初始化,
					  //以在Kobject的Init,Put,Add等操作时进行异常校验。
        unsigned int state_in_sysfs:1;   //state_in_sysfs,指示该Kobject是否已在sysfs中呈现,以便在自动注销时从sysfs中移除。	
        unsigned int state_add_uevent_sent:1;  // state_add_uevent_sent/state_remove_uevent_sent,记录是否已经向用户空间发送ADD uevent,
						//如果有,且没有发送remove uevent,则在自动注销时,补发REMOVE uevent,
						//以便让用户空间正确处理。
        unsigned int state_remove_uevent_sent:1;
        unsigned int uevent_suppress:1;  //uevent_suppress,如果该字段为1,则表示忽略所有上报的uevent事件。
    };

:Uevent 提供了 “用户空间通知” 的功能实现,通过该功能,当内核中有 kobject 的增加、删除、修改等动作是,会通知用户空间。

Ktype:代表 Kobject(严格地讲,是包含了 Kobject 的数据结构)的属性操作集合(由于通用性,多个 Kobject 可能共用同一个属性操作集,因此把 Ktype 独立出来了)。

struct kobj_type {
     void (*release)(struct kobject *kobj); //release,通过该回调函数,可以将包含该种类型kobject的数据结构的内存空间释放掉。
     const struct sysfs_ops *sysfs_ops;  //sysfs_ops,该种类型的Kobject的sysfs文件系统接口。
     struct attribute **default_attrs; //default_attrs,该种类型的Kobject的atrribute列表
					//(所谓attribute,就是sysfs文件系统中的一个文件)。
					//将会在Kobject添加到内核时,一并注册到sysfs中。
     const struct kobj_ns_type_operations *(*child_ns_type)(struct kobject *kobj); 
					//child_ns_type/namespace,和文件系统(sysfs)的命名空间有关
     const void *(*namespace)(struct kobject *kobj);
 };

实际上这里实现的类似于对 Kobject 的派生,包含不同 kobj_type 的 kobject 可以看作不同的子类。通过实现相同的函数来实现多态。在这样的设计下,每一个内嵌 kobject 的数据结构(如 kset、device、deivce_driver 等),都要实现自己的 kobj_type,并定义其中的回调函数。

Kset:一个特殊的 Kobject(因此它也会在 “/sys/” 文件系统中以目录的形式出现),它用来集合相似的 Kobject(这些 Kobject 可以是相同属性的,也可以不同属性的)

struct kset {
     struct list_head list; //list用于保存该kset下所有的kobject的链表。
     spinlock_t list_lock;  //list自旋锁
     struct kobject kobj;  //kobj,该kset自己的kobject(kset是一个特殊的kobject,也会在sysfs中以目录的形式体现)。
     const struct kset_uevent_ops *uevent_ops; //uevent_ops,该kset的uevent操作函数集。
						//当任何Kobject需要上报uevent时,都要调用它所从属的kset的uevent_ops,添加环境变量,
						//或者过滤event(kset可以决定哪些event可以上报)。
						//因此,如果一个kobject不属于任何kset时,是不允许发送uevent的。
 };

而 kset 的 uevent 操作回调函数为:

struct kset_uevent_ops {
     int (* const filter)(struct kset *kset, struct kobject *kobj);
		//filter,当任何Kobject需要上报uevent时,它所属的kset可以通过该接口过滤,
		//阻止不希望上报的event,从而达到从整体上管理的目的。
     const char *(* const name)(struct kset *kset, struct kobject *kobj);
		//name,该接口可以返回kset的名称。如果一个kset没有合法的名称,
		//则其下的所有Kobject将不允许上报uvent
     int (* const uevent)(struct kset *kset, struct kobject *kobj,
         struct kobj_uevent_env *env);
	//uevent,当任何Kobject需要上报uevent时,它所属的kset可以通过该接口统一为这些event添加环境变量。
	//因为很多时候上报uevent时的环境变量都是相同的,因此可以由kset统一处理,就不需要让每个Kobject独自添加了。
 };

注意 Kset 和 Ktype 的关联,Kobject 会利用成员 kset 找到自己所属的 kset,然后才设置自身的 ktype 为 kobj.ktype。当没有指定 kset 成员时,才会用到 ktype 来建立关系。

由于 kobject 调用的是它所属 kset 的 uevent 操作函数,所以 kset 可以对其行为进行控制。如果 kobject 不属于任何 kset,则无法发送 uevent。

总结,Ktype 以及整个 Kobject 机制的理解:

Kobject 的核心功能是:保持一个引用计数,当该计数减为 0 时,自动释放(由本文所讲的 kobject 模块负责)Kobject 所占用的 memory 空间。这就决定了 Kobject 必须是动态分配的(只有这样才能动态释放)

而 Kobject 大多数的使用场景,是内嵌在大型的数据结构中(如 kset、device_driver 等),因此这些大型的数据结构,也必须是动态分配、动态释放的那么释放的时机是什么呢?是内嵌的 Kobject 释放时。但是 Kobject 的释放是由 Kobject 模块自动完成的(在引用计数为 0 时),那么怎么一并释放包含自己的大型数据结构呢?

这是 Ktype 就派上用场了。我们知道,Ktype 的 release 回调函数负责释放 Kobject(甚至是包含 Kobject 的数据结构)的内存空间,那么 Ktype 及其内部函数,是由谁实现的呢?是由上层数据结构所在的模块!因为只有它,才清楚 Kobject 嵌在哪个数据结构中,并通过 Kobject 指针以及自身的数据结构类型,找到需要释放的上层数据结构的指针,然后释放它

讲到这里,就清晰多了。所以每一个内嵌 Kobject 的数据结构,例如 kset、device、device_driver 等等,都要实现一个 Ktype,并定义其中的回调函数。同理,sysfs 相关的操作也一样,必须经过 ktype 的中转,因为 sysfs 看到的是 Kobject,而真正的文件操作的主体,是内嵌 Kobject 的上层数据结构!

顺便提一下,Kobject 是面向对象的思想在 Linux Kernel 中的极致体现,但 C 语言的优势却不在这里,所以 Linux Kernel 需要用比较巧妙(也很啰嗦)的手段去实现。

1.2、mdev 原理

上面我们分析了 sysfs,下面我们就开始分析 mdev,我们通过分析 mdev 了解它与 sysfs 的关系。mdev 在 busybox 的代码包中能找到,位于 busybox/util-linux/mdev.c 文件中,他通过 uevent_helper 函数被调用。在 mdev 中主要完成两件事情:

第一件事:

执行 mdev -s 命令时,mdev 扫描 /sys/block(块设备保存在 /sys/block 目录下,内核 2.6.25 版本以后,块设备也保存在 /sys/class/block 目录下。mdev 扫描 /sys/block 是为了实现向后兼容)和 /sys/class 两个目录下的 dev 属性文件,从该 dev 属性文件中获取到设备编号(dev 属性文件以 “major:minor\n” 形式保存设备编号),并以包含该 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。

第二件事:

当 mdev 因 uevent 事件(以前叫 hotplug 事件)被调用时,mdev 通过 uevent 事件传递给它的环境变量获取到:引起该 uevent 事件的设备 action 及该设备所在的路径 device path。然后判断引起该 uevent 事件的 action 是什么。并根据 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 目录下;
  2. 创建一个以设备名 device_name 作为名称的目录;
  3. 并且在该 device_name 目录下还必须包含一个 dev 属性文件,该 dev 属性文件以 “major:minor\n” 形式输出设备编号。

而从上面这些内容我们可以知道,sysfs 为 uevent 机制做前期的准备工作,即创建相应的目录,而 mdev 则是在 sysfs 的基础上通过调用 sysfs 创建的目录或文件来实现设备节点的创建。

第二部分:结合代码介绍使用 uevent 机制创建设备节点

现在我们就要结合代码分析 uevent 机制了,而要分析这个机制我们就要从 class_create 和 class_device_create 这两个函数来分析这个过程是怎么实现的。我们现在先分析 class_create:

class_create(THIS_MODULE,"buttonsdrv");
    class_register(cls);
        kobject_set_name(&cls->subsys.kobj, "%s", cls->name); //将类的名字led_class赋值给对应的kset
	subsys_set_kset(cls, class_subsys);
	subsystem_register(&cls->subsys);
	    kset_register(s);  //创建class设备类目录
		kset_add(k);
	            kobject_add(&k->kobj);
			kobject_shadow_add(kobj, NULL);
		            parent = kobject_get(kobj->parent); // parent即class_kset.kobj,即/sysfs/class对应的目录
			    list_add_tail(&kobj->entry,&kobj->kset->list);
			    create_dir(kobj, shadow_parent);  //创建一个class设备类目录
				sysfs_create_dir(kobj, shadow_parent); //该接口是sysfs文件系统接口,代表创建一个目录,不再展开。

从上面我们可以看出 kobject 在 sysfs 对应的是目录(dir),当我们注册一个 kobject 时,会调用 kobject_add(&k->kobj); 然后在其后创建 class 设备目录。而同时我们可以看出 class_create 函数是为 class_device_create 函数做了目录的准工作。

而从文章开始介绍的图片我们知道当 kobject 的状态发生改变(如:add,remove 等)时,会通知用户空间,用户空间接收到事件通知后可以做相应的处理。

而 uevent 把时间上报给用户空间有两种途径:

  1. 通过 kmod 模块,直接调用用户空间的可执行程序或脚本;
  2. 通过 netlink 通信机制,将事件从内核空间传递到用户空间;

而本文主要讲解通过 kmod 模块,直接调用用户空间的可执行程序或脚本。而通过 kobject.h,uevent 模块提供了如下的 API(这些 API 的实现是在 “lib/kobject_uevent.c” 文件中):

int kobject_uevent(struct kobject *kobj, enum kobject_action action);
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
                         char *envp[]);
  
int add_uevent_var(struct kobj_uevent_env *env, const char *format, ...);
   
int kobject_action_type(const char *buf, size_t count,enum kobject_action *type);

下面我们从 class_device_create 函数开始分析,看他是如何走到 kobject_uevent 函数的。我们看 class_device_create 函数的层级关系:

class_device_create(buttonsdrv_class,NULL,MKDEV(auto_major,0),NULL,"buttonsdrv");
	class_device_register(class_dev);
		class_device_add(class_dev);
			class_dev = class_device_get(class_dev);
			parent_class = class_get(class_dev->class);
			parent_class_dev = class_device_get(class_dev->parent);
			kobject_set_name(&class_dev->kobj, "%s", class_dev->class_id);
			kobject_add(&class_dev->kobj);
			class_device_create_file(class_dev, attr);
			class_device_add_groups(class_dev);
			make_deprecated_class_device_links(class_dev);
			kobject_uevent(&class_dev->kobj, KOBJ_ADD);

从前面的代码看 class_create 和 class_device_create 做了很多相似的工作——就是创建目录,而当到 kobject_uevent 函数后,它们就不一样了,下面我们分析 kobjec_uevent 函数。

/**
 * 通过终端事件通知用户层
 *
 * @action: 发生的事件 (通常是 KOBJ_ADD 和 KOBJ_REMOVE)
 * @kobj: 事件发生的kobject 结构体
 *
 */
int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{
	return kobject_uevent_env(kobj, action, NULL);
}

他调用了 kobject_uevent_env 函数,而上面的介绍中我们知道了要发送事件,那么都有什么事件呢?

我们看 linux-3.5/include/linux/kobject.h

 enum kobject_action {   
     KOBJ_ADD,  //ADD/REMOVE,Kobject(或上层数据结构)的添加/移除事件。
     KOBJ_REMOVE,    
     KOBJ_CHANGE, //CHANGE,Kobject(或上层数据结构)的状态或者内容发生改变。
		  //CHANGE,如果设备驱动需要上报的事件不再上面事件的范围内,
		  //或者是自定义的事件,可以使用该event,并携带相应的参数。
     KOBJ_MOVE,  //MOVE,Kobject(或上层数据结构)更改名称或者更改Parent(意味着在sysfs中更改了目录结构)。
     KOBJ_ONLINE, //ONLINE/OFFLINE,Kobject(或上层数据结构)的上线/下线事件,其实是是否使能。
     KOBJ_OFFLINE,
     KOBJ_MAX 
 };

下面我们接着分析 kobject_uevent_env 函数

/**
 * 发送一个带有环境变量的事件
 *
 * @action: 发生的事件(通常为KOBJ_MOVE)
 * @kobj: 事件发生的kobject结构体
 * @envp_ext: 环境变量数据指针
 *
 */
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
			char *envp_ext[])
{
	action_string = action_to_string(action);
 
	/* 查找当前kobject或其parent是否从属于某个kset;如果都不从属于某个kset,则返回错误。(说明一个kobject若没有加入kset,是不会上报uevent的) */
	top_kobj = kobj;
	while (!top_kobj->kset && top_kobj->parent) {
		top_kobj = top_kobj->parent;
	}
	if (!top_kobj->kset) {
		pr_debug("kobject attempted to send uevent without kset!\n");
		return -EINVAL;
	}
 
	kset = top_kobj->kset;
	uevent_ops = kset->uevent_ops;
 
	/*  如果所属的kset有uevent_ops->filter,则调用该函数,若该函数返回0,则过滤此次上报。(kset 可以通过filter接口过滤不希望上报的event) */
	if (uevent_ops && uevent_ops->filter)
		if (!uevent_ops->filter(kset, kobj)) {
			pr_debug("kobject filter function caused the event to drop!\n");
			return 0;
		}
 
	/*判断所属的kset是否有合法的名称,若uevent_ops->name存在就用其返回的名称作为subsystem;若uevent_ops->name不存在就用kset本身的kobject的名称作为subsystem;若没有合法的名称,则不上报uevent */
	if (uevent_ops && uevent_ops->name)
		subsystem = uevent_ops->name(kset, kobj);
	else
		subsystem = kobject_name(&kset->kobj);
	if (!subsystem) {
		pr_debug("unset subsytem caused the event to drop!\n");
		return 0;
	}
 
	/* 分配一个此次上报的环境变量 */
	envp = kzalloc(NUM_ENVP * sizeof (char *), GFP_KERNEL);
	if (!envp)
		return -ENOMEM;
 
	/*分配一个此次上报的用于保存环境变量的buffer, */
	buffer = kmalloc(BUFFER_SIZE, GFP_KERNEL);
	if (!buffer) {
		retval = -ENOMEM;
		goto exit;
	}
 
	/* 获得该kobject在sysfs中路径 */
	devpath = kobject_get_path(kobj, GFP_KERNEL);
	if (!devpath) {
		retval = -ENOENT;
		goto exit;
	}
 
	/* uevent_helper的环境变量*/
	envp[i++] = "HOME=/";
	envp[i++] = "PATH=/sbin:/bin:/usr/sbin:/usr/bin";
 
	/* 添加环境变量 */
	scratch = buffer;
	envp [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;
	for (j = 0; envp_ext && envp_ext[j]; j++)
		envp[i++] = envp_ext[j];
	/* just reserve the space, overwrite it after kset call has returned */
	envp[i++] = seq_buff = scratch;
	scratch += strlen("SEQNUM=18446744073709551616") + 1;
 
	/* 如果 uevent_ops->uevent 存在,调用该接口,添加kset统一的环境变量到env指针 */
	if (uevent_ops && uevent_ops->uevent) {
		retval = uevent_ops->uevent(kset, kobj,
				  &envp[i], NUM_ENVP - i, scratch,
				  BUFFER_SIZE - (scratch - buffer));
		if (retval) {
			pr_debug ("%s - uevent() returned %d\n",
				  __FUNCTION__, retval);
			goto exit;
		}
	}
 
	/* 调用add_uevent_var接口,添加格式为"SEQNUM=%llu”的序列号 */
	spin_lock(&sequence_lock);
	seq = ++uevent_seqnum;
	spin_unlock(&sequence_lock);
	sprintf(seq_buff, "SEQNUM=%llu", (unsigned long long)seq);
 
 
	/* 以uevent_helper、 subsystem 以及添加了标准环境变量(HOME=/,PATH=/sbin:/bin:/usr/sbin:/usr/bin)的env指针为参数,调用kmod模块提供的call_usermodehelper函数,上报uevent。 */
	if (uevent_helper[0]) {
		char *argv [3];
 
		argv [0] = uevent_helper;
		argv [1] = (char *)subsystem;
		argv [2] = NULL;
		call_usermodehelper (argv[0], argv, envp, 0);
	}
}

uevent 模块通过 kmod 上报 uevent 时,会通过 call_usermodehelper 函数,调用用户空间的可执行文件(或者脚本,简称 uevent helper)处理该 event。而该 uevent helper 的路径保存在 uevent_helper 数组中。可以在编译内核时,通过 CONFIG_UEVENT_HELPER_PATH 配置项,静态指定 uevent helper。

但这种方式会为每个 uevent fork 一个进程,随着内核支持的设备数量的增多,这种方式在系统启动时将会是致命的(可以导致内存溢出等)。因此只有在早期的内核版本中会使用这种方式,现在内核不再推荐使用该方式。因此内核编译时,需要把该配置项留空。在系统启动后,大部分的设备已经 ready,可以根据需要,重新指定一个 uevent helper,以便检测系统运行过程中的热拔插事件。

这可以通过把  helper 的路径写入到 “/sys/kernel/uevent_helper” 文件中实现。实际上,内核通过 sysfs 文件系统的形式,将 uevent_helper 数组开放到用户空间,供用户空间程序修改访问,具体可参考 “./kernel/ksysfs.c” 中相应的代码。

在 /etc/init.d/rcS 脚本中添加 echo "/sbin/mdev" > /proc/sys/kernel/hotplug,会发现 cat /sys/kernel/uevent_helper 即是 /sbin/mdev。说明 /proc/sys/kernel/hotplug 中可执行文件路径最终还是会写到 /sys/kernel/uevent_helper 中。自己手动 echo "/kernel/main" > uevent_helper(之前的 /sbin/mdev 会被覆盖),当 lsmod、rmmod 时,/sys/kernel/uevent_helper 中的 /kernel/main 会执行,表明时间已经上报给用户空间。

下面我们看在 Busybox 中是如何创建设备节点的。

轮到 mdev 出场了,前面的描述都是在 sysfs 文件系统中创建目录或者文件,而应用程序访问的设备文件则需要创建在 /dev/ 目录下。该项工作由 mdev 完成。

mdev 的原理是解释 /etc/mdev.conf 文件定义的命名设备文件的规则,并在该规则下根据环境变量的要求来创建设备文件。mdev.conf 由用户层指定,因此更具灵活性。本文无意展开对 mdev 配置脚本的分析。相关知识可以看我的翻译:mdev.conf 翻译

mdev 相应的程序在 Busybox/util-linux/mdev.c:

int mdev_main(int argc UNUSED_PARAM, char **argv)
    xchdir("/dev");
    if (argv[1] && strcmp(argv[1], "-s")//系统启动时mdev –s才会执行这个分支
    else
    action = getenv("ACTION");
    env_path = getenv("DEVPATH");
    G.subsystem = getenv("SUBSYSTEM");
    snprintf(temp, PATH_MAX, "/sys%s", env_path);//到/sysfs/devices/led目录
    make_device(temp, /*delete:*/ 0);
    strcpy(dev_maj_min, "/dev"); //读出dev属性文件,得到设备号
    open_read_close(path, dev_maj_min + 1, 64);    
    ….
    mknod(node_name, rule->mode | type, makedev(major, minor)) //最终mknod创建节点

最终我们会跟踪到 mknod 在 /dev 目录下创建了设备文件。

参考文献:

参考文献讲解详细,值得一观!!!----- 感谢原作者的辛苦整理!!!

猜你喜欢

转载自blog.csdn.net/u014674293/article/details/115083363