4. 内核模块编程基础(3.x vs 5.x)

内核模块是嵌入式Linux驱动开发的基本单元。从3.x到5.x版本,内核模块的加载/卸载机制、许可证声明和参数传递接口经历了显著变化。本章聚焦module_init/module_exit宏、许可证声明和模块参数传递三个核心点,给出兼容性写法,帮助你在不同内核版本间平滑迁移。

4.1 module_init/module_exit宏的变化

在3.x内核中,module_init()和module_exit()宏直接展开为__initcall()和__exitcall(),函数指针被放入特定的ELF段。5.x内核引入了initcall_levels机制,宏内部增加了优先级控制和模块状态跟踪,但对外接口保持兼容。

对比项 3.x内核 5.x内核
宏展开方式 __initcall(fn) __initcall(fn) + 优先级修饰
段属性 .init.text .init.text + .init.data 分离
返回值处理 直接返回int 支持module_init()返回__init属性
兼容性 基础写法 完全向后兼容
关键点:5.x内核中,module_init()和module_exit()的函数签名未变——仍为int __init init(void)和void __exit exit(void)。但宏内部增加了__used和__section属性,防止编译器优化掉未引用的函数。

兼容性写法示例:

// 适用于3.x ~ 5.x的通用模块入口/出口
#include <linux/module.h>
#include <linux/kernel.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");
}

module_init(my_driver_init);
module_exit(my_driver_exit);
移植建议:在3.x内核中,如果模块初始化失败,需要手动调用module_put()释放引用计数;5.x内核的module_init()宏会自动处理引用计数,但不要依赖此行为,始终在init函数中显式管理资源。
module_init/module_exit 宏展开流程对比 3.x 内核 module_init(fn) __initcall(fn) .init.text 段 5.x 内核 module_init(fn) __initcall(fn) + 优先级 .init.text + .init.data 向后兼容 说明:5.x 内核在宏展开时增加了优先级修饰和段属性分离 但函数签名和调用方式完全兼容 3.x 内核

4.2 许可证声明的兼容性写法

内核模块必须声明许可证,否则内核会标记为“污染”(tainted)。3.x和5.x内核都支持MODULE_LICENSE()宏,但5.x内核增加了许可证白名单和自动导出符号限制。

支持的许可证字符串:

  • "GPL" — 通用公共许可证 v2
  • "GPL v2" — 同上,显式版本
  • "GPL and additional rights" — GPL + 附加权利
  • "Dual BSD/GPL" — BSD 或 GPL 双许可
  • "Dual MPL/GPL" — MPL 或 GPL 双许可
  • "Proprietary" — 专有许可证(会污染内核)
注意:在5.x内核中,如果模块声明为"Proprietary",内核将禁止导出GPL-only符号(如EXPORT_SYMBOL_GPL导出的函数)。3.x内核虽然也有此机制,但5.x内核的检查更严格,可能导致专有模块无法调用某些核心API。

推荐写法:

// 兼容3.x ~ 5.x的许可证声明
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Your Name <email@example.com>");
MODULE_DESCRIPTION("A sample driver for embedded Linux");
MODULE_VERSION("1.0");

4.3 模块参数传递的兼容性写法

模块参数允许在加载时传递配置值。3.x内核使用module_param()宏,5.x内核增加了module_param_named()和module_param_array()的增强版本,但基础接口完全兼容。

宏 3.x 支持 5.x 支持 说明
module_param(name, type, perm) ✓ ✓ 基本参数,类型包括 byte, short, ushort, int, uint, long, ulong, charp, bool
module_param_named(name, var, type, perm) ✓ ✓ 参数名与变量名不同时使用
module_param_array(name, type, num, perm) ✓ ✓ 数组参数,3.x中num为指针,5.x中支持const修饰
module_param_cb() ✗ ✓ 带回调的参数,5.0+新增

兼容性示例:

// 适用于3.x ~ 5.x的模块参数传递
#include <linux/module.h>
#include <linux/moduleparam.h>

static int debug_level = 0;
static char *device_name = "default_dev";
static int array_param[4];
static int array_count;

module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");

module_param_named(name, device_name, charp, 0444);
MODULE_PARM_DESC(name, "Device name string");

module_param_array(array_param, int, &array_count, 0444);
MODULE_PARM_DESC(array_param, "An array of up to 4 integers");

static int __init param_demo_init(void)
{
    int i;
    pr_info("debug_level = %d\n", debug_level);
    pr_info("device_name = %s\n", device_name);
    for (i = 0; i < array_count; i++)
        pr_info("array_param[%d] = %d\n", i, array_param[i]);
    return 0;
}

static void __exit param_demo_exit(void)
{
    pr_info("Module unloaded\n");
}

module_init(param_demo_init);
module_exit(param_demo_exit);

MODULE_LICENSE("GPL v2");
移植要点:在3.x内核中,module_param_array()的num参数必须是指向int的指针;5.x内核允许const int *,但为了兼容性,始终使用int *。另外,perm参数在3.x中支持S_IRUGO等宏,5.x中推荐使用八进制数(如0644)。
模块参数传递流程(3.x / 5.x 兼容) insmod module.ko 内核参数解析 3.x: 直接写入变量 5.x: 权限检查 + 回调 module_init 使用参数

4.4 综合对比与移植清单

下表总结了从3.x到5.x内核模块编程的关键变化点,供移植时逐项检查:

模块元素 3.x 写法 5.x 写法 兼容性建议
入口/出口宏 module_init(fn) module_init(fn)(增加优先级) 无需修改,保持原样
许可证声明 MODULE_LICENSE("GPL") MODULE_LICENSE("GPL v2")(推荐) 建议升级到"GPL v2"
参数权限 S_IRUGO 宏 0444 八进制 使用八进制数兼容两版本
数组参数 module_param_array() module_param_array()(增强) 保持int *类型
模块状态 MODULE_STATE 枚举 MODULE_STATE 增加MODULE_STATE_UNFORMED 检查状态枚举值
移植黄金法则:在3.x内核上开发的模块,迁移到5.x内核时,90%的代码无需修改。主要关注点在于:许可证字符串的规范化、参数权限宏的替换、以及数组参数指针类型的显式声明。使用pr_info()替代printk()也是推荐做法,但非强制。