3. 第一个内核模块
内核模块是 Linux 内核可动态加载和卸载的代码片段,它运行在内核空间,拥有最高特权级。编写第一个内核模块“Hello World”是理解模块加载/卸载机制、模块许可证声明以及 module_init / module_exit 宏的最佳起点。
3.1 模块代码结构
一个完整的内核模块至少包含以下四个部分:
- 头文件包含:
#include <linux/module.h>和#include <linux/kernel.h>,提供模块 API 和内核打印函数。 - 入口函数:使用
module_init()宏注册的初始化函数,模块加载时执行。 - 出口函数:使用
module_exit()宏注册的清理函数,模块卸载时执行。 - 许可证声明:使用
MODULE_LICENSE()宏声明模块使用的许可证,通常为"GPL"。
关键点: 内核模块不能使用标准 C 库(如 glibc),只能使用内核导出的 API。打印输出使用
printk(),而非 printf()。
3.2 Hello World 模块完整代码
#include <linux/module.h> // 所有模块都需要
#include <linux/kernel.h> // 提供 printk() 和 KERN_INFO
static int __init hello_init(void)
{
printk(KERN_INFO "Hello World from Kernel Module!\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("Your Name");
MODULE_DESCRIPTION("A simple Hello World kernel module");
3.3 module_init 与 module_exit 宏
这两个宏是模块与内核框架之间的“注册契约”。它们将开发者定义的函数指针注册到内核的模块管理表中。
| 宏 | 作用 | 函数签名 | 返回值含义 |
|---|---|---|---|
module_init(x) |
指定模块加载时调用的初始化函数 | int __init func(void) |
0 成功;负数表示失败,模块加载被中止 |
module_exit(x) |
指定模块卸载时调用的清理函数 | void __exit func(void) |
无返回值 |
__init 和 __exit 标记: 这两个标记是内核的优化提示。
__init 告诉内核该函数仅在初始化阶段使用,执行完毕后可以释放占用的内存;__exit 标记的函数在模块被编译进内核(非模块方式)时会被忽略,从而节省空间。
3.4 模块许可证声明
内核模块必须声明许可证,否则内核会认为该模块是“污染”的,加载时会打印警告信息,并且某些 GPL-only 的符号将无法使用。
| 宏 | 常用值 | 说明 |
|---|---|---|
MODULE_LICENSE() |
"GPL" |
最常用,表示模块遵循 GNU General Public License v2 |
"GPL v2" |
明确指定 GPL 版本 2 | |
"Dual BSD/GPL" |
双许可证,BSD 或 GPL 均可 | |
"Proprietary" |
专有许可证,内核会标记为“污染” |
重要: 如果模块使用了
EXPORT_SYMBOL_GPL() 导出的内核符号,则必须声明 MODULE_LICENSE("GPL"),否则链接会失败。这是内核社区保护 GPL 代码不被专有模块使用的机制。
3.5 模块加载与卸载流程
下图展示了从用户空间执行 insmod 和 rmmod 命令到内核模块函数被调用的完整流程。
3.6 编译与测试步骤
- 创建 Makefile:利用内核构建系统自动编译模块。
- 执行编译:运行
make,生成hello.ko文件。 - 加载模块:
sudo insmod hello.ko,观察dmesg输出。 - 查看模块信息:
lsmod | grep hello或modinfo hello.ko。 - 卸载模块:
sudo rmmod hello,再次查看dmesg。
Makefile 示例:
obj-m += hello.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
3.7 常见错误与排查
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
insmod: ERROR: could not insert module |
内核版本不匹配或模块格式错误 | 确认 KDIR 指向当前运行内核的构建目录 |
modprobe: FATAL: Module not found |
模块未安装到标准路径 | 使用 insmod 加绝对路径,或执行 depmod |
printk 无输出 |
日志级别被控制台过滤 | 使用 dmesg 查看;或调整 /proc/sys/kernel/printk |
内核报 tainted 警告 |
缺少 MODULE_LICENSE 或声明为专有 |
添加 MODULE_LICENSE("GPL") |