解析 Linux 内核可装载模块的版本检查机制

本文探讨了Linux内核模块的版本检查机制,包括模块的构建、加载与卸载过程,以及版本信息如何保证模块与内核之间的兼容性和安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

refer from http://www.ibm.com/developerworks/cn/linux/l-cn-kernelmodules/

http://www.ibm.com/developerworks/cn/linux/l-lkm/


解析 Linux 内核可装载模块的版本检查机制

王 华东, 系统工程师, 自由职业者

简介: 为保持 Linux 内核的稳定与可持续发展,内核在发展过程中引进了可装载模块这一特性。内核可装载模块就是可在内核运行时加载到内核的一组代码。通常 , 我们会在两个版本不同的内核上装载同一模块失败,即使是在两个相邻的补丁级(Patch Level)版本上。这是因为内核在引入可装载模块的同时,对模块采取了版本信息校验。这是一个与模块代码无关,却与内核相连的机制。该校验机制保证了内核装载的模块是用户认可的,且安全的。本文将从内核模块发布者的角度思考模块版本检查机制,并从开发者与授权 root 用户的角度去使用及理解该机制。

发布日期: 2011 年 11 月 17 日
级别: 初级
访问情况 : 19649 次浏览
评论: 1 (查看 | 添加评论 - 登录)

平均分 4 星 共 13 个评分 平均分 (13个评分)
为本文评分

内核可装载模块概述

Linux 在发展过程中(即自 Linux 1.2 之后)引进了模块这一重要特性,该特性提供内核可在运行时进行扩展。可装载模块(Loadable Kernel Module,即 LKM)也被称为模块,就是可在内核运行时加载到内核的一组目标代码(并非一个完整的可执行程序)。这就意味着在重构和使用可装载模块时并不需要重新编译内核。模块依据代码编写与编译时的位置可分:内部模块和外部模块,即 in-tree module 和 out-of-tree module,在内核树外部编写并构建的模块就是外部模块。如果只是认为可装载模块就是外部模块或者认为在模块与内核通讯时模块是位于内核的外部的,那么这在 Linux 下均是错误的。当模块被装载到内核后,可装载模块已是内核的一部分。另外,我们使用的 Linux 发行版在系统启动过程 initrd 中已使用了必要的模块,除非我们只讨论基础内核(base kernel)。本文主要是对 Linux 2.6 的外部模块进行讨论的。

可装载模块在 Linux 2.6 与 2.4 之间存在巨大差异,其最大区别就是模块装载过程变化(如 图 1所示,在 Linux 2.6 中可装载模块在内核中完成连接)。其他一些变化大致如下:

  • 模块的后缀及装载工具;

对于使用模块的授权用户而言,模块最直观的改变应是模块后缀由原先的 .o 文件(即 object)变成了 .ko 文件(即 kernel object)。同时,在 Linux 2.6 中,模块使用了新的装卸载工具集 module-init-tools(工具 insmod 和 rmmod 被重新设计)。模块的构建过程改变巨大,在 Linux 2.6 中代码先被编译成 .o 文件,再从 .o 文件生成 .ko 文件,构建过程会生成如 .mod.c、.mod.o 等文件。

  • 模块信息的附加过程;

在 Linux 2.6 中,模块的信息在构建时完成了附加;这与 Linux 2.4 不同,先前模块信息的附加是在模块装载到内核时进行的(在 Linux 2.4 时,这一过程由工具 insmod 完成)。

  • 模块的标记选项。

在 Linux 2.6 中,针对管理模块的选项做了一些调整,如取消了 can_unload 标记(用于标记模块的使用状态),添加了 CONFIG_MODULE_UNLOAD 标记(用于标记禁止模块卸载)等。还修改了一些接口函数,如模块的引用计数。


图 1. 模块在内核中完成连接
图 1. 模块在内核中完成连接

发展到 Linux 2.6,内核中越来越多的功能被模块化。这是由于可装载模块相对内核有着易维护,易调试的特点。可装载模块还为内核节省了内存空间,因为模块一般是在真正需要时才被加载。根据模块作用,可装载模块还可分三大类型:设备驱动、文件系统和系统调用。另须指出的是,虽然可装载模块是从用户空间加载到内核空间的,但是并非用户空间的程序。

模块的版本检查

Linux 的迅速发展致使相邻版本的内核之间亦存在较大的差异,即在版本补丁号(Patch Level,即内核版本号的第四位数)相邻的内核之间。为此 Linux 的开发者为了保证内核的稳定,Linux 在加载模块到内核时对模块采用了版本校验机制。当被期望加载模块的系统环境与模块的构建环境相左时,通常会出现如清单 1 所示的装载模块失败。


清单 1. 装载模块失败
				
 # insmod ./hello/hello.ko 
 insmod: error inserting './hello/hello.ko': -1 Invalid module format 

 # dmesg | grep hello 
 [ 9206.599843] hello: disagrees about version of symbol module_layout 


清单 1 中,模块 hello.ko 构建时的环境与当前系统不一致,导致工具 insmod 在尝试装载模块 hello.ko 到内核时失败。hello.ko 是一个仅使用了函数 printk 的普通模块(您可在示例源码中找到文件 hello/hello.c)。我们通过命令 dmesg(或者您也可以查看系统日志文件如 /var/log/messages 等,如果您启用了这些系统日志的话)获取模块装载失败的具体原因,模块 hello.ko 装载失败是由于模块中 module_layout 的导出符号的版本信息与当前内核中的不符。函数 module_layout 被定义在内核模块版本选项 MODVERSIONS(即内核可装载模块的版本校验选项)之后。清单 2所示为 module_layout 在内核文件 kernel/module.c 中的定义。


清单 2. 函数 module_layout
				
 /* kernel/module.c */ 
 #ifdef CONFIG_MODVERSIONS 
 void module_layout(struct module *mod, 
       struct modversion_info *ver, 
       struct kernel_param *kp, 
       struct kernel_symbol *ks, 
       struct tracepoint * const *tp) 
 { 
 } 
 EXPORT_SYMBOL(module_layout); 
 #endif 


清单 3. 结构体 modversion_info
				
 /* include/linux/module.h */ 
 struct modversion_info 
 { 
     unsigned long crc; 
     char name[MODULE_NAME_LEN]; 
 }; 

正如您所想,函数 module_layout 的第二个参数 ver 存储了模块的版本校验信息。结构体 modversion_info 中保存了用于模块校验的 CRC(Cyclic Redundancy Check,即循环冗余码校验)值(见 清单 3)。Linux 对可装载模块采取了两层验证:模块的 CRC 值校验和 vermagic 的检查。其中模块 CRC 值校验针对模块(内核)导出符号,是一种简单的 ABI(即 Application Binary Interface)一致性检查,清单 1中模块 hello.ko 加载失败的根本原因就是没有通过 CRC 值校验(即 module_layout 的 CRC 值与当前内核中的不符);而模块 vermagic(即 Version Magic String)则保存了模块编译时的内核版本以及 SMP 等配置信息(见 清单 4,模块 hello.ko 的 vermagic 信息),当模块 vermagic 与主机信息不相符时亦将终止模块的加载。


清单 4. 模块的 vermagic 信息
				
 # uname – r 
 2.6.38-10-generic 

 # modinfo ./hello/hello.ko 
 filename:       ./hello/hello.ko 
 license:        Dual BSD/GPL 
 srcversion:     31FE72DA6A560C890FF9B3F 
 depends:        
 vermagic:       2.6.38-9-generic SMP mod_unload modversions 

通常,内核与模块的导出符号的 CRC 值被保存在文件 Module.symvers 中,该文件需在开启内核配置选项 CONFIG_MODVERSIONS 之后并完全编译内核获得(或者您也可在编译外部模块后获得该文件,保存的是模块的导出符号的 CRC 信息),其信息的保存格式如清单 5 所示。


