4. 内核模块基础:module_init/module_exit、MODULE_LICENSE、MODULE_AUTHOR、MODULE_DESCRIPTION 的版本兼容性
内核模块是 Linux 驱动开发的基本载体。每个模块都依赖一组固定的宏和函数来定义入口、出口、许可证和元信息。这些 API 虽然看似稳定,但在不同内核版本间仍存在细微差异。本章从版本兼容性角度,系统梳理 module_init/module_exit、MODULE_LICENSE、MODULE_AUTHOR、MODULE_DESCRIPTION 的演进历史、适配要点和常见陷阱。
4.1 模块入口与出口:module_init / module_exit
module_init 和 module_exit 是模块加载和卸载时内核调用的入口函数。其核心语义在 2.4 到 6.x 内核中保持一致,但底层实现和辅助宏经历了多次重构。
4.1.1 基本用法与版本差异
| 内核版本 | module_init 行为 | module_exit 行为 | 关键变化 |
|---|---|---|---|
| 2.4.x | 直接调用 init_module() | 直接调用 cleanup_module() | 无参数,函数名固定 |
| 2.6.x ~ 3.x | 宏展开为 __initcall | 宏展开为 __exitcall | 引入 __init/__exit 节属性 |
| 4.x ~ 5.x | 使用 gcc 的 constructor 属性 | 使用 gcc 的 destructor 属性 | 支持模块参数、异步 probe |
| 6.x | 兼容旧宏,推荐 module_init() | 兼容旧宏,推荐 module_exit() | 无重大变化,保持稳定 |
module_init 和 module_exit 必须配合 __init 和 __exit 属性使用。这些属性将函数放入特定的 ELF 节,在模块加载/卸载后释放内存。如果遗漏 __exit,编译时可能产生警告,但不会影响功能。
// 标准写法(2.6+ 内核通用)
static int __init my_driver_init(void)
{
pr_info("My driver loaded\n");
return 0;
}
static void __exit my_driver_exit(void)
{
pr_info("My driver unloaded\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
4.1.2 返回值与错误处理兼容性
module_init 函数必须返回 int。返回 0 表示成功,负值表示错误。内核在 2.6.10 之后增加了更严格的错误码检查,如果返回非零值,模块加载会被中止,且内核日志会打印错误码。在 2.4 内核中,部分错误码可能被忽略。
module_init 函数如果返回 -EPROBE_DEFER,内核不会自动重试。从 3.8 开始,驱动模型支持 probe deferral,但 module_init 本身仍不重试。如果需要延迟加载,应使用 probe 回调而非 module_init。
4.1.3 异步初始化支持
从 4.1 内核开始,引入了异步 probe 机制。虽然 module_init 本身仍是同步调用,但驱动可以在 init 函数中注册异步 probe 请求。在 5.7 内核中,新增了 module_driver 辅助宏,简化了标准驱动的 init/exit 注册流程。
// 使用 module_driver 宏(5.7+ 内核推荐)
static struct platform_driver my_driver = {
.driver = {
.name = "my_device",
},
.probe = my_probe,
.remove = my_remove,
};
module_driver(my_driver, platform_driver_register, platform_driver_unregister);
module_platform_driver、module_i2c_driver 等专用宏。这些宏在 3.x 内核中逐步引入,能自动生成标准的 module_init/module_exit 代码,减少手动编写错误。
4.2 模块元信息宏:许可证、作者与描述
MODULE_LICENSE、MODULE_AUTHOR、MODULE_DESCRIPTION 是模块的元信息声明。它们不产生执行代码,而是将信息写入模块的 ELF 节中,供 modinfo 命令和内核加载器读取。
4.2.1 MODULE_LICENSE 的版本兼容性
| 许可证字符串 | 含义 | 内核版本支持 | 对符号导出的影响 |
|---|---|---|---|
| "GPL" | GNU General Public License v2 | 2.4+ | 可访问所有 EXPORT_SYMBOL_GPL |
| "GPL v2" | GPL v2 专用 | 2.6+ | 同 GPL |
| "GPL and additional rights" | GPL 附加额外权利 | 2.6+ | 同 GPL |
| "Dual BSD/GPL" | 双许可证 BSD 或 GPL | 2.6+ | 同 GPL |
| "Dual MPL/GPL" | 双许可证 MPL 或 GPL | 2.6+ | 同 GPL |
| "Proprietary" | 专有许可证 | 2.4+ | 不能访问 EXPORT_SYMBOL_GPL |
EXPORT_SYMBOL_GPL 机制。如果模块声明为 "Proprietary",则无法调用任何标记为 EXPORT_SYMBOL_GPL 的内核函数。在 2.4 内核中,所有符号都是公开的,许可证不影响符号访问。
// 许可证声明示例
MODULE_LICENSE("GPL"); // 最常用,兼容所有版本
MODULE_LICENSE("Dual BSD/GPL"); // 双许可证
MODULE_LICENSE("Proprietary"); // 专有,限制 GPL 符号访问
4.2.2 MODULE_AUTHOR 与 MODULE_DESCRIPTION
这两个宏在所有 2.4+ 内核中保持稳定,没有版本兼容性问题。它们接受字符串参数,用于描述模块的作者和功能。
MODULE_AUTHOR("Zhang San <zhangsan@example.com>");
MODULE_DESCRIPTION("PCIe DMA engine driver for XYZ hardware");
MODULE_AUTHOR 中包含邮箱地址,方便用户报告问题。MODULE_DESCRIPTION 应简洁描述模块功能,不超过 80 个字符,因为 modinfo 输出时可能截断。
4.3 模块元信息与内核版本适配关系图
下图展示了模块元信息宏与内核版本之间的适配关系,以及不同许可证对符号访问的影响路径。
4.4 版本适配策略与常见陷阱
4.4.1 跨版本兼容性检查清单
- 2.4 内核:使用
init_module()和cleanup_module()函数名,而非宏。MODULE_LICENSE 不影响符号访问。 - 2.6 ~ 3.x 内核:必须使用
module_init/module_exit宏,配合__init/__exit属性。MODULE_LICENSE 开始影响 EXPORT_SYMBOL_GPL 访问。 - 4.x ~ 5.x 内核:引入异步 probe 和
module_driver辅助宏。推荐使用专用宏简化代码。 - 6.x 内核:保持向后兼容,但废弃了部分旧 API。建议使用最新辅助宏。
4.4.2 常见陷阱与解决方案
| 陷阱 | 现象 | 原因 | 解决方案 |
|---|---|---|---|
| init 函数未加 __init | 编译警告,内存未释放 | 遗漏节属性 | 添加 __init 关键字 |
| exit 函数未加 __exit | 编译警告,卸载时可能异常 | 遗漏节属性 | 添加 __exit 关键字 |
| MODULE_LICENSE 拼写错误 | modinfo 显示 unknown | 字符串不匹配 | 使用标准字符串 |
| 专有模块调用 GPL 符号 | 编译通过,加载失败 | 符号不可访问 | 改用 GPL 许可证或联系厂商 |
| module_init 返回正数 | 模块加载成功但状态异常 | 错误码约定 | 返回 0 或负错误码 |
4.4.3 条件编译适配示例
当需要同时支持 2.4 和 2.6+ 内核时,可以使用条件编译:
#include <linux/version.h>
#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,0)
/* 2.4 内核风格 */
int init_module(void)
{
pr_info("Module loaded (2.4 style)\n");
return 0;
}
void cleanup_module(void)
{
pr_info("Module unloaded (2.4 style)\n");
}
#else
/* 2.6+ 内核风格 */
static int __init my_init(void)
{
pr_info("Module loaded (2.6+ style)\n");
return 0;
}
static void __exit my_exit(void)
{
pr_info("Module unloaded (2.6+ style)\n");
}
module_init(my_init);
module_exit(my_exit);
#endif
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Developer <dev@example.com>");
MODULE_DESCRIPTION("Cross-version compatible module example");
LINUX_VERSION_CODE 的比较方式发生了变化。建议使用 KERNEL_VERSION 宏进行版本比较,避免直接比较整数。同时,对于新驱动开发,不再需要支持 2.4 内核,应直接使用 2.6+ 风格。
4.5 本章小结
module_init/module_exit从 2.6 内核开始标准化,配合__init/__exit属性使用。5.7+ 内核推荐使用module_driver系列辅助宏。MODULE_LICENSE在 2.6+ 内核中影响EXPORT_SYMBOL_GPL符号的访问权限,专有许可证模块无法调用 GPL 专用符号。MODULE_AUTHOR和MODULE_DESCRIPTION在所有内核版本中保持完全兼容,没有版本差异。- 跨版本适配时,使用
LINUX_VERSION_CODE和KERNEL_VERSION宏进行条件编译,但新驱动应直接面向当前主线内核开发。