目录
1. 基础概念
1.1 内核模块是什么?Linux为什么要引入内核模块这一机制?
内核,是一个操作系统的核心,是基于硬件的第一层软件封装,提供操作系统的最基本的功能,是操作系统运行的基础,决定着整个操作系统的性能和稳定性。内核按照体系结构分为两类:微内核和宏内核。
微内核:
在微内核架构中,内核只提供操作系统的核心功能,如进程管理,存储器管理,进程间通信,I/O设备管理等。而其它功能如文件系统、设备驱动等不被包含到内核中,将这些功能模块置于微内核之外,所以针对这些功能模块的修改并不会影响到微内核的核心性能。微内核具有动态扩展性强的优点。如Windows 操作系统、华为的鸿蒙操作系统就属于这类微内核架构。
宏内核:
宏内核架构是将上述包括微内核以及微内核之外的应用层IPC、文件系统功能、设备驱动模块都编译成一个整体。其优点是执行效率非常高,但缺点也是十分明显的,一旦想要修改、增加内核某个功能时(如增加设备驱动程序)都需要重新编译一遍内核。Linux 操作系统正是采用了宏内核结构。为了解决这一缺点,linux 中引入了内核模块这一机制。
1.2 内核模块引入原因
Linux 是一个跨平台的操作系统,支持众多的设备,在Linux 内核源码中有超过50% 的代码都与设备驱动相关。Linux 为宏内核架构,如果开启所有的功能,内核就会变得十分臃肿。因此就需要解决内核庞大臃肿的问题,Linux就引入了内核模块机制,内核模块是实现了某个功能的一段内核代码,在内核运行过程,可以加载这部分代码到内核中,从而动态地扩展了内核的功能。基于这种特性,我们进行设备驱动开发时,以内核模块的形式编写设备驱动,只需要编译相关的驱动代码即可,无需对整个内核进行编译。
1.3 内核模块涉及的操作命令
- lsmod
功能:打印出当前内核中已经安装的模块列表
英文:list module,将模块列表显示 - insmod
功能:向当前内核中去安装一个模块,用法是insmod xxx.ko
英文:install module,安装模块 - modinfo
功能:打印出一个内核模块的自带信息
英文:module information,模块信息
用法:modinfo xxx.ko
,注意要加.ko,也就是说是一个静态的文件形式。 - rmmod
功能:从当前内核中卸载一个已经安装了的模块
英文:remove module,卸载模块
用法:rmmod xxx.ko
,rmmod xxx
都可以 - modprobe
功能:和insmod一样都是载入内核,不过modprobe能够处理module载入的相依问题。
英文:module probe, 模块探测
用法:rmmod xxx
,注意这里无需输入.ko后缀 - depmod
功能:用于生成内核模块的依赖关系列表。它通过分析/lib/modules/kernel-release目录中的内核模块,创建一个类似“Makefile”的依赖文件,名为modules.dep。这些模块通常来自配置文件中指定的目录或在命令行上提到的目录。
英文:dependency modules
2. 内核模块加载的过程
2.1 提要
内核模块加载是一个将模块代码(.ko 文件)动态插入运行中的内核,使其成为内核功能一部分的过程。
这个过程涉及用户空间工具和内核空间协作。以下是详细步骤。
2.2 用户空间:触发加载
工具: 用户通常使用insmod
(直接加载) 或modprobe
(智能加载,处理依赖) 命令。
命令:
sudo insmod /path/to/module.ko # 直接加载指定模块文件
sudo modprobe module_name # 按模块名加载,自动处理依赖
2.3 内核空间:系统调用
系统调用入口:insmod/modprobe
最终会调用 init_module() 或 finit_module() 系统调用。
init_module(void *module_image, unsigned long len, const char *param_values): 将整个模块镜像(.ko 文件内容)复制到内核空间。
finit_module(int fd, const char *param_values, int flags): 更高效,通过文件描述符 fd 让内核直接读取 .ko 文件内容。
2.4 内核空间:核心加载流程
内核收到加载请求后,执行一系列关键操作:
2.4.1 权限检查:
- 检查调用进程是否具有 CAP_SYS_MODULE 能力(通常需要 root 权限)。
- 检查内核是否启用了模块加载 (CONFIG_MODULES=y)。
- (现代内核重要安全机制) 模块签名验证:
如果内核配置要求模块签名 (CONFIG_MODULE_SIG=y),内核会使用内置的公钥验证模块的加密签名。确保模块来源可信且未被篡改,防止加载恶意模块。
2.4.2 分配内存:
在内核空间分配内存,用于存放即将加载的模块代码、数据和元数据。
2.4.3 解析模块文件 (ELF 格式):
1.内核将 .ko 文件视为一个 ELF (Executable and Linkable Format) 对象。
2.解析 ELF 头部、程序头、节头等信息,识别出:
.text
节(代码段): 模块的可执行指令。.data/.bss
节 (数据段): 模块的已初始化/未初始化全局变量。.rodata
节: 只读数据。.modinfo
节: 包含模块的元信息(作者、描述、许可证、依赖、参数等)。__versions
节: 包含模块所依赖的内核符号及其预期 CRC 校验值(用于版本检查)。.symtab/.strtab
节: 符号表和字符串表(用于符号解析)。.rel.*
节: 重定位信息(用于地址修正)。
2.4.4 版本和许可证检查:
版本检查 (CONFIG_MODVERSIONS):
- 内核计算模块引用的每个内核符号的实际 CRC 值。
- 与模块 __versions 节中存储的预期 CRC 值进行比较。
- 如果不匹配,说明内核接口可能已改变,加载会失败(防止模块使用不兼容的接口导致崩溃)。
许可证检查 (MODULE_LICENSE)::
- 检查模块声明的许可证(如 GPL, GPL v2, BSD, Proprietary 等)。
- 如果模块声明为 GPL 或兼容许可证,它可以访问内核中使用
EXPORT_SYMBOL_GPL()
导出的专有 GPL 接口。 - 非 GPL 模块只能访问使用 EXPORT_SYMBOL() 导出的接口。
2.4.5 符号解析与重定位:
- 解析未定义符号: 遍历模块的符号表,找出所有未定义的符号(即模块需要但自身未定义的函数或变量)。
- 查找内核符号表: 在内核导出的全局符号表 (
/proc/kallsyms
) 中查找这些符号的地址。
内核使用EXPORT_SYMBOL()
或EXPORT_SYMBOL_GPL()
导出的符号才能被模块使用。 - 处理依赖模块的符号: 如果符号是由另一个模块导出的:
内核会确保该依赖模块已被加载(modprobe
在用户空间已处理)。
从依赖模块的导出符号表中查找地址。 - 重定位: 根据找到的符号地址和模块内的重定位信息(
.rel.*
节),修改模块代码和数据段中的引用地址,使其指向正确的内核或依赖模块符号。
2.4.6 执行模块初始化函数:
- 找到模块源代码中通过 module_init() 宏指定的初始化函数入口点。
- 调用初始化函数: 内核执行这个函数。
- 初始化函数的任务:
向内核注册模块提供的功能(如:注册字符设备驱动cdev_add()
、注册文件系统register_filesystem()
、注册网络协议proto_register()
等)。
申请资源(内存、I/O 端口、IRQ 中断线等)。
初始化模块内部数据结构。
执行其他必要的设置工作。 - 返回值: 初始化函数返回 0 表示成功。返回非零值表示失败,加载过程会终止,模块会被卸载。
2.4.7 模块状态更新:
- 将新模块添加到内核维护的模块链表 (
struct module
) 中。 - 更新
/proc/modules
和sysfs
(/sys/module/
) 中的信息,使模块可见。 - 模块状态标记为
LIVE
。
2.5 加载完成
- 用户空间工具 (
insmod/modprobe
) 收到内核返回的成功信号后退出。 - 模块的功能(如驱动、文件系统、网络协议等)立即生效,无需重启系统。
- 可以使用
lsmod
命令查看已加载的模块列表及其依赖关系。
2.6 总结
用户触发: insmod 或 modprobe 命令发起请求。
系统调用: 通过 init_module/finit_module 进入内核。
安全检查: 权限、签名验证。
内存分配: 为模块分配内核内存。
ELF 解析: 拆解 .ko 文件结构。
兼容性检查: 版本 (CRC)、许可证。
符号解析与重定位: 链接模块到内核和其他模块(核心链接过程)。
初始化执行: 调用 module_init 函数注册功能、申请资源。
加入系统: 将模块加入内核链表,更新状态信息。
3. 内核模块简单示例
3.1 编写内核模块代码
创建一个名为 hello.c 的文件,并在其中编写以下代码:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello Kernel Module\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel Module!\n");
}
/*声明 hello_init是模块的加载函数,即内核加载此模块时调用此函数*/
module_init(hello_init);
/*声明 hello_init是模块的卸载函数,即内核卸载此模块时调用此函数*/
module_exit(hello_exit);
/*模块所遵循的版本约束*/
MODULE_LICENSE("GPL");
这个简单的内核模块在初始化时打印"Hello, Kernel Module!“,在退出时打印"Goodbye, Kernel Module!”。
…未完