清单 5. 导出符号的 CRC 值
				
 0x1de386dd  module_layout  vmlinux  EXPORT_SYMBOL 
 <CRC>        <Symbol>  <module> 

Linux 内核在进行模块装载时先完成模块的 CRC 值校验,再核对 vermagic 中的字符信息,图 2展示了内核中与模块版本校验相关的函数的调用过程(分别在函数 setup_load_info 和 check_modinfo 中完成校验)。Linux 使用 GCC 中的声明函数属性 __attribute__ 完成对模块的版本信息附加。构建的模块存在几个 section,如 .modinfo、.gnu.linkonce.this_module 和 __versions 等,这些 ELF 小节(即 section)保存了模块校验所需的信息(关于这些 section 信息的附加过程,您可查看模块构建时生成的文件 <module>.mod.c 及工具 modpost,见 清单 8 清单 15)。


图 2. 模块的两层版本校验过程
图 2. 模块的两层版本校验过程

为了更好的理解可装载模块,我们查看内核头文件 include/linux/module.h,它不仅定义了上述中的 struct modversion_info(见 清单 3)还定义了 struct module 等结构体。模块 CRC 值校验查看的是就是模块 __versions 小节的内容,即是附加的 struct modversion_info 信息。模块的 CRC 校验过程在函数 setup_load_info 中完成。Linux 使用 .gnu.linkonce.this_module 小节来解决模块对 struct module 信息的附加。文件 kernel/module.c 中的函数 check_modinfo 完成了主机与模块的 vermagic 值的对比(见 清单 6)。清单 6 中函数 get_modinfo 用于获取内核中的 vermagic 信息,模块 vermagic 信息则被保存在了 ELF 的 .modinfo 小节中。


清单 6. 函数 check_modinfo
				
 /* kernel/module.c */ 
 static int check_modinfo(struct module *mod, struct load_info *info) 
 { 
  const char *modmagic = get_modinfo(info, "vermagic"); 

  ... 
  } else if (!same_magic(modmagic, vermagic, info->index.vers)) { 
    ... 
  } 
  ... 

  return 0; 
 } 

内核空间与用户空间

操作系统须负责程序的独立操作并保护资源不受非法访问,而这一功能在现代 CPU 中以设计不同的操作模式(级别)来实现。内核运行在 CPU 的最高级别,即内核态,也被称为超级用户态;而应用程序则运行在最低级别,即用户态。由此系统内存在 Linux 中可分为两个不同的区域:内核空间与用户空间。模块运行在内核空间里,而应用程序则运行在对应的用户空间中。

须指出的是模块的 vermagic 信息来自内核头文件 include/linux/vermagic.h 中的宏 VERMAGIC_STRING,其中宏 UTS_RELEASE 保存了内核版本信息(见 清单 7)。与其关联的头文件 include/generated/utsrelease.h 需经内核预编译生成,即通过命令 make 或 make modules_prepare 等。


清单 7. 宏 VERMAGIC_STRING
				
 /* kernel/module.c */ 
 static const char vermagic[] = VERMAGIC_STRING; 

 /* include/linux/vermagic.h */ 
 #define VERMAGIC_STRING                         \ 
     UTS_RELEASE " "                         \ 
     MODULE_VERMAGIC_SMP MODULE_VERMAGIC_PREEMPT             \ 
     MODULE_VERMAGIC_MODULE_UNLOAD MODULE_VERMAGIC_MODVERSIONS   \ 
     MODULE_ARCH_VERMAGICLINE 

模块的装载与卸载

上述中,我们在装载模块时使用了工具 insmod。在 Linux 2.6 中,工具 insmod 被重新设计并作为工具集 module-init-tools 中的一个程序,其通过系统调用 sys_init_module(您可查看头文件 include/asm-generic/unistd.h)衔接了模块的版本检查,模块的装载等功能(如 图 3所示)。module-init-tools 是为 2.6 内核设计的运行在 Linux 用户空间的模块装卸载工具集,其包含的程序 rmmod 用于卸载当前内核中的模块。


