2. Linux内核模块编程基础:模块初始化/退出函数、module_init/module_exit宏、模块参数与许可证

Linux内核模块是动态加载到内核中的代码片段,无需重新编译整个内核即可扩展功能。理解模块编程基础是编写设备驱动的第一步。本节将深入剖析模块的生命周期、关键宏定义、参数传递机制以及许可证声明规范。

2.1 模块生命周期与核心函数

每个内核模块都有明确的加载和卸载阶段,分别由初始化函数和退出函数处理。这两个函数通过特定的宏注册到内核模块框架中。

关键概念:模块初始化函数在模块加载时执行一次,用于分配资源、注册设备、初始化数据结构;模块退出函数在模块卸载时执行,负责释放资源、注销设备、清理现场。

2.1.1 模块初始化函数

初始化函数是模块的入口点,通常命名为 xxx_init 或 xxx_module_init。函数原型必须返回 int 类型,成功返回 0,失败返回负的错误码。

static int __init my_driver_init(void)
{
    printk(KERN_INFO "My driver loaded successfully.\n");
    /* 在此处进行硬件初始化、注册设备等操作 */
    return 0;
}

关键字 __init 是一个编译器属性宏,告诉内核该函数仅在初始化阶段使用,初始化完成后可以释放其占用的内存。

2.1.2 模块退出函数

退出函数负责撤销初始化函数所做的一切。函数原型返回 void,通常命名为 xxx_exit 或 xxx_module_exit。

static void __exit my_driver_exit(void)
{
    printk(KERN_INFO "My driver unloaded.\n");
    /* 在此处释放资源、注销设备等操作 */
}

关键字 __exit 与 __init 对应,如果模块被编译进内核(非动态加载),该函数会被丢弃以节省内存。

2.1.3 module_init / module_exit 宏

这两个宏是模块与内核模块加载器之间的桥梁。它们将初始化函数和退出函数的地址注册到内核的模块表中。

module_init(my_driver_init);
module_exit(my_driver_exit);
编程规范:module_init 和 module_exit 必须放在源文件的末尾,紧跟在所有函数定义之后。它们不接收参数,只接收函数名。

下表总结了初始化与退出函数的对比:

特性 初始化函数 退出函数
执行时机 模块加载时(insmod) 模块卸载时(rmmod)
返回值 int(0成功,负值失败) void
关键字 __init __exit
注册宏 module_init() module_exit()
主要职责 资源分配、设备注册 资源释放、设备注销

2.2 模块加载/卸载流程 SVG 图

下图展示了从用户空间执行 insmod/rmmod 命令到内核模块初始化/退出函数被调用的完整流程:

模块加载与卸载流程 用户空间 insmod my_module.ko rmmod my_module 内核空间 系统调用层 模块加载器 module_init() module_exit() 硬件初始化/释放 流程说明 1. insmod 触发系统调用 2. 模块加载器解析 ELF 3. 调用 module_init 函数 4. rmmod 触发系统调用 5. 模块加载器检查引用计数 6. 调用 module_exit 函数

2.3 模块参数

模块参数允许用户在加载模块时传递配置值,无需重新编译。通过 module_param 宏声明参数,用户可以在 insmod 命令行中指定。

2.3.1 参数声明宏

#include <linux/moduleparam.h>

static int irq_num = 10;
static char *device_name = "my_i2c_dev";

module_param(irq_num, int, 0644);
module_param(device_name, charp, 0644);

参数说明:

  • 第一个参数:变量名,必须与前面定义的全局变量一致。
  • 第二个参数:数据类型,支持 int、long、charp(字符串指针)、bool 等。
  • 第三个参数:文件权限掩码,用于 /sys/module/<module>/parameters/ 下的 sysfs 文件权限。0 表示不创建 sysfs 条目。

2.3.2 加载时传递参数

# insmod my_i2c_driver.ko irq_num=33 device_name="i2c-1"
注意:字符串参数 charp 传递时,内核会复制字符串指针,确保字符串在模块生命周期内有效。建议使用静态字符串或 kstrdup 分配。

2.3.3 参数描述宏

使用 MODULE_PARM_DESC 为参数添加描述信息,便于用户查看:

