3. 内核模块编程:模块加载/卸载、module_init/module_exit、许可证声明
内核模块是 Linux 内核在运行时动态扩展功能的机制,是网关外设驱动移植中最基础的编程单元。与用户态程序不同,内核模块运行在内核空间,拥有最高特权级,其生命周期由加载、初始化和卸载三个阶段构成。理解模块的入口/出口函数、加载/卸载流程以及许可证声明规则,是编写安全、兼容的驱动代码的前提。
3.1 模块基本结构
一个标准的内核模块至少包含以下四个要素:
- 头文件包含:
<linux/module.h>(模块核心 API)和<linux/init.h>>(初始化/退出宏)。 - 入口函数:通过
module_init()宏注册,模块加载时自动调用。 - 出口函数:通过
module_exit()宏注册,模块卸载时自动调用。 - 许可证声明:使用
MODULE_LICENSE()宏声明许可证类型,影响内核符号导出与法律合规。
3.2 module_init / module_exit 宏与函数签名
内核通过 module_init() 和 module_exit() 宏将开发者定义的函数注册到模块生命周期管理表中。这两个宏在编译时会将函数指针放入特定的 ELF 段(.init.text 和 .exit.text),由内核模块加载器在适当阶段调用。
| 宏 | 注册函数 | 返回值类型 | 调用时机 | 典型行为 |
|---|---|---|---|---|
module_init(x) |
int __init x(void) |
int(0 成功,负值失败) |
模块加载时,insmod/modprobe 后 | 注册设备、分配内存、初始化锁 |
module_exit(y) |
void __exit y(void) |
void |
模块卸载时,rmmod 后 | 注销设备、释放内存、销毁锁 |
__init 标记,出口函数使用 __exit 标记。这些标记告诉内核:入口函数在初始化完成后可以释放其占用的内存(如果内核支持 CONFIG_MODULE_FORCE_UNLOAD 等特性),出口函数在非模块化编译时可以被丢弃。
3.3 许可证声明:MODULE_LICENSE 与符号导出
Linux 内核使用 MODULE_LICENSE() 宏声明模块的许可证类型。该声明不仅影响法律合规性,还直接影响模块能否使用内核导出的 EXPORT_SYMBOL_GPL 符号。如果模块声明了非 GPL 兼容许可证,则无法链接 GPL-only 的内核符号。
| 许可证字符串 | 含义 | GPL-only 符号访问 | 常见场景 |
|---|---|---|---|
"GPL" |
GNU General Public License v2 | 允许 | 开源驱动、主线内核贡献 |
"GPL v2" |
GPL v2 明确版本 | 允许 | 严格遵循 GPLv2 的项目 |
"Dual BSD/GPL" |
BSD 或 GPL 双许可 | 允许 | 兼容性驱动、学术代码 |
"Proprietary" |
专有许可证 | 禁止 | 闭源商业驱动 |
MODULE_LICENSE(),内核默认视为“专有”,但会打印警告。更严重的是,如果驱动使用了 EXPORT_SYMBOL_GPL 导出的内核函数(如 class_create、device_create 的某些变体),而模块声明了非 GPL 许可证,则链接阶段会失败,模块无法加载。网关外设驱动移植时,务必确认目标内核中关键 API 的导出符号类型。
3.4 模块加载/卸载流程与内核事件
从用户执行 insmod 或 rmmod 命令到内核完成模块生命周期管理,中间经历多个内核子系统协作。下图展示了完整的加载与卸载流程:
3.5 完整模块代码示例
以下是一个符合内核编码规范的网关外设驱动模块骨架,包含入口/出口函数、许可证声明以及模块信息描述:
// gateway_uart_drv.c — 网关 UART 外设驱动模块骨架
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
static int __init gateway_uart_init(void)
{
pr_info("Gateway UART driver: module loaded.\n");
/* TODO: 注册 platform 设备、分配 I/O 内存、初始化串口硬件 */
/* 返回 0 表示初始化成功;返回负值表示失败 */
return 0;
}
static void __exit gateway_uart_exit(void)
{
pr_info("Gateway UART driver: module unloaded.\n");
/* TODO: 注销设备、释放 I/O 内存、关闭硬件 */
}
module_init(gateway_uart_init);
module_exit(gateway_uart_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Gateway Driver Team <dev@gateway.com>");
MODULE_DESCRIPTION("Gateway UART peripheral driver for external device compatibility");
MODULE_VERSION("1.0.0");
MODULE_ALIAS("platform:gateway-uart");
3.6 模块信息与辅助宏
除了 MODULE_LICENSE,内核还提供一系列辅助宏用于描述模块元数据。这些信息可通过 modinfo 命令查看,对驱动兼容性测试和版本管理非常有用。
| 宏 | 参数 | 用途 |
|---|---|---|
MODULE_AUTHOR |
字符串 | 声明模块作者及联系方式 |
MODULE_DESCRIPTION |
字符串 | 简要描述模块功能 |
MODULE_VERSION |
字符串 | 模块版本号,用于兼容性匹配 |
MODULE_ALIAS |
字符串 | 为模块提供别名,便于 modprobe 自动查找 |
MODULE_DEVICE_TABLE |
设备表指针 | 导出设备 ID 表,用于热插拔匹配 |
3.7 加载/卸载测试与常见错误排查
在网关外设驱动移植过程中,模块加载/卸载测试是最基本的验证步骤。以下列出典型错误及其根因:
- insmod: ERROR: could not insert module: Invalid parameters — 通常由
module_init()返回负值引起,需检查入口函数中的资源分配逻辑。 - rmmod: ERROR: Module gateway_uart is in use — 模块引用计数不为 0,可能有用户态进程打开了设备文件,或内核中有其他模块依赖该模块。
- modprobe: FATAL: Module gateway_uart not found in directory — 模块未安装到标准路径,或
modules.dep未更新。 - kernel: BUG: unable to handle kernel NULL pointer dereference — 入口函数中访问了未初始化的指针,常见于 ioremap 失败后未做错误处理。
insmod 手动加载并观察 dmesg 输出,确认入口函数无错误后再使用 modprobe 进行依赖加载。卸载前使用 lsmod 确认引用计数为 0,避免强制卸载导致内核 panic。
3.8 本章小结
内核模块编程是网关外设驱动移植的基石。开发者需要掌握 module_init/module_exit 的注册机制、许可证声明对符号访问的影响、以及加载/卸载流程中的内核行为。一个规范的模块骨架应包含完整的错误处理、许可证声明和元信息描述,这不仅是编码规范的要求,更是驱动兼容性测试和长期维护的基础。