基础知识
模块是一种向Linux内核添加设备驱动程序、文件系统及其他组件的有效方法,不需要编译新内核
- 优点
- 通过使用模块,内核发布者能够预先编译大量驱动程序,而不会致使内核映像的尺寸发生膨胀。
- 内核开发者可以将实验性的代码打包到模块中,模块可以卸载,修改代码或重新打包后可以重新装载。
添加和删除
- 从用户角度来看,模块可以通过两个不同的系统程序添加到运行内核中。他们分别是
modprobe
和insmod
。modprobe
在识别出目标模块所依赖的模块后,在内核也会使用insmod
,再用户空间队模块的处理也是基于insmod
。 - 在处理系统调用的时候,模块代码首先复制到内核内存中。接下来是重定位工作和解决模块中为定义的引用。因为模块使用了持久编译到内核中的函数,在模块本身编译时无法确定这些函数的地址,所以需要再这里处理为定义的引用。
- 处理未解决的引用,为与内核的剩余部分协作,模块必须使用内核提供的函数。这些可能是通用的辅助函数,比如几乎内核每一部分都会使用的
printk
或者kmalloc
。 - 很明显这些函数定义在内核的基础代码中,因而已经家在到内存。但是如何找到与相关函数名匹配的地址,以便解决这些引用呢?为此,内核提供了一个导出所有函数的列表。该列表给出了所有导出函数的内存地址和对应函数名,可以通过
proc
文件系统访问,即文件/proc/kallsyms
。
向内核添加模块时,需要考虑下列相关问题
- 内核提供的函数符号表,可以在模块加载时动态扩展其长度。
- 如果模块之间相互依赖,那么向内核添加模块的顺序很重要。
插入和删除模块
用户空间工具和内核的模块实现之间的接口,包括两个系统调用。
init_module
:将一个新模块插入到内核中。用户空间工具只需提供二进制数据。所有其他工作(特别是重定位和解决引用)由内核自身完成。delete_module
:从内核移除一个模块。当然,前提是该模块的代码不再使用,并且其他模块不再使用该模块导出的函数。
还有一个
request_module
函数(不是系统调用),用于从内核端加载模块。它不仅用于加载模块,还用于实现热插拔功能。
模块的表示
在详细讲解模块相关函数实现之前,有必要解释如何在内核中表示模块(及其属性)。首先需要定义一组数据结构。首先需要定义一组数据结构。其中,module
是最重要的数据结构。内核中驻留的每个模块,都分配了该结构的一个实例。其定义如下:
state
表示模块当前的状态,可以从枚举类型moudule_state
取值。
syms、num_syms和crc用于管理模块导出的符号。syms是一个数组,有num_syms个数组项,数组项类型为kernel_symbol
,负责将标识符(name)分配到内存地址(value):
依赖关系和引用
如果模块B依赖模块A提供的函数,那么模块A和模块B之间就存在关系。可以用两种不同的方式来看这种关系。
- 模块B依赖模块A:除非模块A已经驻留在内核内存,否则模块B无法加载。
- 模块B引用模块A:换句话说,除非模块B已经移除,否贼模块A无法从内核中移除。事实上,条件应该是所有引用模块A的模块都已经从内核移除。在内核中,这种关系称之为模块B使用模块A。为了正确管理这些依赖关系,内核需要引入另一个数据结构:
模块的二进制结构
- 生成模块的三个步骤
- 首先,模块源代码中的所有c文件都编译为普通的.o文件。
- 在为所有模块产生目标文件后,内核可以分析他们。找到的附加信息(例如,模块依赖关系)保存在一个独立的文件中,也编译为一个二进制文件。
- 将前述两个步骤的二进制文件链接起来,生成最终的模块。
初始化及清理函数
<init.h>
中的module_init
和module_exit
宏用于定义init
函数和exit
函数。
导出符号
内核为导出符号提供了两个宏:EXPORT_SYMBOL
和EXPORT_SYMBOL_GPL
。顾名思义,二者分别用于一般的导出符号和只用于GPL
兼容代码和导出符号。同样,其目的在于将相应的符号放置到模块二进制映像的适当段中。
一般模块信息
模块许可证、开发者和描述、备选名称、基本版本控制
插入模块
init_module
系统调用是用户空间和内核之间用于装载新模块的接口
插入模块
init_module
系统调用是用户空间和内核之间用于装载新模块的接口,通过load_module
函数将二进制数据传输到内核地址空间中。具体源码如下:
删除模块
从内核删除模块比插入模块简单的多,系统调用delete_module
函数实现移除模块。具体源码如下: