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