1. Linux 内核模块基础:模块结构、加载与卸载、模块参数、模块依赖与符号导出
Linux 内核模块是动态加载到内核中的代码片段,它允许在不重新编译整个内核的情况下扩展功能。对于网络设备驱动开发而言,模块是驱动的基本载体。本章将深入剖析模块的骨架结构、生命周期管理、参数传递机制以及模块间的依赖与符号导出规则。
1.1 模块的基本结构
一个标准的内核模块至少包含两个核心宏:module_init() 和 module_exit(),它们分别指定模块加载和卸载时的入口函数。此外,模块还需要通过 MODULE_LICENSE() 声明许可证,通过 MODULE_AUTHOR() 和 MODULE_DESCRIPTION() 提供元信息。
MODULE_LICENSE("GPL") 会导致内核拒绝导出某些符号,且模块加载时会产生“tainted kernel”警告。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init my_driver_init(void)
{
printk(KERN_INFO "My network driver loaded.\n");
return 0; /* 成功返回 0 */
}
static void __exit my_driver_exit(void)
{
printk(KERN_INFO "My network driver unloaded.\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Expert");
MODULE_DESCRIPTION("Sample network driver module");
下表总结了模块结构中的关键元素及其作用:
| 元素 | 宏/函数 | 说明 |
|---|---|---|
| 加载入口 | module_init() |
指定模块加载时调用的初始化函数 |
| 卸载出口 | module_exit() |
指定模块卸载时调用的清理函数 |
| 许可证声明 | MODULE_LICENSE() |
必须声明,否则内核标记为“污染” |
| 作者信息 | MODULE_AUTHOR() |
可选,用于标识驱动作者 |
| 描述信息 | MODULE_DESCRIPTION() |
可选,简要描述模块功能 |
1.2 模块加载与卸载
模块的加载与卸载由用户空间工具 insmod、rmmod 和 modprobe 触发,内核则通过系统调用 init_module() 和 delete_module() 完成实际工作。加载流程如下:
- 文件读取: 内核将 .ko 文件从用户空间复制到内核空间。
- 符号解析: 解析模块中引用的外部符号,并与内核符号表或已加载模块的导出符号进行链接。
- 调用 init: 执行
module_init()指定的函数。若返回非 0 值,则加载失败并回滚。 - 注册设备: 在 init 函数中通常完成 PCI 设备注册、网络设备分配等操作。
insmod 加载模块后,可通过 lsmod 查看已加载模块列表,通过 dmesg 查看内核打印的日志信息。
卸载流程则相反:先调用 module_exit() 清理资源(如注销设备、释放内存),然后从内核中移除模块代码。
1.3 模块参数
模块参数允许用户在加载模块时传递配置值,无需重新编译。使用 module_param() 宏声明参数,支持基本类型(int、charp、bool 等)以及数组。
#include <linux/moduleparam.h>
static int irq_num = 0;
static char *dev_name = "eth0";
static int debug_level = 1;
module_param(irq_num, int, 0644);
module_param(dev_name, charp, 0644);
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(irq_num, "IRQ number for the device");
MODULE_PARM_DESC(dev_name, "Network device name");
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
加载时通过 insmod 传递参数:
sudo insmod my_driver.ko irq_num=17 dev_name="eth1" debug_level=2
参数权限位(如 0644)控制 sysfs 中对应文件的访问权限,允许运行时查看或修改参数值。
| 参数类型 | 宏 | 示例 | sysfs 文件类型 |
|---|---|---|---|
| 整型 | module_param(name, int, perm) |
irq_num |
可读写整数 |
| 字符串 | module_param(name, charp, perm) |
dev_name |
可读写字符串 |
| 布尔 | module_param(name, bool, perm) |
enable_rx |
可读写 0/1 |
| 数组 | module_param_array(name, type, num, perm) |
irq_list |
逗号分隔列表 |
1.4 模块依赖与符号导出
模块之间可以存在依赖关系。例如,一个网络驱动模块可能依赖于一个公共的硬件抽象层模块。依赖关系通过 EXPORT_SYMBOL() 和 EXPORT_SYMBOL_GPL() 宏建立。
符号导出规则:
EXPORT_SYMBOL(func):将函数或变量导出到所有模块,无论其许可证类型。EXPORT_SYMBOL_GPL(func):仅导出给声明了 GPL 许可证的模块。- 未导出的符号对其他模块不可见,链接时会报“Unknown symbol”错误。
/* 在公共模块中导出函数 */
#include <linux/module.h>
int shared_reg_read(unsigned int reg)
{
/* 硬件寄存器读取实现 */
return 0;
}
EXPORT_SYMBOL(shared_reg_read);
/* 在依赖模块中声明并使用 */
extern int shared_reg_read(unsigned int reg);
static int __init dependent_init(void)
{
int val = shared_reg_read(0x100);
printk("Register value: %d\n", val);
return 0;
}
依赖关系由 modprobe 工具自动处理,它读取 /lib/modules/$(uname -r)/modules.dep 文件,按顺序加载所有依赖模块。