图 3. 模块的装卸载
图 3. 模块的装卸载

表 1. 工具集 module-init-tools 中的部分程序
名称说明
insmod装载模块到当前运行的内核中
rmmod从当前运行的内核中卸载模块
lsmod显示当前内核已加装的模块信息
modinfo检查与内核模块相关联的目标文件,并打印出所有得到的信息
modprobe利用 depmod 创建的依赖关系文件自动加载相关的模块
depmod创建一个内核可装载模块的依赖关系文件,modprobe 用它来自动加载模块

值得一提的是在 module-init-tools 中可用于模块装卸载的程序 modprobe。程序 modprobe 的内部函数调用过程正如您所想与 insmod 类似,只是其装载过程会查找一些模块装载的配置文件,且 modprobe 在装载模块时可解决模块间的依赖性,即若有必要,程序 modprobe 会在装载一个模块时自动加载该模块依赖的其他模块。

其他一些细节

从用户空间装载模块到内核时,Linux 还对用户权限进行了检查。模块的装载须是获得 CAP_SYS_MODULE 权限的超级用户,这正是模块装载时最先检查的内容(见 图 2)。在 Linux 2.6 中,模块在构建时生成了一些临时文件,如 .o 文件、.mod.o 文件等。了解这些文件的生成有助于我们更好的理解 Linux 2.6 的内核模块构建过程以及版本信息的检查等内容。文件 .o 是模块代码(即 .c 文件)经编译后获得的目标文件,文件 .mod.o 则对应文件 .mod.c。文件 <module>.mod.c 是对 <modulue>.c 的扩展,清单 8展示了文件 kobject-example.mod.c 的内容 ( 即模块 kobject-example.ko 的 .mod.c 文件 ),您可见到与模块版本检查相关三个小节。


清单 8. 文件 kobject-example.mod.c
				
 # cat ./kobject/kobject-example.mod.c 
 ... 

 MODULE_INFO(vermagic, VERMAGIC_STRING); 

 struct module __this_module 
 __attribute__((section(".gnu.linkonce.this_module"))) = { 
 ... 
 }; 

 static const struct modversion_info ____versions[] 
 __used 
 __attribute__((section("__versions"))) = { 
 ... 
 }; 

 static const char __module_depends[] 
 __used 
 __attribute__((section(".modinfo"))) = 
"depends="; 

 MODULE_INFO(srcversion, "B06F9B8B7AB52AEED247B9F"); 

清单 8 中显示了模块 kobject-example.ko 中的三个 section 以及宏 MODULE_INFO,最后一行 srcversion 则需开启内核配置选项 MODULE_SRCVERSION_ALL。经上述,我们知道这三个 section 正是模块版本检查的附加信息。我们通过工具 objdump 查看 .modinfo 小节(见 清单 9, 即模块的 vermagic 信息)。<module>.ko 的附加信息合并自文件 <module>.o 与文件 <module>.mod.o。内核工具 modpost 完成了一这步骤,且该工具是 Linux 2.6 内核模块构建时所必须的。


清单 9. 使用工具 objdump 查看 .modinfo 小节
				
 # objdump --section=.modinfo -s hello/hello.o 

 hello/hello.o:     file format elf64-x86-64 

 Contents of section .modinfo: 
 0000 6c696365 6e73653d 4475616c 20425344  license=Dual BSD 
 0010 2f47504c 00                          /GPL. 

 # modinfo hello/hello.o 
 filename:       hello/hello.o 
 license:        Dual BSD/GPL 

 # objdump --section=.modinfo -s hello/hello.mod.o 
 ... 
 # objdump --section=.modinfo -s hello/hello.ko 
 ... 

