uevent是kobject的一部分,用于在kobject状态发生改变时,例如增加、移除等,通知用户空间程序。用户空间程序收到这样的事件后,会做相应的处理。
该机制通常是用来支持热拔插设备的,例如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定义了event的类型,包括:
ADD/REMOVE,kobject(或上层数据结构)的添加/移除事件。
ONLINE/OFFLINE,kobject(或上层数据结构)的上线/下线事件,其实是是否使能。
CHANGE,kobject(或上层数据结构)的状态或者内容发生改变。
MOVE,kobject(或上层数据结构)更改名称或者更改parent(意味着在sysfs中更改了目录结构)。
CHANGE,如果设备驱动需要上报的事件不再上面事件的范围内,或者是自定义的事件,可以使用该event,并携带相应的参数。
前面有提到过,在利用Kmod向用户空间上报event事件时,会直接执行用户空间的可执行文件。而在Linux系统,可执行文件的执行,依赖于环境变量,因此kobj_uevent_env用于组织此次事件上报时的环境变量。
envp,指针数组,用于保存每个环境变量的地址,最多可支持的环境变量数量为UEVENT_NUM_ENVP。
envp_idx,用于访问环境变量指针数组的index。
buf,保存环境变量的buffer,最大为UEVENT_BUFFER_SIZE。
buflen,访问buf的变量。
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就是kobj_uevent_ops结构的。
filter,当任何kobject需要上报uevent时,它所属的kset可以通过该接口过滤,阻止不希望上报的event,从而达到从整体上管理的目的。
name,该接口可以返回kset的名称。如果一个kset没有合法的名称,则其下的所有Kobject将不允许上报uvent
uevent,当任何kobject需要上报uevent时,它所属的kset可以通过该接口统一为这些event添加环境变量。因为很多时候上报uevent时的环境变量都是相同的,因此可以由kset统一处理,就不需要让每个kobject独自添加了。
这些回调函数很简单,不过他们的函数指针原型的定义可以关注一下,他们的原型是这样的:
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);
这里有一堆const,这些const是什么意思,如果搞清楚了相信你对const就有更深入的认识了。算是思考题,原意深入思考的可以解答一下,顺便说一下,面试c语言程序员经常会被问到const的。
当设备加载或卸载时,是怎么通过这几个uevent的核心类通知用户空间的呢?通过前面的分析,大家应该知道,设备加载或卸载最直观的体现在/sys下目录的变化,/sys下的目录和kobject是对应的,因此还得从kobject说起。
这几个类之间的关系比较简单,而且很多都是通过方法产生依赖关系,类之间的关系比较弱。通过序列图可以更清楚的看到类之间如何交互。
环境变量 |
说明 |
ACTION |
对应kobject_action定义的kobject动作,不过是将枚举转换成了字符串 |
DEVPATH |
被创建或删除的kobject在sysfs中的路径 |
SEQNUM |
热插拔事件序列号,使程序可以区分热插拔事件 |
SUBSYSTEM |
描述子系统的字符串,与class中的name对应。 |
熟悉linux驱动程序编写的人都知道,需要在/dev下建立设备文件,但是如果用LDDM来写驱动程序可能就看不到熟悉的mknod,modprobe等了,这些操作并非消失了,而是由其他机制代替人工做了。本章就这方面内容进行一个介绍。
大家都知道创建设备节点的工作是在用户空间进行的,为什么不能由驱动直接创建呢?试想,如果创建设备由驱动程序来做,驱动位于内核层,如果由其负责这个任务,那么驱动就得知道它要创建的设备名。简单的字符驱动还好,如果是USB等可插拔的设备,驱动怎么知道自己要创建什么设备名呢?有人说可以写明一套规则。确实如此,但如果把这套规则放到应用层,由应用程序开发人员去明确这个规则(mdev正是这样做的),会不会更好?因为是应用程序直接编程访问这个设备名对应的设备驱动的。所以设备驱动不应该直接负责设备文件的创建。
用户层创建设备文件也有两种方法:
一种方法就是用户在shell中使用mknod命令创建设备文件,同时传入设备名和设备号。这应该是大家最熟悉的一种方法,但是这种人工的做法,很不科学。它只是一种演示的方法,不适于作为工程方法。
另外一种方法就是依赖设备模型来辅助创建设备文件。这也是设备模型的作用之一。
udev和mdev就是使用第二种方法依赖LDDM自动创建设备文件的机制。
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程序。通过:
echo /sbin/mdev > /sys/kernel/uevent_helper
实现使用使用mdev处理uevent的目的(有的资料说是将mdev加到/proc/sys/kernel/hotplug_helper,其实这两个是一样的,前面说过/sys改进了/proc,所以新的系统应该优先使用/sys)。
那么问题又来了?
LDDM也是内核程序,为什么就能包含设备名呢?这不是与上面讲的矛盾吗?既然讲了这么多LDDM的内容了,思考一下按照你的理解给出一个答案吧。
1、执行mdev -s命令时,mdev扫描/sys/block(块设备保存在/sys/block目录下,内核2.6.25版本以后,块设备也保存在/sys /class/block目录下。mdev扫描/sys/block是为了实现向后兼容)和/sys/class两个目录下的dev属性文件,从该dev 属性文件中获取到设备编号(dev属性文件以”major:minor”形式保存设备编号),并以包含该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。
2、当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目录下,
2、创建一个以设备名device_name作为名称的目录,
3、并且在该device_name目录下还必须包含一个 dev属性文件,该dev属性文件以”major:minor\n”形式输出设备编号。
这里就用到了LDDM的class相关知识,大家可以参看相关章节。
也就是说,大家判断能不能使用mdev自动生成设备节点,可以在加载完驱动后,到/class/dev下看看有没有驱动主从设备号的符号链接,如果有,证明此类设备的核心已经处理了相关过程,如果没有,若还想使用mdev动态生成设备节点的话,则需要你在驱动程序中显式的调用函数创建子系统节点。
用示意图表示次过程就是:
mdev最基本的使用是在启动时或热插拔时,生成/dev设备节点,在启动脚本中加入
mount -t proc proc /proc
mount -t sysfs sysfs /sys
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
除了基本功能外mdev也提供了简单的根据规则执行操作的能力,这需要编辑mdev的规则文件/etc/mdev.conf,规则是这样的:
@ 创建节点后执行的
$ 删除节点前执行的
* 创建后和删除前都运行的
看到这,大家对于概述部分LDDM为什么就可以包含设备名称的问题有没有思路呢?