4.6.2 udev加载驱动和建立设备节点
前面我们探讨了内核向用户空间报告uevent事件的过程。这一节,我们来讨论udev是如何根据内核报告的uevent事件加载硬盘控制器驱动以及建立设备节点的。
udev是用户空间动态管理设备的机制,包括加载驱动、管理设备节点等。udev机制的核心是其服务进程udevd。当启动过程进入用户空间阶段后,udevd将被启动。udevd启动后,首先读取并分析所有的规则文件,并将其缓存在内存中。一般情况下,系统默认的规则文件存放在/lib/udev/rules.d目录下,用户自定义的规则存放在/etc/udev/rules.d目录下。每当动态地增加、删除或者改变某个规则文件时,udevd将更新其缓存在内存中的规则。然后,udevd通过NETLINK协议,监听并处理来自内核的uevent事件。每当udevd收到一个内核的uevent,udevd均创建一个单独的子进程处理uevent。
对于每个内核报告的uevent,udevd根据uevent中的变量逐个匹配规则。规则文件通常以数字开头,数字小的先进行匹配。若每个规则文件中包含若干个规则,同一规则不允许断行,每个规则至少包含一个key-value对,每个key-value对之间使用逗号分隔。可以将规则理解为由匹配条件和赋值动作组成,当所有的匹配条件都满足后,赋值动作就会发生。规则中可以加载驱动模块;规定如何给设备接点命名、建立符号连接;设备连接和断开时分别执行指定的程序等。
前面我们看到内核在发现新设备时会将设备的一些信息通过NETLINK发送到用户空间,udev接收到事件后,如果发现设备尚未被驱动,将尝试加载驱动模块。那么udev如何确定设备对应的驱动模块呢?一般而言,根据设备的vender ID和device ID就可以标识一类设备,当然有的也需要根据subvendor ID和subdevice ID进一步细分。而在驱动代码中,恰恰使用这些设备信息明确声明了其可以支持的设备。以驱动AHCI模式的SATA硬盘控制器驱动为例:
ID table中的每一项表示该驱动支持的一类设备,根据PCI_VDEVICE的定义:
以ahci_pci_tbl中的第一项为例,该项声明了该驱动支持vender ID为PCI_VENDOR_ID_INTEL(0x8086),device ID为0x2652,subvendor ID、subdevice ID为任意的Intel SATA控制器。
内核将ID table中的每一项中的信息按照一定的格式组合起来,作为驱动的一个别名。这些别名存储在编译好的驱动模块中,模块安装后,需要使用工具depmod将其提取出来并存储在/lib/modules/'uname-r'目录下的modules.alias.bin/modules.alias中,如同前面讨论的modules.dep和modules.dep.bin的关系一样,modules.alias.bin与modules.alias完全相同,只不过modules.alias.bin是为了加快搜索速度采用Trie树存储的。很多读者可能会说,编译安装模块时从来没有显示执行depmod啊,那是因为make等安装脚本已经替我们调用了这个命令。
我们可以使用工具modinfo来查看驱动模块的相关信息,下面是查看驱动模块ahci的别名信息。
上述输出表示驱动模块ahci可以驱动别名为"pci:vdsvsdbc01sc06i01"、"pci:v00001 B21d00000612svsdbcsci"等的设备,其中“*”表示可以匹配任意ID。
通过depmod生成的典型的modules.alias文件如下所示:
显然,这个文件就是简单地将别名和驱动名称对应起来。
前面讨论内核向用户空间发送uevent时,我们看到,内核将在uevent的消息体中封装一个变量MODALIAS,其值形如"pci:v00008086d00001C03sv000017AAsd000021CEbc01s c06i01"。看上去是不是与驱动的别名一致?没错,内核的设计者们设计了这个机制,内核创建变量MODALIAS和模块创建别名采用相同的算法。当udevd收到内核uevent后,从uevent中提取这个字符串,然后将这个字符串作为modprobe的参数。modprobe首先查找文件modules.alias.bin,将该别名对应的模块找到。以该别名为例,显然其会与上面modules.alias文件片断中的第一行匹配成功,而该行明确表明该别名对应的驱动模块是ahci,因此,modprobe将加载模块ahci。
udev设计了规则文件80-drivers.rules用来描述如何加载驱动模块,以v173版本的udev的80-drivers.rules为例:
我们先来看第一个规则,该规则表示如果uevent的动作是删除设备(remove),则忽略下面所有规则,什么也不用做。
第二个规则包含两个匹配条件,一个赋值动作。其中“?”匹配一个字符,“*”匹配0或多个字符。这个规则表达的含义是:当设备还没有加载驱动,即环境变量DRIVER的值为空,并且环境变量MODALIAS的值非空,那么调用modprobe加载驱动。我们看到这里加载模块的方式就是采用我们前面讨论的别名的方式。这里追加到环境变量RUN中的程序,如果不给出绝对路径,将在/lib/udev目录下寻找,如果这个程序不在/lib/udev目录下,必须给出绝对路径。
80-drivers.rules也会包含对个别特殊subsystem类型的设备的特殊处理,我们这里不作过多讨论。
一旦驱动被正确加载,并且设备需要在用户空间建立设备节点,那么内核向用户空间再次报告的uevent中会包含创建设备节点需要的主次设备号以及节点的名称等环境变量,类似于下面的这个示例uevent事件。事实上,在发现设备、加载驱动过程中,内核一般会多次向用户空间报告uevent事件,只有设备和驱动匹配成功后发送的事件中才会包含主次设备号等变量。
该消息中,内核为udev创建设备节点提供了必要的变量,包括主设备号为8,次设备号为1,内核提供的该设备节点的名字为sda1。当udevd收到的uevent消息中,如果uevent的变量中包含设备号,则使用系统调用mknod创建设备节点。