2. 内核模块基础:模块加载与卸载、module_init/module_exit机制、模块参数传递、模块依赖与符号导出
2.1 内核模块概述
Linux内核模块(Loadable Kernel Module, LKM)是内核在运行时动态加载和卸载的代码片段。模块机制允许开发者在不重新编译整个内核、不重启系统的情况下扩展内核功能,是驱动开发的核心基础。
模块 vs 内置驱动 对比
| 特性 | 内核模块(.ko) | 内置驱动(built-in) |
|---|---|---|
| 加载方式 | 动态加载/卸载 | 编译进内核,随内核启动 |
| 灵活性 | 高,可热插拔 | 低,需重新编译内核 |
| 内存占用 | 按需加载,节省内存 | 始终驻留内存 |
| 调试便利性 | 可单独编译、快速迭代 | 需整体编译,周期长 |
| 适用场景 | 驱动开发、实验性功能 | 稳定、必须的驱动 |
2.2 模块加载与卸载流程
模块的生命周期由两个核心入口函数控制:module_init() 和 module_exit()。加载时内核调用初始化函数,卸载时调用清理函数。
2.3 module_init / module_exit 机制
这两个宏是模块的入口和出口声明,它们将指定的初始化/清理函数注册到内核模块框架中。
2.3.1 基本实现模板
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init my_driver_init(void)
{
printk(KERN_INFO "My driver loaded successfully\n");
/* 注册设备、分配资源等 */
return 0;
}
static void __exit my_driver_exit(void)
{
printk(KERN_INFO "My driver unloaded\n");
/* 释放资源、注销设备等 */
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Linux kernel module example");
关键点:
__init标记:初始化函数在调用后释放内存,节省内核空间__exit标记:卸载函数在模块编译进内核时被忽略module_init()必须返回 0 表示成功,负值表示失败MODULE_LICENSE("GPL")是必须的,否则内核会标记为“污染”
2.4 模块参数传递
模块支持在加载时通过命令行传递参数,使用 module_param() 宏声明。
2.4.1 参数声明与使用
#include <linux/moduleparam.h>
static int debug_level = 0;
static char *device_name = "default_dev";
static int array_param[4] = {0, 0, 0, 0};
static int array_count;
module_param(debug_level, int, 0644);
module_param(device_name, charp, 0644);
module_param_array(array_param, int, &array_count, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
MODULE_PARM_DESC(device_name, "Device name string");
MODULE_PARM_DESC(array_param, "Array of 4 integers");
2.4.2 加载时传参方式
| 命令 | 说明 |
|---|---|
insmod my_module.ko debug_level=2 | 传递整型参数 |
insmod my_module.ko device_name="mydev" | 传递字符串参数 |
insmod my_module.ko array_param=1,2,3,4 | 传递数组参数 |
modprobe my_module debug_level=2 | modprobe 自动处理依赖 |
权限位说明:
module_param 的第三个参数是 sysfs 文件权限(如 0644)。设为 0 表示不在 sysfs 中暴露该参数;非 0 值允许用户在运行时通过 /sys/module/<name>/parameters/ 查看或修改。
2.5 模块依赖与符号导出
模块之间可以存在依赖关系:一个模块可以导出符号(函数或变量)供其他模块使用。
2.5.1 符号导出机制
2.5.2 导出符号的两种方式
| 宏 | 适用范围 | 使用限制 |
|---|---|---|
EXPORT_SYMBOL(func) | 所有模块 | 无许可证限制 |
EXPORT_SYMBOL_GPL(func) | 仅 GPL 许可模块 | 非 GPL 模块无法引用 |
2.5.3 模块依赖管理
/* 模块 A:导出符号 */
#include <linux/module.h>
void shared_function(void)
{
printk(KERN_INFO "Shared function called\n");
}
EXPORT_SYMBOL(shared_function);
/* 模块 B:引用符号 */
extern void shared_function(void);
static int __init module_b_init(void)
{
shared_function(); /* 调用模块 A 导出的函数 */
return 0;
}
依赖处理命令:
depmod:生成模块依赖文件modules.depmodprobe:自动加载依赖模块(推荐使用)lsmod:查看已加载模块及其依赖关系modinfo my_module.ko:查看模块信息,包括依赖和参数
2.6 完整示例:带参数和符号导出的模块
/* calculator_module.c */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int op_count = 0;
module_param(op_count, int, 0644);
MODULE_PARM_DESC(op_count, "Number of operations to simulate");
int add_numbers(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(add_numbers);
int multiply_numbers(int a, int b)
{
return a * b;
}
EXPORT_SYMBOL_GPL(multiply_numbers);
static int __init calc_init(void)
{
int i;
printk(KERN_INFO "Calculator module loaded\n");
printk(KERN_INFO "Operation count: %d\n", op_count);
for (i = 0; i < op_count; i++) {
printk(KERN_INFO "Test %d: add(3,4)=%d, mul(2,5)=%d\n",
i+1, add_numbers(3,4), multiply_numbers(2,5));
}
return 0;
}
static void __exit calc_exit(void)
{
printk(KERN_INFO "Calculator module unloaded\n");
}
module_init(calc_init);
module_exit(calc_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Driver Expert");
MODULE_DESCRIPTION("Calculator module with parameters and symbol export");
2.7 模块编译与测试
2.7.1 Makefile 模板
obj-m := calculator_module.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
2.7.2 测试步骤
- 编译模块:
make - 加载模块(带参数):
sudo insmod calculator_module.ko op_count=3 - 查看模块信息:
lsmod | grep calculator - 查看内核日志:
dmesg | tail -10 - 查看参数:
cat /sys/module/calculator_module/parameters/op_count - 卸载模块:
sudo rmmod calculator_module
常见错误与排查:
- “Unknown symbol”:模块依赖的符号未导出或依赖模块未加载
- “Operation not permitted”:内核开启了模块签名验证,需关闭或签名
- “Invalid module format”:模块与当前内核版本不匹配
- “Module is already loaded”:模块已存在,需先卸载或使用不同名称
2.8 本章小结
| 知识点 | 核心要点 |
|---|---|
| 模块加载/卸载 | insmod/rmmod 触发 module_init/module_exit |
| 入口/出口宏 | module_init() 返回 0 成功,负值失败 |
| 模块参数 | module_param() 声明,支持 int/charp/array 等类型 |
| 符号导出 | EXPORT_SYMBOL / EXPORT_SYMBOL_GPL |
| 依赖管理 | modprobe 自动处理依赖,depmod 生成依赖文件 |
| 调试手段 | dmesg 查看日志,/sys/module/ 查看参数 |