经上述,我们可知内核树的顶层 Makefile 文件包含了内核版本的信息,且该信息经编译后被添加到模块的(头文件 include/generated/utsrelease.h 保存的内核版本信息来自顶层 Makefile)。表 1中,工具 lsmod 打开文件 /proc/modules 查询当前内核中已装载的模块(见清单 10),文件 /proc/modules 还被 rmmod 在卸载模块时使用。另外,若您在装载模块 hello.ko 后没能在终端下看到相应的字符串输出,则需检查文件 /proc/sys/kernel/printk,并重设下消息级别。


清单 10. 工具 lsmod 的使用
				
 # insmod ./kobject/kobject-example.ko 

 # ls /sys/kernel/kobject_example/ 
 bar  baz  foo 

 # lsmod | grep kobject 
 kobject_example        12857  0 

 # cat /proc/modules | grep kobject 
 kobject_example 12857 0 - Live 0xffffffffa0523000 

Linux 2.6 构建模块时工具 modpost 被 scripts/Makefile.modpost 调用,生成 <module>.mod.c 及文件 Module.symvers(见 清单 15)。在开启内核选项 CONFIG_MODVERSIONS 之后,文件 Makefile.Build 会调用工具 genksyms(现位于内核树 scripts/genksyms 目录下,在 Linux 2.4 时是模块工具集 Modutils 的一部分)生成 CRC 信息(见 清单 11)。其中代码 call cmd_gensymtypes 就是对工具 genksyms 的调用。另外一个较为明晰的方式是,使用工具 objdump 或 readelf 查看相关的 ELF 小节,并使用 make – n 查看模块构建过程。


清单 11. 文件 Makefile.Build 的部分内容
				
 ifndef CONFIG_MODVERSIONS 
 cmd_cc_o_c = $(CC) $(c_flags) -c -o $@ $< 
 else 
 cmd_cc_o_c = $(CC) $(c_flags) -c -o $(@D)/.tmp_$(@F) $< 
 cmd_modversions =              \ 
  if $(OBJDUMP) -h $(@D)/.tmp_$(@F) | grep -q __ksymtab; then  \ 
    $(call cmd_gensymtypes, $(KBUILD_SYMTYPES))    \ 
        > $(@D)/.tmp_$(@F:.o=.ver);      \ 
                  \ 
    $(LD) $(LDFLAGS) -r -o $@ $(@D)/.tmp_$(@F)     \ 
      -T $(@D)/.tmp_$(@F:.o=.ver);      \ 
    rm -f $(@D)/.tmp_$(@F) $(@D)/.tmp_$(@F:.o=.ver);  \ 
  else                \ 
    mv -f $(@D)/.tmp_$(@F) $@;        \ 
  fi; 
 endif 

模块的构建与测试

为内核构建外部模块前,我们须准备一颗内核源码树(kernel source tree)。内核源码树就是一套包含系统配置及内核头文件的内核目录树。须指出的是 Linux 2.6 的内核源码树与 2.4 的不同,先前的内核只需一套内核头文件就可以了,但在 2.6 的内核源码树中还需存在一些目标文件及工具,如 scripts/mod/modpost 等。清单 12 所示是从内核源码进行内核模块预编译以此生成内核树,当然您也可使用 Linux 发行版的内核源码树(系统内核树一般存放在 /lib/modules/<kernel version>/build,如果存在的话)。


清单 12. 预编译内核模块
				
 # make menuconfig 
 # make modules_prepare 
 # 

当然,我们最先须根据主机的硬件信息产生内核配置文件 .config。您可使用命令 make menuconfig 或 make config 等来配置与模块相关的选项(清单 13 清单 14相互对应,显示了模块相关的内核配置选项)。设置选项 CONFIG_MODULES=y 以及 CONFIG_MODVERSIONS=y 使内核支持模块的版本检查。另须注意的是,模块预编译并不生成 Module.symvers 文件,即使您开启了 CONFIG_MODVERSIONS 选项。因此最好的方式是完全编译 Linux 内核。


