万书网 > 文学作品 > 深度探索Linux操作系统 > 4.5 将硬盘驱动编译为模块

4.5 将硬盘驱动编译为模块



initramfs的重要作用之一就是允许内核将保存根文件系统的存储设备的驱动不再编译进内核。上一节,我们体验了基本的initramfs,这一节,我们就将硬盘驱动编译为模块。



4.5.1 配置devtmpfs


既然提到设备,而且Linux将设备也抽象为文件,这里就不得不讨论一下设备文件或者说设备节点。通常情况下,某些需要从用户空间访问的设备都会在文件系统中建立一个设备文件,作为用户空间访问设备的接口。得益于Linux中虚拟文件系统的设计,用户空间的程序可以像访问普通文件一样,使用标准的文件访问接口实现与设备的交互。

根据FHS的规定,设备文件存放在/dev目录下。在Linux系统的早期,设备文件是静态创建的,所有的设备节点是手动、事先创建的。笔者还记得在早期制作Linux发行版时,安装系统时,需要静态安装大量的设备文件,把所有可能的设备节点一并创建出来。但是,这样带来的一个问题就是,随着设备的种类越来越多,这个目录会越来越大,对于某一台具体的机器来说,dev目录下充斥着大量无用的设备文件,因为某些设备在某些机器上根本就不存在。而且,这种方法会逐渐耗尽设备号,虽然可以通过扩展设备号的位数来增加设备号,但终究不是长久之计。

鉴于静态创建设备文件的种种问题,开发人员开发了devfs。devfs虽然解决了按需创建设备节点的机制,但是还是有很多问题,比如设备文件的名称依然由驱动开发人员在代码中指定,而不能由系统管理员指定。因此,后来又出现了udev,使得设备命名策略、权限控制等都在用户空间完成。如此,设备文件不再是静态创建,而是由udev根据内核检测到的实际连接的设备,创建相应的设备文件。

对于动态创建设备文件,推荐在/dev目录下挂载一个基于内存的文件系统。基于内存的文件系统会完全驻留在RAM中,读写可以瞬间完成。除了性能优势之外,基于内存的文件系统的另外一个特点就是没有持久性,基于内存的文件系统中的数据在系统重新启动之后不会保留。这看起来可能不像是个积极因素,然而,对于动态创建设备节点这种情况来说,这实际上是一个优势。在系统关闭后,所有的设备节点无须保留,系统重启后,udev将根据内核检测到的实际设备创建设备文件。

Linux从2.6.18开始采用udev,/dev目录使用了基于内存的文件系统tmpfs管理设备文件。

2009年初,开发人员又提出了devtmpfs,并在同年年底被Linux  2.6.32正式收录。内核引导时,devtmpfs将所有注册的设备在devtmpfs中建立相应的设备文件,一旦进入用户空间,在启动udev前,就可以将devtmpfs挂载到/dev目录下。也就是说,在启动udev前,devtmpfs中已经建立了初步的设备文件,一般启动程序不必再等待udev建立设备节点,甚至在某些嵌入式系统上,不再需要udev创建设备节点,因为这个基本的/dev已经足够,从而缩短了系统的启动时间。同rootfs类似,devtmpfs也不是新设计的文件系统,如果内核配置支持tmpfs,那么其就是tmpfs;否则,devtmpfs就是ramfs,只不过换了一个名字而已。

下面我们就实际体验一下devtmpfs。为此,我们需要在initramfs中安装工具ls和mount。

工具ls在coreutils中,所以首先编译安装coreutils:



下面使用ldd检查ls依赖的库:



根据ldd的输出可见,ls依赖的库除了librt和libpthread尚未安装到initramfs中外,其余已经安装,所以我们将ls以及librt和libpthread复制到initramfs中:



工具mount在软件包util-linux中,我们首先来编译这个软件包:



下面使用ldd检查mount依赖的库:



根据ldd的输出可见,mount依赖libmount、libblkid、libuuid以及libc。libc已经安装到了initramfs中,我们将mount和其余几个库复制到initramfs:



重新压缩initramfs,并将其保存到/vita/sysroot/boot/目录下。接下来,我们准备支持devtmpfs的内核,配置步骤如下:

1)执行make  menuconfig,出现如图4-9所示的界面。

