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 命令到内核完成模块生命周期管理,中间经历多个内核子系统协作。下图展示了完整的加载与卸载流程:

模块加载/卸载内核流程 insmod / rmmod 系统调用层 (sys_init_module / sys_delete_module) 加载:模块依赖解析 加载:ELF 段解析与重定位 加载:调用 module_init() 卸载:模块引用计数检查 卸载:调用 module_exit() 卸载:释放模块内存 返回状态给用户空间 (0 或错误码) 说明 加载流程中,如果 module_init() 返回非 0 值,内核会自动回滚已分配的资源并卸载模块;卸载流程中,如果引用计数不为 0,rmmod 会返回 EBUSY 错误。

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 的注册机制、许可证声明对符号访问的影响、以及加载/卸载流程中的内核行为。一个规范的模块骨架应包含完整的错误处理、许可证声明和元信息描述,这不仅是编码规范的要求,更是驱动兼容性测试和长期维护的基础。