MODULE_PARM_DESC(irq_num, "IRQ number for the device");
MODULE_PARM_DESC(device_name, "Device name string");

2.4 模块许可证与作者声明

Linux内核要求所有模块声明许可证,以维护GPL协议的合规性。未声明或使用非GPL兼容许可证的模块将无法使用某些内核导出的符号(EXPORT_SYMBOL_GPL)。

2.4.1 常用许可证宏

宏 许可证类型 说明
MODULE_LICENSE("GPL") GNU General Public License v2 最常用,可访问所有GPL导出的符号
MODULE_LICENSE("GPL v2") GPL v2 与"GPL"等价
MODULE_LICENSE("GPL and additional rights") GPL + 附加权利 允许额外条款
MODULE_LICENSE("Dual BSD/GPL") BSD 或 GPL 双许可证
MODULE_LICENSE("Proprietary") 专有许可证 无法使用GPL导出的符号

2.4.2 其他元信息宏

MODULE_AUTHOR("Your Name <email@example.com>");
MODULE_DESCRIPTION("I2C device driver for sensor XYZ");
MODULE_VERSION("1.0");
MODULE_ALIAS("i2c:my_sensor");
最佳实践:始终使用 MODULE_LICENSE("GPL") 除非你有明确的商业许可需求。许多内核核心功能(如中断处理、内存分配)的符号都标记为 EXPORT_SYMBOL_GPL,非GPL模块无法调用。

2.5 完整的最小模块示例

以下是一个完整的、可编译的内核模块,集成了本节所有知识点:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>

static int irq = 17;
static char *name = "default_dev";

module_param(irq, int, 0644);
module_param(name, charp, 0644);

MODULE_PARM_DESC(irq, "IRQ number for the device");
MODULE_PARM_DESC(name, "Device name string");

static int __init my_module_init(void)
{
    printk(KERN_INFO "Module loaded: irq=%d, name=%s\n", irq, name);
    /* 实际驱动中在此处注册I2C设备、申请中断等 */
    return 0;
}

static void __exit my_module_exit(void)
{
    printk(KERN_INFO "Module unloaded: cleaning up resources\n");
    /* 实际驱动中在此处释放中断、注销设备等 */
}

module_init(my_module_init);
module_exit(my_module_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Engineer <engineer@example.com>");
MODULE_DESCRIPTION("Minimal kernel module example with parameters");
MODULE_VERSION("1.0");

2.6 模块编程常见错误与避坑

  • 忘记 module_init/module_exit:模块加载成功但初始化函数未执行,功能完全失效。
  • 初始化函数返回非零值:模块加载失败,但已分配的资源未释放,导致资源泄漏。
  • 退出函数未完全清理:卸载后设备仍被占用,下次加载时冲突。
  • 参数类型不匹配:声明为 int 却传递字符串,导致内核崩溃。
  • 许可证声明缺失:内核打印警告,且无法使用 GPL 导出的符号。
  • 使用 __init 但函数被多次调用:__init 函数在初始化后可能被释放,再次调用导致非法访问。
调试建议:使用 dmesg 查看 printk 输出;使用 lsmod 查看模块加载状态;使用 modinfo 查看模块元信息;使用 cat /sys/module/<module>/parameters/<param> 查看运行时参数值。

2.7 模块参数与 sysfs 交互 SVG 图

下图展示了模块参数如何在用户空间通过 sysfs 文件系统进行读写:

模块参数与 sysfs 交互 用户空间 insmod irq=33 cat /sys/.../irq 内核空间 static int irq = 17; module_param(irq, int, 0644) /sys/module/.../parameters/irq 交互说明 1. insmod 传递参数值 2. module_param 解析并赋值 3. sysfs 创建只读/写文件 4. cat 读取当前值 5. echo 可修改(需权限)

2.8 小结

本节覆盖了Linux内核模块编程的核心基础:

  • 初始化/退出函数:模块的入口和出口,使用 __init/__exit 优化内存。
  • module_init/module_exit:注册函数到内核模块框架。
  • 模块参数:通过 module_param 实现运行时配置,支持 sysfs 动态调整。
  • 许可证与元信息:使用 MODULE_LICENSE 等宏声明模块属性。