图 4-9 配置内核支持devtmpfs(1)

2)在图4-9中,选择"Device  Drivers",出现如图4-10所示的界面。

图 4-10 配置内核支持devtmpfs(2)

3)在图4-10中,选择"Generic  Driver  Options",出现如图4-11所示的界面。

图 4-11 配置内核支持devtmpfs(3)

4)在图4-11中,选中"Maintain  a  devtmpfs  filesystem  to  mount  at/dev"。

编译内核,并将内核与前面准备好的initramfs复制到虚拟机的目标系统上,然后重启进入vita系统。使用ls列出vita系统/dev下的设备文件,如图4-12所示。

图 4-12 未挂载任何文件系统的/dev目录

根据图4-12可见,/dev目录下包含一个设备节点console。但是我们的initramfs中并没有包含这个设备节点,那么这个设备节点是谁创建的呢?大家一定还记得我们前面讨论过的内置的initramfs吧?没错,这个console就是由内置的initramfs创建的。

接下来我们使用下面的命令将ramfs挂载到/dev目录下。



因为ramfs只是一个基于内存的文件系统,与设备无关,所以mount命令中的"device"参数可以使用任意字串描述。习惯上,对于这类没有具体设备的,一般使用字串"none"表示。但是mount命令不推荐这样使用,因为在某些情况下,某些提示很容易让用户费解,比如"mount:none  already  mounted"。这里我们暂时使用"none",在最终系统中我们使用"udev",表示该目录下的节点是由udev创建的。默认情况下,mount命令会在文件/etc/mtab中维护一份当前已挂载的文件系统列表,因为我们的initramfs中没有创建/etc/mtab文件,initramfs中也不需要,因此使用参数"-n"告诉mount不需维护这份列表。

挂载完成后,我们查看/dev下的设备文件。情况非常糟糕,/dev下挂载了一个空的ramfs文件系统,原有的console设备节点也被覆盖了,如图4-13所示。

图 4-13 挂载ramfs文件系统的/dev目录

接下来我们使用下面的命令将devtmpfs挂载到/dev目录下:



挂载完成后,我们再次查看/dev下的设备文件。可以看到,devtmpfs下面已经建立了若干设备节点,如图4-14所示。

图 4-14 挂载devtmpfs后的/dev目录

既然devtmpfs有这么多的好处,/dev目录当然要使用devtmpfs文件系统了。因此,按照下面的脚本修改initramfs中的init文件:



4.5.2 将硬盘控制器驱动配置为模块

与前面配置硬盘控制器驱动类似,只不过这里我们将"AHCI  SATA  support"和"Intel  ESB,ICH,PIIX3,PIIX4  PATA/SATA  support"配置为模块,如图4-15所示。

图 4-15 配置SATA控制器驱动为模块

接下来重新编译内核和模块。内核和模块可以使用单独的命令分开编译,也可以使用一条make命令同时编译内核和模块。编译完成后,将模块暂时安装在"/vita/sysroot/lib/modules"目录下。



最终安装的硬盘控制器驱动模块包括:



我们将其复制到initramfs中。



为了加载内核模块,我们需要安装加载、卸载等管理模块的工具,这些工具在包kmod中:



检查kmod的依赖:



根据输出可见,kmod及库libkmod只依赖libc库,而libc已经安装到initramfs,所以复制kmod及库libkmod到initramfs即可,具体如下:



kmod是module-init-tools的替代者,但是kmod是向后兼容module-init-tools的,虽然kmod只提供一个工具kmod,但是通过符号链接的形式支持module-init-tools中的各个命令,而且目前来看,也只能使用这种方式来使用各种模块管理命令。因此,需要为各个模块管理命令建立符号链接,并将这些符号链接也复制到initramfs中:



其中,insmod、rmmod、modprobe用于加载/卸载模块;modinfo用于查看模块信息;lsmod用于查看已经加载的模块;depmod用于创建模块间的依赖关系。注意,这里一定要将modprobe等命令放在/sbin目录下,因为后面的udevd将会到使用"/sbin/modprobe"的形式调用modprobe命令。最新的合并到systemd中的udev不再直接调用modprobe等工具,而是使用libkmod提供的库提供的API加载模块,但并无本质区别。

