4.6.3 处理冷插拔设备
前面我们讨论了动态加载驱动的整个过程。但是不知道读者想过没有,对于磁盘这种非热插拔设备,如果驱动没有编译进内核,那么当内核引导枚举设备时,系统运行在内核空间,尚未进入用户空间,更谈不上启动用户空间的udev服务了,因此内核发送到用户空间的uevent自然会被丢掉,更别提加载硬盘驱动模块和建立设备节点了。
为了解决这个问题,开发人员基于sys文件系统设计了一种巧妙的机制。在Linux操作系统进入用户空间,udevd启动后,通过sys文件系统请求内核重新发出uevent。此时udevd已经启动了,就会收到uevent,然后结合这些事件和规则,完成驱动的加载、设备节点的建立等。我们可以将这个过程看作是内核和udev导演的一出戏,对于冷插拔的设备,模拟了一遍热插拔的过程。
下面我们简单探讨一下这个机制的原理。
当新设备注册时,内核将调用device_create_file在sys文件系统中为设备注册一个名字为uevent的文件,当用户空间的程序读取该文件时,内核将调用函数show_uevent处理用户的读操作,而当用户空间的程序向该文件写入时,内核将调用函数store_uevent处理用户的写操作。我们以函数store_uevent为例,看看内核是如何处理用户的写操作的。函数store_uevent代码如下:
store_uevent的参数buf指向复制自用户空间的用户写入的字符串。函数kobject_action_type根据buf中的字符串,来决定发送给用户空间的uevent的类型。写入的字符串和发送的事件类型间的对应关系的代码如下所示。
也就是说,当用户空间的程序向该属性文件写入字符串"add"时,函数kobject_action_type认为用户空间的程序要求KOBJ_ADD类型的事件,于是调用kobject_uevent向用户空间发送KOBJ_ADD类型的uevent。
利用这种机制,我们可以在用户空间的udev服务程序启动后,向所有设备的属性文件uevent写入"add"字符串,请求内核重新发送一遍KOBJ_ADD事件,模拟一遍热插拔动作。如此,udevd就可以收到这些事件,完成驱动加载、设备节点创建等工作。
为此,udev提供了一个管理工具udevadm,我们可以使用这个工具请求内核重新发送设备相关事件。假设请求内核对全部设备模拟一遍热插拔,即重新发送事件KOBJ_ADD,则使用如下命令:
我们来简单地看一下这个命令背后的代码:
根据上面代码可见,udevadm的trigger命令对应的函数是adm_trigger。当用户请求内核重新发送设备相关的事件时,adm_trigger首先调用udev_enumerate_scan_devices在sys文件系统中寻找设备,使用udevadm的trigger命令时我们可以指定一些属性,匹配特定的设备。但是无论如何,会有多个设备满足匹配条件的情况,比如我们上面的命令,没有任何限制条件,那么内核将匹配所有设备。于是udev在结构体udev_enumerate中设计了一个链表,udev_enumerate_scan_devices将找到的所有设备连接到结构体udev_enumerate中的设备链表中。
然后,adm_trigger调用函数exec_list遍历这个链表,向这些设备在sys文件系统中注册的属性文件uevent写入用户请求内核重新发送的事件类型对应的字符串。比如,如果请求内核发送KOBJ_ADD类型的uevent,则写入字符串"add";如果请求内核发送KOBJ_CHANGE类型的uevent,则写入字符串"change",等等。