清单 13. 使用 make menuconfig 配置内核模块选项
				
 # make menuconfig 
 --- Enable loadable module support 
    [ ]   Forced module loading 
    [*]   Module unloading 
    [ ]     Forced module unloading 
    [*]   Module versioning support 
    [*]   Source checksum for all modules 


清单 14. 模块相关的内核配置选项
				
 CONFIG_MODULES 
 CONFIG_MODULE_FORCE_LOAD 
 CONFIG_MODULE_UNLOAD 
 CONFIG_MODULE_FORCE_UNLOAD 
 CONFIG_MODVERSIONS 
 CONFIG_MODULE_SRCVERSION_ALL 

内核 2.6 时,我们常为模块的构建编写一个 Makefile 文件,但仍可使用类似内核 2.4 下的模块构建命令。清单 15 展示了外部模块构建的 make 命令,其中 $KDIR 是内核树的绝对路径,$MDIR 是期望构建的模块的绝对路径(若是内部模块则可使用 make CONFIG_EXT2_FS=m …)。


清单 15. 构建外部模块
				
 # make -C $KDIR M=$MDIR [target] 

 # make -C /lib/modules/2.6.38-10-generic/build M=$PWD/hello  modules 
 make: Entering directory `/usr/src/linux-headers-2.6.38-10-generic'
 CC [M] /home/harris/work/samples/hello/hello.o 
 Building modules, stage 2. 
 MODPOST 1 modules 
 CC /home/harris/work/samples/hello/hello.mod.o 
 LD [M] /home/harris/work/samples/hello/hello.ko 
 make: Leaving directory `/usr/src/linux-headers-2.6.38-10-generic'

结束语

虽然本文已尽量集中描述可装载模块的版本检查机制,但是仍然涉及了非常多的内容。您需花一些时间来了解这些看似与模块不相关的内容,如 /proc、/sys 文件系统、ELF(即 Executable and Linkable Format)格式等,以此更好的全面理解内核可装载模块以及模块版本版本检查机制。另外,由于文章讨论的是 Linux 2.6 的外部模块,没有清晰的讲述内核的 Kbuild 系统及 Makefile 文件,但这对于模块亦是重要的内容。


下载

描述名字大小下载方法
样例代码samples.tar.gz38KBHTTP

关于下载方法的信息



Linux 可加载内核模块剖析

2.6 内核透视图

Linux® 可加载内核模块(从内核的 1.2 版本开始引入)是 Linux 内核的最重要创新之一。它们提供了可伸缩的、动态的内核。探索隐藏在可加载模块后面的原理,并学习这些独立的对象如何动态地转换成 Linux 内核的一部分。

M. Tim Jones, 顾问工程师, Emulex Corp.

2008 年 8 月 07 日

  • +内容

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 函数是用户定义的,所以存在 module_initmodule_exit 宏,用于定义这些函数属于哪种函数。LKM 还包含一组必要的宏和一组可选的宏,用于定义模块的许可证、模块的作者、模块的描述等等。图 1 提供了一个非常简单的 LKM 的视图。

图 1. 简单 LKM 的源代码视图
简单 LKM 的源代码视图

2.6 版本的 Linux 内核提供了一个新的更简单的方法,用于构建 LKM。构建 LKM 时,可以使用典型的用户工具管理模块(尽管内部已经改变):标准 insmod(安装 LKM),rmmod (删除 LKM),modprobeinsmodrmmod 的包装器),depmod(用于创建模块依赖项),以及 modinfo(用于为模块宏查找值)。更多关于为 2.6 版本内核构建 LKM 的信息,请查看 参考资料

剖析内核模块对象

