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 段分离管理,提升安全性。
移植提示:如果你的模块在 4.x 上使用 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 所有模块
模块加载失败回滚 部分回滚,可能残留符号 原子回滚,无残留 所有模块
模块加载/卸载流程对比:4.x vs 6.x Linux 4.x 加载流程 用户态 insmod init_module() 系统调用 签名验证(可选) 执行 module_init() Linux 6.x 加载流程 用户态 modprobe init_module() 系统调用 签名验证(强制) 参数类型严格校验 执行 module_init() 4.x: 引用计数减1 6.x: 依赖图检查 + 原子回滚

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);
兼容性陷阱:虽然宏名称未变,但 6.x 内核在 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 强制要求显式声明许可证
最佳实践:在 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>");
MODULE_LICENSE 检查流程:4.x vs 6.x Linux 4.x 读取 MODULE_LICENSE 字符串匹配检查 未知字符串 → 默认专有 设置 taint 标志 Linux 6.x 读取 MODULE_LICENSE 空字符串 → 加载失败 已知字符串 → 正常加载 非 GPL → 限制符号访问

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 的内核模块基础变化主要体现在三个方面:

  1. 加载/卸载机制:签名验证从可选变为强制,参数校验更加严格,内存保护增强,卸载回滚机制原子化。
  2. module_init/module_exit:宏接口保持向后兼容,但 6.x 增加了 __exit 属性检查和 initcall 级别支持,多入口场景需使用兼容宏。
  3. MODULE_LICENSE 与 MODULE_AUTHOR:许可证字符串解析规则收紧,空字符串导致加载失败;作者信息格式规范化,建议包含姓名和邮箱。