linux 内核 :uevent原理分析

转 :https://blog.csdn.net/W1107101310/article/details/80211885

第一部分:预备知识和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。
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、device_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所占用的meomry空间。这就决定了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需要用比较巧妙(也很啰嗦)的手段去实现。

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因uevnet事件(以前叫hotplug事件)被调用时,mdev通过由uevent事件传递给它的环境变量获取到:引起该uevent 事件的设备action及该设备所在的路径device path。然后判断引起该uevent事件的action是什么。并根据action的不同做相应操作。若该action是add,即有新设备加入到系统中,不管该设备是虚拟设备还是实际物理设备,mdev都会通过device path路径下的dev属性文件获取到设备编号,然后以device path路径最后一个目录(即包含该dev属性文件的目录)作为设备名,在/dev目录下创建相应的设备文件。若该action是remote,即设备已 从系统中移除,则删除/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机制创建设备节点

下面是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函数后,他们就不一样了,下面我们分析kobject_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。

但这种方式会为每个event 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相应的程序在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/weixin_43836778/article/details/90793151