模块是一种向 Linux 内核添加设备驱动程序、文件系统及其他组件的有效方法,不需编译新内核或重启系统。
模块具有如下优点:
• 通过使用模块,内核发布者能够预先编译大量驱动程序,而不会致使内核映像的尺寸发生膨胀;
• 内核开发者可以将试验性的代码打包到模块中,模块可以卸载,修改代码或重新打包后可以重新装载。
从用户的角度来看,模块可以通过两个不同的系统程序添加到运行的内核中。它们分别是modprobe
和insmod
。
modprobe
在识别出目标模块所依赖的模块之后,在内核也会使用insmod
,在用户空间对模块的处理也是基于insmod
。
在处理该系统调用时,模块代码首先复制到内核内存中 ,接下来是重定位工作和解决模块中未定义的引用。
- 因为模块使用了持久编译到内核中的函数,在模块本身编译时无法确定这些函数的地址,所以需要在这里处理未定义的引用。
处理未解决的引用,为与内核的剩余部分协作,模块必须使用内核提供的函数。这些可能是通用的辅助函数,比如几乎内核每一部分都会使用的printk
或kmalloc
。 - 函数定义在内核的基础代码中,因而已经加载到内存,为了找到与相关函数名称匹配的地址,内核提供了一个所有导出函数的列表
/proc/kallsyms
文件。该列表给出了所有导出函数的内存地址和对应函数名,可以通过proc文件系统访问:
用户空间工具和内核的模块实现之间的接口,包括两个系统调用:
(1)init_module
:将一个新模块插入到内核中。用户空间工具只需提供二进制数据。所有其他工作(特别是重定位和解决引用)由内核自身完成。
(2)delete_module
:从内核移除一个模块。当然,前提是该模块的代码不再使用,并且其他模块也不再使用该模块导出的函数。
还有一个 request_module
函数(不是系统调用),用于从内核端加载模块。它不仅用于加载模块,还用于实现热插拔功能。
模块的表示
module
是最重要的数据结构。内核中驻留的每个模块,都分配了该结构的一个实例。
state
表示该模块的当前状态,可以从枚举类型module_state
取值:
syms
、 num_syms
和crc
用于管理模块导出的符号。 syms
是一个数组,有num_syms
个数组项,数组项类型为kernel_symbol
,负责将标识符( name
)分配到内存地址( value
):
依赖关系和引用
如果模块 B 使用了模块 A 提供的函数,那么模块 A 和模块 B 之间就存在关系。 可以用两种不同的方式来看这种关系:
(1) 模块 B 依赖模块 A。除非模块 A 已经驻留在内核内存,否则模块 B 无法装载;
(2) 模块 B 引用模块 A。换句话说,除非模块 B 已经移除,否则模块 A 无法从内核移除。事实上,条件应该是所有引用模块 A 的模块都已经从内核移除。在内核中,这种关系称之为模块 B 使用模块 A。
为正确管理这些依赖关系,内核需要引入另一个数据结构:
模块的二进制结构
模块使用 ELF 二进制格式,模块中包含了几个额外的段,普通的程序或库中不会出现。
除了少量由编译器产生、与我们的讨论不相关的段(主要是重定位段),模块由以下ELF段组成。
生成模块需要执行下述3个步骤:
(1) 首先,模块源代码中的所有 C 文件都编译为普通的.o
目标文件;
(2) 在为所有模块产生目标文件后,内核可以分析它们。找到的附加信息(例如,模块依赖关系)保存在一个独立的文件中,也编译为一个二进制文件;
(3) 将前述两个步骤产生的二进制文件链接起来,生成最终的模块。
a.初始化及清理函数 :<init.h>
中的module_init
和module_exit
宏用于定义init
函数和exit
函数。
b.导出符号 :内核为导出符号提供了两个宏: EXPORT_SYMBOL
和 EXPORT_SYMBOL_GPL
。顾名思义,二者分别用于一般的导出符号和只用于GPL兼容代码的导出符号。同样,其目的在于将相应的符号放置到模块二进制映象的适当段中;
在导出符号时,内核不仅考虑可以由所有模块使用符号,还要考虑只能由GPL兼容模块使用的符号。内核提供 license_is_gpl_compatible
函数来判断给定许可是否与GPL兼容
GPL软件的使用者有权力得到软件的代码,只要使用了GPL,在发布(redistribution)时,整个项目也必须是GPL的,即主程序和静态链接的库(linux的.a和Windows的.lib)必须是GPL的,动态链接库(Linux的.so,Windows的.dll)必须是GPL兼容的。所谓GPL兼容,也就是GPL软件中可以使用的库,这些许可证必须比GPL弱(如LGPL,BSD),而不能是某个商业许可证。正因如此,GPL是带有很强的传染性,只要你的软件使用了GPL的代码,那么就请以GPL开放源代码吧,并且你的项目中也不能有任何和GPL不兼容的库。
c.一般模块信息:模块许可证、开发者和描述、备选名称、基本版本控制。
插入删除模块
插入模块:init_module
系统调用是用户空间和内核之间用于装载新模块的接口,通过 load_module
函数将二进制数据传输到内核地址空间中:
二进制数据使用load_module
传输到内核地址空间中,所有需要的重定位都会完成,所有引用都会解决。
在load_module
函数中创建module
实例已经添加到全局的modules
链表后,内核内需要调用模块的初始化函数并释放初始化数据占用的内存即可。
在实现 load_module
时会出现异常,内核源代码中该函数:完成所有碰到异常问题,此函数可以完成任务如下:
- 从用户空间复制模块数据到内核地址空间中的一个临时内存位置;
- 查找各个段位置;
- 确保内核和模块版本控制字符串和struct module的定义匹配问题;
- 将存在的各个段分配到其在内存中的最终位置;
- 重定位符号并解决引用,链接到模块符号任何版本控制信息的都会注意到处理模块的参数。
删除模块:从内核移除模块比插入模块简单得多,系统调用delete_module
函数来实现移除模块