4.6 自动加载硬盘控制器驱动
在前面,我们以Intel的工作在AHCI模式下的SATA硬盘控制器为例,展示了如何加载硬盘控制器的驱动。但是,除非是为一款特定的嵌入式设备定制的系统,否则,对于一个通用设备来说,比如PC,我们是不能假定硬件使用的硬盘控制器的。因此,合理的方法应该是根据具体的硬盘控制器加载对应的驱动模块。但是依靠用户自己手动来完成吗?姑且不提是否方便易用,除非专业用户,否则普通用户如何知道应该加载哪些驱动模块呢?
从2.6版内核开始,Linux采用udev管理驱动模块的加载以及设备节点的管理。每当内核发现新的设备,便通过NETLINK向用户空间发送新设备事件,该事件中记录了设备的相关信息。用户空间的udev服务进程收到内核事件后,根据事件中携带的信息,首先判断该设备的驱动是否已经加载,如果没有,则加载驱动。驱动加载后,内核会再次向用户空间报告发现新设备事件,这时设备已经成功驱动了,并且主次设备号等信息也已经准备好了,udev收到事件后,或者为设备建立节点,或者执行某些特定的操作。整个过程如图4-19所示。
图 4-19 自动加载设备驱动过程
读者可能会有一点疑问:既然有了devtmpfs,为什么还需要udev?
首先,也是最重要的一点,devtmpfs仅是记录了设备驱动注册的节点。udev除了创建设备节点外,还要负责加载设备驱动。后者是devtmpfs所不能实现的,devtmpfs仅是一个被动的记录数据的文件系统而已。
其次,使用udev,在发现新设备或者设备发生了更新时,可以有机会执行某些特定的动作。比如在建立新设备时,为设备节点建立额外的符号链接。
下面我们就分别讨论一下上面描述的各个过程。
4.6.1 内核向用户空间发送事件
PC机上的硬盘控制器,无论是IDE接口的,还是SATA接口的,一般都是通过PCI总线连接到计算机上的。内核在引导时,PCI子系统将进行初始化,枚举总线上的设备,并尝试为设备匹配驱动;然后将收集到的设备相关信息组织为uevent事件;接着调用kobject_uevent,通过NETLINK将组织好的uevent发送到用户空间,通知udev有新设备了。简单地讲,内核的工作就是探测并收集设备信息,将其包装到uevent事件中,然后发送到用户空间。
事实上,无论是发现新的设备,还是有新的驱动载入,抑或是用户向sysfs中的uevent写入字符串,内核都将调用函数kobject_uevent向用户空间发送事件,其代码如下所示:
结构体kobj_uevent_env用来保存收集到的设备相关信息,所以在函数kobject_uevent_env中,首先为kobj_uevent_env申请了一块内存,即变量env指向的内存,用来临时存放准备发送到用户空间的设备相关信息。
然后向该内存中添加了三个默认的变量,包括ACTION、DEVPATH和SUBSYSTEM。其中ACTION指的是热插拔的动作,如"add","remove","change"等。DEVPATH指的是设备在sysfs文件系统中注册的设备路径,比如笔者的硬盘sda的DEVPATH是"/devices/pci0000:00/0000:00:1f.2/ata1/host0/target0:0:0/0:0:0:0/block/sda"。SUBSYSTEM一般是指设备所在的总线,比如笔者的硬盘是挂在PCI总线上的,因此该变量的值是"pci"。
在Linux的设备模型中,除了总线、设备以及驱动这些对象外,还定义了集合,有某些相似特性的kobject将被组织到一个集合中。所以我们看到,在函数kobject_uevent_env的开头,寻找硬盘控制器所属的集合,并在向uevent中添加了三个默认的变量后,调用硬盘控制器所属的集合的uevent_ops中的函数uevent继续向uevent中追加变量。对于硬盘控制器来说,其所属的集合是devices_kset,这是PCI总线在初始化设备时设定的,相关定义如下:
dev_uevent向uevent中继续增加一些设备相关的变量,包括设备节点的主次设备号、名称、设备节点的读、写和执行权限、设备的类型以及驱动模块的名称等准备发送到用户空间的变量。在设备枚举阶段,因为设备还没有被驱动,所以这些信息是没有的。只有当设备被正确地驱动后,内核向用户空间发送的uevent中才包含这些信息。
除了设备信息外,设备所属的总线也可能需要向用户空间报告一些设备所在的总线的相关信息。因此,如果设备属于某个总线,函数dev_uevent则还要调用设备所属总线的event函数。PCI的设备总线类型pci_bus_type及其中的uevent函数代码如下:
pci_uevent又向uevent中追加了pci class、vendor id、device id以及MODALIAS等变量,其中MODALIAS需要重点关注,其是由设备所在总线、vendor ID、device ID等相关参数连接而成的一个字符串。在接下来的章节中,读者将看到,用户空间的udev恰恰就是根据这个变量为设备匹配驱动模块的。
除了总线外,如果硬盘控制器所属的class或者type也需要继续向uevent中追加变量,则继续调用硬盘控制器所属的class或者type中的相应的函数,这里不再继续分析了。最终,内核向用户空间发送的uevent事件包含的大致的内容如下,其中不同变量之间使用“\0”进行分隔。
当伴随着热插拔事件一同发往用户空间的变量准备完毕后,kobject_uevent_env使用内核和用户空间的通信协议NETLINK向用户空间报告事件,代码如下:
kobject_uevent_env申请了一个结构体sk_buff类型的变量skb,这个skb就是用来封装报文的。报文以形如"ACTION@DEVPATH"(如"add@/devices/pci0000:00/0000:00:1f.2")的格式开头,紧接着的消息体中封装的就是前面收集到的存储在变量env中的变量。
至此,对于加载硬盘控制器驱动这个任务,内核已经完成了它的使命:PCI子系统获取硬盘控制器的信息,并将其通过NETLINK抛到了用户空间。接下来,该用户空间的udev出场了。