本章描述 Linux 核心如何只在需要的时候才动态加载函数,例如文件系统。
Linux 是一个完整的核心,就是说,它是一个单一的巨大的程序,核心的功能组件可以访问它的所有的内部数据结构以及例程。另一种方法是使用一个微内核的结构,核心的功能片被分成独立的单元,互相之间有严格的通讯机制。这样通过配置进程向核心增加新的组件不花多少时间。比如你希望增加一个 NCR 810 SCSI 卡的 SCSI 驱动程序,你不需要把它连接到核心。否则你不得不配置并建立一个新的核心才能使用这个 NCR 810 。作为一种变通, Linux 允许在你需要的时候动态地加载和卸载操作系统的组件。 Linux 的模块是可以在系统启动之后任何时候动态连接到核心的代码块。它们可以在不被需要的时候从核心删除并卸载。大多数 Linux 核心模块是设备驱动程序,伪设备驱动程序比如网络驱动程序或文件系统。
你可以使用 insmod 和 rmmod 命令明确地加载和卸载 Linux 核心模块,或者在需要这些模块的时候由核心自己要求核心守护进程( kerneld )加载和卸载这些模块。在需要的时候动态地加载代码相当有吸引力,因为它让核心可以保持最小而且核心非常灵活。我当前的 Intel 核心大量使用模块,它只有 406K 大小。我通常只适用 VFAT 文件系统,所以我建立我的 Linux 核心,当我安装一个 VFAT 分区的时候自动加载 VFAT 文件系统。当我卸载 VFAT 文件系统的时候,系统探测到我不再需要 VFAT 文件系统模块,把它从系统中删除。模块也可以用来尝试新的核心代码而不需要每次都创建和重启动核心。但是,没有这么好的事情,使用核心模块通常伴随轻微的性能和内存开支。一个可加载模块必须提供更多的代码,这种代码和额外的数据结构会占用更多一点的内存。另外因为间接访问核心资源也让模块的效率轻微降低。
一旦 Linux 核心加载,它就和普通核心代码一样成为核心的一部分。它和任何核心代码拥有相同的权利和义务:换句话说, Linux 核心模块和所有的核心代码或设备驱动程序一样可能让核心崩溃。
既然模块在需要的时候可以使用核心资源,它们必须能够找到这些资源。比如一个模块需要调用 kmalloc() ,核心内存分配例程。当建立的时候( build ),模块不知道内存中 kmalloc() 在哪里,所以当这个模块加载的时候,在模块能够工作之前,核心必须整理模块对于 kmmalloc() 的所有的引用。核心在核心符号表中保存了所有核心资源的列表,所以当模块加载的时候它可以解析模块中对于这些资源的引用。 Linux 允许模块堆栈(堆砌),就是一个模块需要另一个模块的服务。例如 VFAT 文件系统模块需要 FAT 文件系统模块的服务,因为 VFAT 文件系统或多或少是 FAT 文件系统上的扩展。一个模块需要另一个模块的服务或资源的情况和一个模块需要核心自己的服务和资源的情况非常相似,只不过这时请求的服务在另一个,此前已经加载的模块钟。当每一个模块加载的时候,核心修改它的符号表,把这个新加载的模块的所有输出的资源或符号加到核心符号表中。这意味着,当下一个模块加载的时候,它可以访问已经加载的模块的服务。
当时图卸载一个模块的时候,核心需要知道这个模块不在用,它还需要一些方法来通知它准备卸载的模块。用这种方法模块可以在它从核心删除之前释放它占用的任何的系统资源,例如核心内存或中断。当模块卸载的时候,核心把这个模块输出到核心符号表中所有的符号都删除。
除了写的不好的可加载模块可能破坏操作系统之外,还有另一个危险。如果你加载一个为比你当前运行的核心要早或迟的核心建立的模块会发生什么?如果这个模块执行一个核心例程而提供了错误的参数就会引起问题。核心可以选择防止这种情况,当模块加载的时候进行严格的版本检查。
12.1 Loading a Module (加载一个模块)
用两种方法可以加载一个核心模块。第一种使用 insmod 命令手工把它插入到核心。第二种,更聪明的方法是在需要的时候加载这个模块:这叫做按需加载( demand loading )。当核心发现需要一个模块的时候,例如当用户安装一个不在核心的文件系统的时候,核心会请求核心守护进程( kerneld )试图加载合适的模块。
Kerneld 和 insmod , lsmod 以及 rmmod 都在 modules 程序包中。
核心守护进程通常是拥有超级用户特权的一个普通的用户进程。当它启动的时候(通常是在系统启动的时候启动),它打开一个通向核心的 IPC 通道。核心使用这个连接向 kerneld 发送消息,请求它执行大量的任务。 Kerneld 的主要功能是加载和卸载核心模块,但是它也可以执行其它任务,比如需要的时候在串行线上启动 PPP 连接,不需要的时候把它关闭。 Kerneld 本身并不执行这些任务,它运行必要的程序比如 insmod 来完成工作。 Kerneld 只是核心的一个代理,调度它的工作。
参见 include/linux/kerneld.h
insmod 命令必须找到它要加载的被请求的核心模块。按徐加载的核心模块通常放在 /lib/mmodules/kernel-version 目录里边。核心模块和系统中的其它程序一样是连接程序的目标文件,但是它们被连接成可以重定位的映像。就是没有连接到特定地址去运行的映像。它们可以是 a.out 或 elf 格式的目标文件。 Insmod 指向一个特权的系统调用,找出系统的输出符号。它们以符号名称和值(例如它的地址)的形式成对存放。核心的输出符号表放在核心维护的模块列表中的第一个 module 数据结构,用 module_list 指针指向。只有在核心编译和连接的时候特殊指定的符号才加到这个表中,而并非核心的每一个符号都输出它的模块。例如符号“ request_irq ”是一个系统例程,当一个驱动程序希望控制一个特定的系统中断的时候必须调用它。在我当前的核心上,它的值是 0x0010cd30 。你可以检查文件 /proc/ksyms 或使用 ksyms 工具简单地查看输出的核心符号和它们的值。 Ksyms 工具可以向你显示所有的输出的核心符号或者只显示哪些加载模块输出的符号。 Insmod 把模块读取到它的虚拟内存,使用核心的输出符号来整理这个模块对于核心例程和资源的未解析的引用。这个整理过程是用向内存中的模块映像打补丁的方式进行, insmod 物理上把符号的地址写到模块的合适的位置。
参见 kernel/module.c kernel_syms() include/linux/module.h
当 insmod 整理完了模块对于输出的核心符号的引用之后,他向核心请求足够的空间放置新的核心,又是通过特权的系统调用。核心分配一个新的 module 数据结构和足够的核心内存来存放这个新的模块,并把它放置到核心的模块列表的最后。这个新的模块被标记为 UNINITIALIZED 。图 12.1 显示了核心模块列表的后面两个模块: FAT 和 VFAT 被加载到了内存。图中没有显示的有列表的第一个模块:这是一个伪模块,用于放置核心的输出符号表。你可以使用命令 lsmod 列出所有加载的核心模块和它们之间的依赖关系。 Lsmod 只是简单地把从核心 module 数据结构列表中提取的 /proc/modules 重新安排了格式。核心为模块分配的内存映射到 insmod 进程的地址空间,所以它可以访问它。 Insmod 把模块拷贝到分配的空间,并把它重定位,这样它就可以从被分配的核心地址运行。必须进行重定位,因为一个模块不能期待在两次被加载到相同的地址或者在两个不同的 Linux 系统上被加载到相同的地址。这一次,重定位又关系到要用适当的地址为模块的映像打补丁。
参见 kernel/module.c create_module()
新的模块也向核心输出符号, Insmod 建立一个输出映像表。每一个核心模块必须包含模块初始化和模块清除的历程,这些符号必须是专用的而不是输出的,但是 insmod 必须知道它们的地址,能把它们传递给核心。所有这些做好之后, Insmod 现在准备初始化这个模块,它执行一个特权的系统调用,把这个模块的初始化和清除例程的地址传递给核心。
参见 kernel/module.c sys_init_module()
当一个新的模块加到核心的时候,它必须更新核心的符号表并改变被新的模块使用的模块。其它模块依赖的模块必须在它们的符号表之后维护一个引用列表,用它们的 module 数据结构指向。图 12.1 显示了 VFAT 文件系统模块依赖于 FAT 文件系统模块。所以 FAT 模块包含一个到 VFAT 模块的引用:这个引用在 VFAT 模块加载的时候增加。核心调用模块的初始化例程,如果成功,它开始安装这个模块。模块的清除例程的地址保存在它的 module 数据结构中,当这个模块卸载的时候核心会去调用。最后,模块的状态被设置为 RUNNING 。
12.2 Unloading a Module
模块可以使用 rmmod 命令删除,但是 kerneld 可以把所有不用的按需加载的模块从系统中删除。每一次它的空闲计时器到期的时候, kerneld 执行系统调用,请求从系统删除所有的不需要的按需加载的模块。这个计时器的值由你在启动 kerneld 的时候设定:我的 kerneld 每 180 秒检查一次。如果你安装了一个 iso9660 CD ROM 而你的 iso9660 文件系统是一个可加载模块,那么,在 CD ROM 卸载不久, iso9660 模块会从核心中删除。
如果核心中的其它组件依赖于一个模块,它就不能被删除。例如如果你安装了一个或更多的 VFAT 文件系统,你就不能卸载 VFAT 模块。如果你检查 ls 输出,你会看到每一个模块关联一个计数器。例如:
Module: #pages: Used by:
msdos 5 1
vfat 4 1 (autoclean)
fat 6 [vfat msdos] 2 (autoclean)
这个计数器( count )是依赖于这个模块的核心实体的数目。在上例中, vfat 和 msdos 都依赖于 fat 模块,所以 fat 模块的计数器是 2 。 Vfat 和 msdos 模块的依赖数都是 1 ,因为它们都有一个安装的文件系统。如果我加载另外一个 VFAT 文件系统,那么 vfat 模块的计数器会变成 2 。一个模块的计数器放在它的映像的第一个长字中( longword )。
因为它也放置 AUTOCLEAN 和 VISITED 标志,所以这个字段有一些轻微过载。这些标志都用于按需加载模块。这些模块被标记为 AUTOCLEAN ,这样系统可以识别出哪些它可以自动卸载。 VISITED 标志表示这个模块被一个或多个系统组件使用:只要另一个组件使用它就设置这个标志。每一次 kerneld 请求系统删除不用的按需加载的模块的时候,它都查看系统中所有的模块,找到合适的候选。它只查看标记为 AUTOCLEAN 而且状态是 RUNNING 的模块。如果这个候选的 VISITED 标记被清除,那么它就删除这个模块,否则它就清除这个 VISITED 标记,继续查找系统中的下一个模块。
假设一个模块可以被卸载,就调用它的清除例程( cleanup ),让它释放它所分配的核心资源。这个 module 数据结构被标记为 DELTED ,从核心模块列表中删除。任何其它的它所依赖的模块的引用表被修改,这样它们不再把它当作一个依赖者。这个模块需要的所有的核心内存被释放。