bash的默认搜索命令的路径为/usr/gnu/bin:/usr/local/bin:/bin:/usr/bin:.。我们当然可以使用全路径运行命令,比如/sbin/insmod,但是为了方便,我们还是在init脚本中对搜索命令的路径进行一些调整。bash中,保存搜索命令的环境变量为PATH:



压缩initramfs,并将其和不包含硬盘驱动的bzImage复制到虚拟机,然后重启系统。进入系统后,因为内核中已经没有硬盘控制器的驱动,所以在/dev目录下不会有类似/dev/sdaX的设备节点。为了识别硬盘,我们需要加载硬盘控制器的驱动。

前面提到,Intel的SATA控制器可以运行在Compatibility模式和AHCI模式。笔者的机器的SATA控制器工作在AHCI模式,因此使用ahci驱动。但是在试图加载ahci模块时,ahci模块在报告了若干找不到的符号后,加载以失败告终,如图4-16所示。

图 4-16 加载ahci模块失败

显然,这些找不到的符号应该定义在其他某个(些)未加载的模块中,我们需要使用命令modinfo查看一下模块ahci依赖了哪些模块,我们使用参数"-F  depends"告诉modinfo仅显示ahci的依赖信息:



根据modinfo的输出,我们可以看到,ahci模块依赖libahci模块。因此,我们首先需要加载libahci模块,然后再加载ahci模块,如图4-17所示。

图 4-17 加载ahci模块成功

加载ahci模块后,该模块正确识别出了硬盘控制器,并且内核在devtmpfs中也建立了对应硬盘的设备节点。

虽然使用insmod可以完成加载模块的功能,但是我们发现必须要对模块的依赖关系非常清楚。幸运的是ahci只依赖libahci,而且libahci不依赖其他模块。但是如果一个模块依赖多个模块,并且依赖的模块又依赖其他的模块,如此下去,可想而知,加载这样一个模块将是多么复杂。好在kmod中提供了另外一个加载/卸载模块的工具modprobe,与insmod和rmmod相比,modprobe可以自动加载/卸载模块依赖的其他模块,而模块间的依赖关系存储在modules目录下的modules.dep中。以硬盘驱动这几个模块为例,其在modules.dep中的内容如下:



该片段表示模块ahci.ko依赖libahci.ko,而模块libahci.ko和ata_piix.ko不依赖其他模块。

如果模块间依赖关系简单也罢,但是如果比较复杂,那么手动去创建modules.dep是不现实的,幸运的是,模块管理工具中也提供了相应的工具创建modules.dep文件,这个工具就是depmod。在安装内核模块时,安装脚本将自动调用这个工具,创建modules.dep等文件。

为了加快搜索过程,modules.dep通常使用更有效率的Trie树来组织,并命名为modules.dep.bin。module-init-tools中实现的modprobe上述两种格式都支持,当然首选使用modules.dep.bin。但是kmod仅支持使用Trie树形式存储的modules.dep.bin。

接下来我们就体验一下使用modprobe加载驱动模块。首先需要创建模块依赖关系文件。一般而言,对于通用系统,通常在安装系统时使用depmod创建依赖关系文件,然后如果模块有变动,可以使用depmod命令更新这些文件。

在这里,在安装内核模块时,安装脚本已经调用depmod创建了modules.dep和使用Trie树组织的modules.dep.bin,注意需要将使用Trie树组织的modules.dep.bin复制到initramfs。当然,如果使用了module-init-tools中的模块管理工具,那么这里完全可以体验一下手写modules.dep文件。



为了验证modprobe是否正确加载了模块,可以使用命令lsmod查看内核加载的模块。但是lsmod是通过proc和sysfs获取内核信息的,因此,为了使用lsmod,首先需要挂载proc和sysfs文件系统。为此,我们需要在initramfs的根目录下创建proc和sys目录作为挂载点:



同时修改init脚本,添加挂载proc和sysfs文件系统的脚本:



重新压缩initramfs,并将其复制到虚拟机,重新启动系统,使用命令modprobe安装ahci模块,并使用命令lsmod查看内核安装的模块,如图4-18所示。

图 4-18 使用modprobe加载ahci模块

根据lsmod的输出可见,虽然我们并没有明确的指示modprobe加载模块libahci,但是modprobe根据modules.dep.bin中记录的依赖关系,自动加载了ahci依赖的模块libahci。