2. Linux 内核模块基础:模块的编写、编译、加载与卸载
Linux 内核模块是内核在运行时动态加载和卸载的代码片段,它允许开发者在不重新编译整个内核的情况下扩展内核功能。本章将系统讲解模块的完整生命周期:从编写、编译到加载、卸载,并深入剖析 module_init/module_exit 宏、LICENSE 与作者信息等关键要素。
2.1 模块的基本结构
一个标准的内核模块通常包含以下核心部分:
- 头文件包含:
<linux/module.h>(模块通用接口)和<linux/init.h>(初始化/退出宏)。 - 入口函数:通过
module_init()注册,模块加载时自动调用。 - 出口函数:通过
module_exit()注册,模块卸载时自动调用。 - 许可证声明:使用
MODULE_LICENSE()指定开源协议。 - 作者与描述:使用
MODULE_AUTHOR()和MODULE_DESCRIPTION()提供元信息。
💡 核心要点:模块入口和出口函数是内核模块的“构造函数”和“析构函数”,它们必须通过宏注册,而非直接调用。
2.2 模块编写实战:Hello World 示例
以下是一个最简内核模块的完整代码,它演示了模块的编写规范:
#include <linux/module.h> // 所有模块都需要
#include <linux/init.h> // 提供 module_init/module_exit 宏
#include <linux/kernel.h> // 提供 printk 等内核打印函数
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Kernel Module Loaded!\n");
return 0; // 返回 0 表示加载成功
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel Module Unloaded!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Expert <expert@example.com>");
MODULE_DESCRIPTION("A simple Hello World kernel module for LED/GPIO course");
MODULE_VERSION("1.0");
2.3 module_init / module_exit 宏解析
| 宏名称 | 作用 | 注册的函数原型 | 返回值含义 |
|---|---|---|---|
module_init() |
指定模块加载时自动调用的入口函数 | int __init func(void) |
0 成功;负数表示错误码 |
module_exit() |
指定模块卸载时自动调用的出口函数 | void __exit func(void) |
无返回值 |
🔧 编译提示:
__init 和 __exit 标记告诉内核:入口函数在初始化完成后可以释放内存(如果模块被编译进内核而非动态加载),出口函数在模块卸载后也可释放。这有助于节省内核内存。
2.4 模块的编译:Makefile 编写
内核模块的编译必须依赖内核源码树(或内核头文件包)。标准 Makefile 模板如下:
obj-m := hello.o
# KDIR 指向已编译的内核源码树路径
KDIR := /lib/modules/$(shell uname -r)/build
# PWD 是当前工作目录
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
执行 make 后,会生成 hello.ko 文件,这就是可加载的内核模块目标文件。
2.5 模块的加载与卸载命令
| 操作 | 命令 | 说明 |
|---|---|---|
| 加载模块 | sudo insmod hello.ko |
将模块插入内核,调用 module_init 注册的函数 |
| 查看已加载模块 | lsmod |
列出所有内核模块及其依赖、使用计数 |
| 查看模块信息 | modinfo hello.ko |
显示作者、许可证、描述、参数等元数据 |
| 卸载模块 | sudo rmmod hello |
从内核移除模块,调用 module_exit 注册的函数 |
⚠️ 常见错误:如果
insmod 返回 Operation not permitted,请确认使用 sudo 或以 root 身份执行。如果返回 Unknown symbol,说明模块依赖的其他模块未加载或内核版本不匹配。
2.6 LICENSE 与作者信息:为什么重要?
内核模块必须声明许可证,这不仅是法律要求,也直接影响模块可使用的内核 API 范围:
- GPL 兼容许可证(如 GPL、GPL v2):可以使用所有内核导出的符号,包括
EXPORT_SYMBOL_GPL标记的函数。 - 非 GPL 许可证(如 Proprietary):只能使用
EXPORT_SYMBOL导出的符号,无法调用 GPL-only 接口。 - 作者与描述:通过
modinfo可查看,有助于模块管理和调试。
2.7 模块生命周期流程图
下图展示了内核模块从编写到卸载的完整生命周期:
2.8 模块信息查看与调试技巧
加载模块后,可以通过以下方式验证模块是否正常工作:
- 查看内核日志:
dmesg | tail -10可看到printk输出的信息。 - 检查模块列表:
lsmod | grep hello确认模块已加载。 - 查看模块元数据:
modinfo hello.ko显示许可证、作者、描述等。 - 卸载验证:卸载后再次
dmesg查看出口函数是否执行。
📌 实战建议:在嵌入式开发板上,建议在 Makefile 中指定交叉编译工具链的
CROSS_COMPILE 变量,并将 KDIR 指向目标板的内核源码路径。例如:CROSS_COMPILE=arm-linux-gnueabihf-。
2.9 本章小结
本章完成了 Linux 内核模块的完整入门:从编写最简模块、理解 module_init/module_exit 宏的注册机制,到编写 Makefile 编译、使用 insmod/rmmod 加载卸载,以及通过 MODULE_LICENSE 等宏声明模块信息。这些基础操作是后续 LED 驱动和 GPIO 中断响应调试的基石,请务必亲手编译并运行一次 Hello World 模块,确认开发环境正确配置。