LKM 只不过是一个特殊的可执行可链接格式(Executable and Linkable Format,ELF)对象文件。通常,必须链接对象文件才能在可执行文件中解析它们的符号和结果。由于必须将 LKM 加载到内核后 LKM 才能解析符号,所以 LKM 仍然是一个 ELF 对象。您可以在 LKM 上使用标准对象工具(在 2.6 版本中,内核对象带有后缀 .ko,)。例如,如果在 LKM 上使用 objdump 实用工具,您将发现一些熟悉的区段(section),比如 .text(说明)、.data(已初始化数据)和 .bss(块开始符号或未初始化数据)。

您还可以在模块中找到其他支持动态特性的区段。.init.text 区段包含 module_init 代码,.exit.text 区段包含 module_exit 代码(参见图 2)。.modinfo 区段包含各种表示模块许可证、作者和描述等的宏文本。

图 2. 具有各种 ELF 区段的 LKM 的示例
具有各种 ELF 区段的 LKM 的示例

了解 LKM 的基础知识之后,现在我们进一步探索模块是如何进入内核的,以及在内核内部是如何管理模块的。

LKM 的生命周期

在用户空间中,insmod(插入模块)启动模块加载过程。insmod 命令定义需要加载的模块,并调用 init_module 用户空间系统调用,开始加载过程。2.6 版本内核的 insmod 命令经过修改后变得非常简单(70 行代码),可以在内核中执行更多工作。insmod 并不进行所有必要的符号解析(处理 kerneld),它只是通过 init_module 函数将模块二进制文件复制到内核,然后由内核完成剩余的任务。

init_module 函数通过系统调用层,进入内核到达内核函数 sys_init_module(参见图 3)。这是加载模块的主要函数,它利用许多其他函数完成困难的工作。类似地,rmmod 命令会使 delete_module 执行 system call 调用,而 delete_module 最终会进入内核,并调用 sys_delete_module 将模块从内核删除。

图 3. 加载和卸载模块时用到的主要命令和函数
加载和卸载模块时用到的主要命令和函数

在模块的加载和卸载期间,模块子系统维护了一组简单的状态变量,用于表示模块的操作。加载模块时,状态为 MODULE_STATE_COMING。如果模块已经加载并且可用,状态为 MODULE_STATE_LIVE。此外,卸载模块时,状态为 MODULE_STATE_GOING

模块加载细节

现在,我们看看加载模块时的内部函数(参见图 4)。当调用内核函数 sys_init_module 时,会开始一个许可检查,查明调用者是否有权执行这个操作(通过 capable 函数完成)。然后,调用 load_module 函数,这个函数负责将模块加载到内核并执行必要的调试(后面还会讨论这点)。load_module 函数返回一个指向最新加载模块的模块引用。这个模块加载到系统内具有双重链接的所有模块的列表上,并且通过 notifier 列表通知正在等待模块状态改变的线程。最后,调用模块的 init() 函数,更新模块状态,表明模块已经加载并且可用。

图 4. 内部(简化的)模块加载过程
内部(简化的)模块加载过程

加载模块的内部细节是 ELF 模块解析和操作。load_module 函数(位于 ./linux/kernel/module.c)首先分配一块用于容纳整个 ELF 模块的临时内存。然后,通过 copy_from_user 函数将 ELF 模块从用户空间读入到临时内存。作为一个 ELF 对象,这个文件的结构非常独特,易于解析和验证。

下一步是对加载的 ELF 映像执行一组健康检查(它是有效的 ELF 文件吗?它适合当前的架构吗?等等)。完成健康检查后,就会解析 ELF 映像,然后会为每个区段头创建一组方便变量,简化随后的访问。因为 ELF 对象的偏移量是基于 0 的(除非重新分配),所以这些方便变量将相对偏移量包含到临时内存块中。在创建方便变量的过程中还会验证 ELF 区段头,确保加载的是有效模块。

任何可选的模块参数都从用户空间加载到另一个已分配的内核内存块(第 4 步),并且更新模块状态,表明模块已加载(MODULE_STATE_COMING)。如果需要 per-CPU 数据(这在检查区段头时确定),那么就分配 per-CPU 块。

