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() 无重大变化,保持稳定
适配要点:从 2.6 内核开始,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 内核中,部分错误码可能被忽略。

版本陷阱:在 3.8 内核之前,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
关键规则:从 2.6.0 开始,内核引入了 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 模块元信息与内核版本适配关系图

下图展示了模块元信息宏与内核版本之间的适配关系,以及不同许可证对符号访问的影响路径。

模块元信息宏与内核版本适配关系 模块源文件 MODULE_LICENSE MODULE_AUTHOR MODULE_DESCRIPTION "GPL" 系列 "Proprietary" 可访问 GPL 符号 限制 GPL 符号 所有版本兼容 所有版本兼容 编译生成 .ko 模块文件 版本适配要点 MODULE_LICENSE 影响符号访问权限(2.6+ 内核),MODULE_AUTHOR 和 MODULE_DESCRIPTION 全版本兼容

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");
重要提示:从 5.4 内核开始,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 宏进行条件编译,但新驱动应直接面向当前主线内核开发。