级别: 中级 M. Tim Jones, 顾问工程师, Emulex Corp.
2008 年 8 月 07 日
Linux? 可加载内核模块(从内核的 1.2 版本开始引入)是 Linux 内核的最重要创新之一。它们提供了可伸缩的、动态的内核。探索隐藏在可加载模块后面的原理,并学习这些独立的对象如何动态地转换成 Linux 内核的一部分。
Linux 就是通常所说的单内核(monolithic kernel),即操作系统的大部分功能都被称为内核,并在特权模式下运行。它与微型内核 不同,后者只把基本的功能(进程间通信 [IPC]、调度、基本的输入/输出 [I/O] 和内存管理)当作内核运行,而把其他功能(驱动程序、网络堆栈和文件系统)排除在特权空间之外。因此,您可能认为 Linux 是一个完全静态的内核,但事实恰恰相反。通过 Linux 内核模块(LKM)可以在运行时动态地更改 Linux。
可动态更改 是指可以将新的功能加载到内核、从内核去除某个功能,甚至添加使用其他 LKM 的新 LKM。LKM 的优点是可以最小化内核的内存占用,只加载需要的元素(这是嵌入式系统的重要特性)。 Linux 不是可以进行动态更改的惟一(也不是第一个)单内核。Berkeley Software Distribution(BSD)的变体、Sun Solaris、更老的内核(比如 OpenVMS),以及其他流行的操作系统(比如 Microsoft? Windows? 和 Apple Mac OS X)都支持可加载模块。 剖析内核模块 LKM 与直接编译到内核或典型程序的元素有根本区别。典型的程序有一个 main 函数,其中 LKM 包含 entry 和 exit 函数(在 2.6 版本,您可以任意命名这些函数)。当向内核插入模块时,调用 entry 函数,从内核删除模块时则调用 exit 函数。因为 entry 和 exit 函数是用户定义的,所以存在 de>module_initde> 和 de>module_exitde> 宏,用于定义这些函数属于哪种函数。LKM 还包含一组必要的宏和一组可选的宏,用于定义模块的许可证、模块的作者、模块的描述等等。图 1 提供了一个非常简单的 LKM 的视图。 图 1. 简单 LKM 的源代码视图
2.6 版本的 Linux 内核提供了一个新的更简单的方法,用于构建 LKM。构建 LKM 时,可以使用典型的用户工具管理模块(尽管内部已经改变):标准 de>insmodde>(安装 LKM),de>rmmodde> (删除 LKM),de>modprobede>(de>insmodde> 和 de>rmmodde> 的包装器),de>depmodde>(用于创建模块依赖项),以及 de>modinfode>(用于为模块宏查找值)。更多关于为 2.6 版本内核构建 LKM 的信息,请查看 参考资料。
剖析内核模块对象 LKM 只不过是一个特殊的可执行可链接格式(Executable and Linkable Format,ELF)对象文件。通常,必须链接对象文件才能在可执行文件中解析它们的符号和结果。由于必须将 LKM 加载到内核后 LKM 才能解析符号,所以 LKM 仍然是一个 ELF 对象。您可以在 LKM 上使用标准对象工具(在 2.6 版本中,内核对象带有后缀 .ko,)。例如,如果在 LKM 上使用 de>objdumpde> 实用工具,您将发现一些熟悉的区段(section),比如 .text(说明)、.data(已初始化数据)和 .bss(块开始符号或未初始化数据)。 您还可以在模块中找到其他支持动态特性的区段。.init.text 区段包含 de>module_initde> 代码,.exit.text 区段包含 de>module_exitde> 代码(参见图 2)。.modinfo 区段包含各种表示模块许可证、作者和描述等的宏文本。 图 2. 具有各种 ELF 区段的 LKM 的示例
了解 LKM 的基础知识之后,现在我们进一步探索模块是如何进入内核的,以及在内核内部是如何管理模块的。
LKM 的生命周期 在用户空间中,de>insmodde>(插入模块)启动模块加载过程。de>insmodde> 命令定义需要加载的模块,并调用 de>init_modulede> 用户空间系统调用,开始加载过程。2.6 版本内核的 de>insmodde> 命令经过修改后变得非常简单(70 行代码),可以在内核中执行更多工作。de>insmodde> 并不进行所有必要的符号解析(处理 de>kerneldde>),它只是通过 de>init_modulede> 函数将模块二进制文件复制到内核,然后由内核完成剩余的任务。 de>init_modulede> 函数通过系统调用层,进入内核到达内核函数 de>sys_init_modulede>(参见图 3)。这是加载模块的主要函数,它利用许多其他函数完成困难的工作。类似地,de>rmmodde> 命令会使 de>delete_modulede> 执行 de>system callde> 调用,而 de>delete_modulede> 最终会进入内核,并调用 de>sys_delete_modulede> 将模块从内核删除。 图 3. 加载和卸载模块时用到的主要命令和函数
在模块的加载和卸载期间,模块子系统维护了一组简单的状态变量,用于表示模块的操作。加载模块时,状态为 de>MODULE_STATE_COMINGde>。如果模块已经加载并且可用,状态为 de>MODULE_STATE_LIVEde>。此外,卸载模块时,状态为 de>MODULE_STATE_GOINGde>。
模块加载细节 现在,我们看看加载模块时的内部函数(参见图 4)。当调用内核函数 de>sys_init_modulede> 时,会开始一个许可检查,查明调用者是否有权执行这个操作(通过 de>capablede> 函数完成)。然后,调用 de>load_modulede> 函数,这个函数负责将模块加载到内核并执行必要的调试(后面还会讨论这点)。de>load_modulede> 函数返回一个指向最新加载模块的模块引用。这个模块加载到系统内具有双重链接的所有模块的列表上,并且通过 notifier 列表通知正在等待模块状态改变的线程。最后,调用模块的 de>init()de> 函数,更新模块状态,表明模块已经加载并且可用。 图 4. 内部(简化的)模块加载过程
加载模块的内部细节是 ELF 模块解析和操作。de>load_modulede> 函数(位于 ./linux/kernel/module.c)首先分配一块用于容纳整个 ELF 模块的临时内存。然后,通过 de>copy_from_userde> 函数将 ELF 模块从用户空间读入到临时内存。作为一个 ELF 对象,这个文件的结构非常独特,易于解析和验证。 下一步是对加载的 ELF 映像执行一组健康检查(它是有效的 ELF 文件吗?它适合当前的架构吗?等等)。完成健康检查后,就会解析 ELF 映像,然后会为每个区段头创建一组方便变量,简化随后的访问。因为 ELF 对象的偏移量是基于 0 的(除非重新分配),所以这些方便变量将相对偏移量包含到临时内存块中。在创建方便变量的过程中还会验证 ELF 区段头,确保加载的是有效模块。 任何可选的模块参数都从用户空间加载到另一个已分配的内核内存块(第 4 步),并且更新模块状态,表明模块已加载(de>MODULE_STATE_COMINGde>)。如果需要 per-CPU 数据(这在检查区段头时确定),那么就分配 per-CPU 块。 在前面的步骤,模块区段被加载到内核(临时)内存,并且知道哪个区段应该保持,哪个可以删除。步骤 7 为内存中的模块分配最终的位置,并移动必要的区段(ELF 头中的 de>SHF_ALLOCde>,或在执行期间占用内存的区段)。然后执行另一个分配,大小是模块必要区段所需的大小。迭代临时 ELF 块中的每个区段,并将需要执行的区段复制到新的块中。接下来要进行一些额外的维护。同时还进行符号解析,可以解析位于内核中的符号(被编译成内核映象),或临时的符号(从其他模块导出)。 然后为每个剩余的区段迭代新的模块并执行重新定位。这个步骤与架构有关,因此依赖于为架构(./linux/arch/<arch>/kernel/module.c)定义的 helper 函数。最后,刷新指令缓存(因为使用了临时 .text 区段),执行一些额外的维护(释放临时模块内存,设置系统文件),并将模块最终返回到 de>load_modulede>。
模块卸载细节 卸载模块的过程和加载模块基本一样,除了必须进行几个健康检查外(确保安全删除模块)。卸载模块过程首先在用户空间调用 de>rmmodde>(删除模块)命令。在 de>rmmodde> 命令内部,对 de>delete_modulede> 执行系统调用,它最终会导致在内核内部调用 de>sys_delete_modulede>(查看 图 3)。图 5 演示了删除模块的基本操作过程。 图 5. 内部(简化的)模块卸载过程
当调用内核函数 de>sys_delete_modulede>(将要删除的模块的名称作为参数传入)之后,第一步便是确保调用方具有权限。接下来会检查一个列表,查看是否存在依赖于这个模块的其他模块。这里有一个名为 de>modules_which_use_mede> 的列表,它包含每个依赖模块的一个元素。如果这个列表为空,就不存在任何模块依赖项,因此这个模块就是要删除的模块(否则会返回一个错误)。接下来还要测试模块是否加载。用户可以在当前安装的模块上调用 de>rmmodde>,因此这个检查确保模块已经加载。在几个维护检查之后,倒数第二个步骤是调用模块的 exit 函数(模块内部自带)。最后,调用 de>free_modulede> 函数。 调用 de>free_modulede> 函数之后,您将发现模块将被安全删除。该模块不存在依赖项,因此可以开始模块的内核清理过程。首先,从安装期间添加的各种列表中(系统文件、模块列表等)删除模块。其次,调用一个与架构相关的清理例程(可以在 ./linux/arch/<arch>/kernel/module.c 中找到)。然后迭代具有依赖性的模块,并将这个模块从这些列表中删除。最后,从内核的角度而言,清理已经完成,为模块分配的各种内存已被释放,包括参数内存、per-CPU 内存和模块的 ELF 内存(de>corede> 和 de>initde>)。
为模块管理优化内核 在许多应用程序中,动态加载模块非常重要,但加载之后,就没有必要卸载模块。这允许内核在启动时是动态的(根据找到的设备加载模块),但并不是在整个操作过程中都是动态的。如果不需要在加载之后卸载模块,那么可以进行一些优化,减少模块管理所需的代码。您可以 “取消” 内核配置选项 de>CONFIG_MODULE_UNLOADde>,删除大量与卸载模块相关的内核功能。
结束语 这一直是内核里面模块管理过程的高级视图。要获得模块管理的细节,源代码本身就是最佳的文档。关于在模块管理中调用的主要函数,请查看 ./linux/kernel/module.c(以及 ./linux/include/linux/module.h 中的头文件)。您还可以在 ./linux/arch/<arch>/kernel/module.c 中找到几个与架构相关的函数。最后,可以在 ./linux/kernel/kmod.c 中找到内核自动加载函数(可以根据需要从内核自动加载模块)。这个功能可以通过 de>CONFIG_KMODde> 配置选项启用。
参考资料 学习
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文。
- 查看 Rusty Russell 的博客 “Bleeding Edge”,了解他当前的 Linux 内核开发。Rusty 是新的 Linux 模块架构的主要开发人员。
- Linux Kernel Module Programming Guide 虽然有点过时,但提供了大量关于 LKM 及其开发的详细信息。
- 查看 “使用 /proc 文件系统来访问 Linux 内核的内容”(developerWorks,2006 年 3 月),详细了解在 /proc 文件系统上进行 LKM 编程。
- 通过 “使用 Linux 系统调用的内核命令”(developerWorks,2007 年 3 月)学习有关系统调用的细节。
- 要更多地了解 Linux 内核,请阅读 Tim 的 “Linux 内核剖析”(developerWorks,2007 年 6 月),它是本系列的第一篇文章,从较高层次介绍了 Linux 内核以及一些有趣的内容。
- “Standards and specs: An unsung hero: the hardworking ELF”(developerWorks,2005 年 12 月)是有关 ELF 的出色介绍。ELF 是 Linux 的标准对象格式。ELF 是一种灵活的文件格式,它涵盖了可执行映像、对象、共享库,甚至内核转储等。您还可以在 格式参考资料(PDF 文档)和 有关 ELF 格式的书籍 中找到更加详细的信息。
- Captain's Universe 通过示例生成文件提供了关于 LKM 构建的出色介绍。在 2.6 版本内核中,构建 LKM 的过程有了变化(变得更好)。
- 这里有一些用于插入、删除和管理模块的模块实用程序。可以通过 de>insmodde> 命令将模块插入内核,通过 de>rmmodde> 命令删除模块。要查询内核中当前的模块,使用 de>lsmodde> 命令。因为模块可以依赖于其他模块,所以可以用 de>depmodde> 命令构建一个依赖项文件。要在感兴趣的模块之前自动加载依赖模块,可以使用 de>modprobede> 命令(de>insmodde> 的包装器)。最后,您可以使用 de>modinfode> 命令读取 LKM 的模块信息 。
- Linux Journal 上的文章 “Linkers and Loaders”(2002 年 11 月)大量介绍了使用 ELF 文件(包括符号解析和重定位)的链接器和加载器的目的。
- developerWorks Linux 专区 提供了更多针对 Linux 开发人员的资源,同时请浏览我们的 最受欢迎的文章和教程。
- 查看 developerWorks 上所有 Linux 技巧 和 Linux 教程。
- 随时关注 developerWorks 技术活动和网络广播。
获得产品和技术
- 订购 SEK for Linux,共包含两张 DVD,其中有用于 Linux 的最新 IBM 试用软件,包括 DB2?、Lotus?、Rational?、Tivoli? 和 WebSphere?。
- 使用可直接从 developerWorks 下载的 IBM 试用软件 构建您的下一个 Linux 开发项目。
讨论
- 通过博客、论坛、podcast 和空间参与 developerWorks 社区。
关于作者
|
|
|
M. Tim Jones 是一名嵌入式固件架构师,同时也是 Artificial Intelligence: A Systems Approach, GNU/Linux Application Programming(第二版)、AI Application Programming(第二版)和 BSD Sockets Programming from a Multilanguage Perspective 等书的作者。他的工程背景非常广泛,从同步宇宙飞船的内核开发到嵌入式架构设计,再到网络协议的开发。Tim 是位于科罗拉多州 Longmont 的 Emulex Corp. 的一名顾问工程师。 |
|