在前面的步骤,模块区段被加载到内核(临时)内存,并且知道哪个区段应该保持,哪个可以删除。步骤 7 为内存中的模块分配最终的位置,并移动必要的区段(ELF 头中的 SHF_ALLOC,或在执行期间占用内存的区段)。然后执行另一个分配,大小是模块必要区段所需的大小。迭代临时 ELF 块中的每个区段,并将需要执行的区段复制到新的块中。接下来要进行一些额外的维护。同时还进行符号解析,可以解析位于内核中的符号(被编译成内核映象),或临时的符号(从其他模块导出)。

然后为每个剩余的区段迭代新的模块并执行重新定位。这个步骤与架构有关,因此依赖于为架构(./linux/arch/<arch>/kernel/module.c)定义的 helper 函数。最后,刷新指令缓存(因为使用了临时 .text 区段),执行一些额外的维护(释放临时模块内存,设置系统文件),并将模块最终返回到 load_module

模块卸载细节

卸载模块的过程和加载模块基本一样,除了必须进行几个健康检查外(确保安全删除模块)。卸载模块过程首先在用户空间调用 rmmod(删除模块)命令。在 rmmod 命令内部,对 delete_module 执行系统调用,它最终会导致在内核内部调用 sys_delete_module(查看 图 3)。图 5 演示了删除模块的基本操作过程。

图 5. 内部(简化的)模块卸载过程
内部(简化的)模块卸载过程

当调用内核函数 sys_delete_module(将要删除的模块的名称作为参数传入)之后,第一步便是确保调用方具有权限。接下来会检查一个列表,查看是否存在依赖于这个模块的其他模块。这里有一个名为 modules_which_use_me 的列表,它包含每个依赖模块的一个元素。如果这个列表为空,就不存在任何模块依赖项,因此这个模块就是要删除的模块(否则会返回一个错误)。接下来还要测试模块是否加载。用户可以在当前安装的模块上调用 rmmod,因此这个检查确保模块已经加载。在几个维护检查之后,倒数第二个步骤是调用模块的 exit 函数(模块内部自带)。最后,调用 free_module 函数。

调用 free_module 函数之后,您将发现模块将被安全删除。该模块不存在依赖项,因此可以开始模块的内核清理过程。首先,从安装期间添加的各种列表中(系统文件、模块列表等)删除模块。其次,调用一个与架构相关的清理例程(可以在 ./linux/arch/<arch>/kernel/module.c 中找到)。然后迭代具有依赖性的模块,并将这个模块从这些列表中删除。最后,从内核的角度而言,清理已经完成,为模块分配的各种内存已被释放,包括参数内存、per-CPU 内存和模块的 ELF 内存(coreinit)。

为模块管理优化内核

在许多应用程序中,动态加载模块非常重要,但加载之后,就没有必要卸载模块。这允许内核在启动时是动态的(根据找到的设备加载模块),但并不是在整个操作过程中都是动态的。如果不需要在加载之后卸载模块,那么可以进行一些优化,减少模块管理所需的代码。您可以 “取消” 内核配置选项 CONFIG_MODULE_UNLOAD,删除大量与卸载模块相关的内核功能。

结束语

这一直是内核里面模块管理过程的高级视图。要获得模块管理的细节,源代码本身就是最佳的文档。关于在模块管理中调用的主要函数,请查看 ./linux/kernel/module.c(以及 ./linux/include/linux/module.h 中的头文件)。您还可以在 ./linux/arch/<arch>/kernel/module.c 中找到几个与架构相关的函数。最后,可以在 ./linux/kernel/kmod.c 中找到内核自动加载函数(可以根据需要从内核自动加载模块)。这个功能可以通过 CONFIG_KMOD 配置选项启用。

参考资料

学习

获得产品和技术

  • 使用可直接从 developerWorks 下载的 IBM 试用软件 构建您的下一个 Linux 开发项目。

讨论


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值