内核模块机制
什么是内核模块
内核模块(Kernel Module)是可以在运行时动态加载/卸载的内核代码片段,文件扩展名为 .ko(Kernel Object)。驱动开发几乎都以模块形式进行,好处是:
- 无需重新编译整个内核
- 出错时可以卸载,不必重启
- 减小内核镜像体积
最小模块示例
c
// hello.c
#include <linux/module.h>
#include <linux/init.h>
static int __init hello_init(void)
{
pr_info("Hello, kernel!\n");
return 0; /* 返回非零值表示初始化失败,模块不会加载 */
}
static void __exit hello_exit(void)
{
pr_info("Goodbye, kernel!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A minimal kernel module example");
MODULE_VERSION("1.0");对应的 Makefile:
makefile
# Makefile
obj-m += hello.o
# 内核源码路径(通常指向正在运行的内核头文件)
KDIR ?= /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean编译与加载:
bash
make
sudo insmod hello.ko
dmesg | tail -5 # 查看 "Hello, kernel!"
sudo rmmod hello
dmesg | tail -5 # 查看 "Goodbye, kernel!"模块加载流程
insmod hello.ko
│
▼
sys_init_module() # 系统调用入口
│
├── 读取 .ko ELF 文件
├── 内核版本魔数校验(vermagic)
├── 符号依赖解析(依赖其他模块的符号)
├── 分配模块内存(module_alloc)
├── 重定位(relocate)
├── 调用 module->init() ← 即 hello_init()
└── 将模块加入 /proc/modules 链表vermagic 校验:
.ko文件头部嵌入了编译时的内核版本字符串,与当前运行内核不匹配时insmod报错Invalid module format。交叉编译时需确保KDIR指向目标内核的构建目录。
模块参数
c
static int baudrate = 115200;
static char *device = "/dev/ttyS0";
module_param(baudrate, int, 0644);
MODULE_PARM_DESC(baudrate, "Serial baud rate (default: 115200)");
module_param(device, charp, 0444);
MODULE_PARM_DESC(device, "Device node path");加载时传参:
bash
sudo insmod mydriver.ko baudrate=9600 device=/dev/ttyS1运行时通过 sysfs 修改(权限 0644 时):
bash
echo 9600 > /sys/module/mydriver/parameters/baudrate符号导出
模块间可以共享符号(函数/变量),需要显式导出:
c
// 在 module_a.c 中导出
int shared_function(int x)
{
return x * 2;
}
EXPORT_SYMBOL(shared_function); /* 所有模块可用 */
EXPORT_SYMBOL_GPL(shared_function); /* 仅 GPL 模块可用 */c
// 在 module_b.c 中使用
extern int shared_function(int x);
/* 需要在 Makefile 中声明依赖,或确保 module_a 先加载 */查看内核导出的所有符号:
bash
cat /proc/kallsyms | grep shared_function模块依赖管理
modprobe 比 insmod 更智能,能自动处理依赖:
bash
# 生成依赖数据库(安装新模块后执行)
sudo depmod -a
# 自动加载依赖
sudo modprobe mydriver
# 查看模块依赖树
modinfo mydriver | grep depends/lib/modules/$(uname -r)/modules.dep 记录了所有依赖关系。
__init 与 __exit 宏
c
static int __init my_init(void) { ... }
static void __exit my_exit(void) { ... }__init:标记初始化函数,内核启动完成后该段内存会被释放(节省内存)__exit:标记退出函数,若模块被编译进内核(非模块方式),该函数会被优化掉__initdata:同理,用于初始化阶段的数据
模块许可证
MODULE_LICENSE 影响能否使用 GPL 专属符号:
| 值 | 说明 |
|---|---|
"GPL" | GNU GPL v2,可使用所有内核符号 |
"GPL v2" | 同上 |
"GPL and additional rights" | GPL + 额外权利 |
"Dual BSD/GPL" | 双许可 |
"Proprietary" | 私有,无法使用 EXPORT_SYMBOL_GPL 符号,内核会打 taint 标记 |
常用调试命令
bash
lsmod # 列出已加载模块
modinfo hello.ko # 查看模块信息
cat /proc/modules # 模块列表(含地址)
cat /sys/module/hello/ # sysfs 中的模块信息
dmesg -w # 实时监控内核日志常见错误
| 错误信息 | 原因 | 解决方法 |
|---|---|---|
Invalid module format | vermagic 不匹配 | 确认 KDIR 指向正确内核版本 |
Unknown symbol in module | 依赖符号未导出或未加载 | 检查 modprobe 依赖,确认符号已 EXPORT_SYMBOL |
Operation not permitted | 未以 root 运行 | sudo insmod |
Device or resource busy | 模块正在使用中 | 先停止使用该设备的进程 |
Module in use | 引用计数不为零 | lsmod 查看引用计数,找到并卸载依赖模块 |