3. 内核模块基础对比:模块加载/卸载机制变化、module_init/module_exit宏的兼容性、MODULE_LICENSE与MODULE_AUTHOR的规范
从 Linux 4.x 到 6.x,内核模块的基础框架经历了多项关键变化。虽然 module_init() 和 module_exit() 的宏接口在源码层面保持向后兼容,但底层的加载/卸载机制、模块参数传递方式、许可证检查策略以及模块元数据规范都发生了实质性调整。本章将逐一对比这些变化,帮助你在移植过程中精准定位需要修改的代码点。
3.1 模块加载/卸载机制的核心变化
内核模块的加载与卸载由 init_module() 和 delete_module() 系统调用驱动。在 4.x 到 6.x 的演进中,内核引入了更严格的模块签名验证、更细粒度的依赖跟踪以及更安全的模块参数处理。
- 模块签名强制化:4.x 中签名验证可选,6.x 中多数发行版默认启用
CONFIG_MODULE_SIG_FORCE,未签名模块加载失败。 - 模块参数类型检查:6.x 内核在加载时对
module_param()声明的参数类型进行更严格的运行时校验,不匹配时拒绝加载。 - 模块依赖自动解析:6.x 改进了
request_module()的异步加载路径,避免在卸载时出现悬空依赖。 - 模块内存布局:6.x 引入
module_memory结构体,将模块代码段、数据段、rodata 段分离管理,提升安全性。
module_param_call() 自定义参数读写函数,在 6.x 上需要检查参数类型是否与内核期望的 param_ops_* 完全匹配,否则加载时会返回 -EINVAL。
下表总结了加载/卸载机制在 4.x 与 6.x 之间的主要差异:
| 特性 | Linux 4.x | Linux 6.x | 影响范围 |
|---|---|---|---|
| 模块签名验证 | 可选(CONFIG_MODULE_SIG=y 但非强制) | 默认强制(CONFIG_MODULE_SIG_FORCE=y) | 所有第三方模块 |
| 模块参数类型校验 | 宽松,仅检查基本类型 | 严格,校验数组长度、字符串边界 | 使用 module_param_array 的模块 |
| 模块依赖跟踪 | 基于模块引用计数 | 引用计数 + 依赖图拓扑排序 | 多模块协同驱动 |
| 模块内存保护 | 代码段只读,数据段可读写 | 代码段只读 + rodata 段只读 + 数据段 NX | 所有模块 |
| 模块加载失败回滚 | 部分回滚,可能残留符号 | 原子回滚,无残留 | 所有模块 |
3.2 module_init / module_exit 宏的兼容性分析
module_init() 和 module_exit() 是内核模块的入口/出口宏,在 4.x 到 6.x 的演进中保持了源码级兼容,但底层实现和链接行为发生了变化。理解这些变化对于编写跨版本兼容的模块至关重要。
3.2.1 宏定义演变
在 4.x 内核中,module_init() 将初始化函数指针放入 .init.text 段,并在模块加载完成后释放该段内存。6.x 内核保留了这一机制,但增加了对 initcall 级别的显式控制,允许模块指定初始化优先级。
/* Linux 4.x 典型定义(简化) */
#define module_init(x) __initcall(x);
/* Linux 6.x 典型定义(简化) */
#define module_init(x) __initcall(x);
/* 注意:6.x 新增了 module_init_level() 宏用于指定优先级 */
#define module_init_level(x, level) __initcall_level(x, level);
module_exit() 中增加了对 __exit 属性的强制检查。如果出口函数未标记 __exit,编译器可能产生警告或链接错误。移植时请确保出口函数声明包含 __exit 属性。
3.2.2 多入口/出口支持
6.x 内核引入了 module_init_compat() 和 module_exit_compat() 宏,用于支持同一模块在不同内核版本上注册多个入口点。这在设备树驱动移植中尤为常见。
/* 兼容性写法示例:同时支持 4.x 和 6.x */
#include <linux/module.h>
static int __init my_driver_init(void)
{
pr_info("My driver initialized\n");
return 0;
}
static void __exit my_driver_exit(void)
{
pr_info("My driver exited\n");
}
/* 使用标准宏,4.x 和 6.x 均支持 */
module_init(my_driver_init);
module_exit(my_driver_exit);
/* 6.x 可选:指定 initcall 级别 */
#ifdef CONFIG_MODULE_INITCALL_LEVEL
module_init_level(my_driver_init, 6); /* 6 表示设备初始化级别 */
#endif
3.3 MODULE_LICENSE 与 MODULE_AUTHOR 的规范
模块元数据宏 MODULE_LICENSE() 和 MODULE_AUTHOR() 在 4.x 到 6.x 的演进中,许可证字符串的解析规则和作者信息的格式要求变得更加严格。
3.3.1 MODULE_LICENSE 合法值对比
| 许可证字符串 | 4.x 行为 | 6.x 行为 | 说明 |
|---|---|---|---|
"GPL" |
接受,视为 GPL v2 | 接受,视为 GPL v2 | 最常用,兼容性最好 |
"GPL v2" |
接受 | 接受 | 与 "GPL" 等价 |
"GPL and additional rights" |
接受 | 接受,但触发内核 taint | 6.x 会标记模块为“非纯净 GPL” |
"Dual BSD/GPL" |
接受 | 接受 | 双许可证,无 taint |
"Proprietary" |
接受,触发 taint | 接受,触发 taint + 限制 EXPORT_SYMBOL_GPL | 6.x 更严格限制专有模块访问 GPL 符号 |
空字符串或未定义 |
警告,默认视为专有 | 加载失败,返回 -EPERM | 6.x 强制要求显式声明许可证 |
MODULE_LICENSE("GPL") 或 MODULE_LICENSE("GPL v2") 以确保最大兼容性。如果你的模块包含非 GPL 兼容代码,请使用 "Dual BSD/GPL" 并确保所有导出的符号使用 EXPORT_SYMBOL 而非 EXPORT_SYMBOL_GPL。
3.3.2 MODULE_AUTHOR 格式规范
6.x 内核引入了对 MODULE_AUTHOR() 字符串的格式检查。虽然不会导致加载失败,但不符合规范的作者信息会被内核日志标记为“malformed”。
/* 推荐的 MODULE_AUTHOR 格式(6.x 兼容) */
MODULE_AUTHOR("Zhang San <zhangsan@example.com>");
/* 不推荐的格式(6.x 可能产生警告) */
MODULE_AUTHOR("Zhang San"); /* 缺少邮箱 */
MODULE_AUTHOR("zhangsan@example.com"); /* 缺少姓名 */
/* 多作者写法(4.x 和 6.x 均支持) */
MODULE_AUTHOR("Zhang San <zhangsan@example.com>");
MODULE_AUTHOR("Li Si <lisi@example.com>");
3.4 模块元数据完整示例
以下是一个同时兼容 4.x 和 6.x 内核的模块元数据声明模板,涵盖了许可证、作者、描述、参数和版本信息:
/*
* 兼容 4.x 和 6.x 内核的模块元数据示例
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
/* 模块参数 */
static int debug_level = 0;
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0=off, 1=info, 2=debug)");
/* 模块元数据 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Zhang San <zhangsan@example.com>");
MODULE_DESCRIPTION("Sample driver for 4.x to 6.x migration training");
MODULE_VERSION("1.0.0");
MODULE_ALIAS("platform:my-sample-device");
/* 初始化与退出函数 */
static int __init sample_init(void)
{
pr_info("Sample driver loaded (debug_level=%d)\n", debug_level);
return 0;
}
static void __exit sample_exit(void)
{
pr_info("Sample driver unloaded\n");
}
module_init(sample_init);
module_exit(sample_exit);
- 确认
MODULE_LICENSE不为空,且使用内核已知的字符串。 - 确认
MODULE_AUTHOR包含姓名和邮箱,格式为"Name <email>"。 - 确认出口函数使用
__exit属性。 - 确认模块参数类型与内核期望的
param_ops_*匹配。 - 如果模块依赖其他模块,使用
MODULE_SOFTDEP()声明软依赖。
3.5 本章小结
从 4.x 到 6.x 的内核模块基础变化主要体现在三个方面:
- 加载/卸载机制:签名验证从可选变为强制,参数校验更加严格,内存保护增强,卸载回滚机制原子化。
- module_init/module_exit:宏接口保持向后兼容,但 6.x 增加了
__exit属性检查和initcall级别支持,多入口场景需使用兼容宏。 - MODULE_LICENSE 与 MODULE_AUTHOR:许可证字符串解析规则收紧,空字符串导致加载失败;作者信息格式规范化,建议包含姓名和邮箱。