2. 内核模块基础:模块的加载与卸载、module_init/module_exit机制、LICENSE与作者信息

Linux内核模块是动态加载到内核中的代码片段,用于扩展内核功能而不需要重新编译整个内核或重启系统。理解模块的加载与卸载机制、初始化与退出函数以及许可证声明,是编写可靠驱动程序的基石。

2.1 模块的加载与卸载

内核模块的生命周期由两个核心操作控制:加载(insmod/modprobe)和卸载(rmmod)。加载时,内核将模块的二进制代码复制到内核地址空间,并执行模块的初始化函数;卸载时,内核执行模块的退出函数,并释放相关资源。

操作 用户态命令 内核态回调 说明
加载 insmod mymodule.ko module_init() 指定的函数 将模块插入内核,执行初始化
卸载 rmmod mymodule module_exit() 指定的函数 从内核移除模块,执行清理
依赖加载 modprobe mymodule 自动处理依赖 自动加载依赖模块
查看已加载 lsmod — 列出当前所有内核模块
提示: modprobe 比 insmod 更智能,它会自动解析并加载模块依赖(如 depmod 生成的依赖关系),推荐在开发后期使用。

2.2 module_init / module_exit 机制

每个内核模块必须声明初始化函数和退出函数,并通过宏 module_init() 和 module_exit() 注册到内核框架中。这两个宏本质上是将函数指针放入特定的 ELF section(.init.text 和 .exit.text),内核在加载/卸载时通过符号表找到并调用它们。

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

static int __init mymodule_init(void)
{
    printk(KERN_INFO "MyModule: 模块已加载\n");
    /* 在此处注册设备、分配资源、初始化定时器等 */
    return 0;  /* 返回0表示成功,负值表示失败 */
}

static void __exit mymodule_exit(void)
{
    printk(KERN_INFO "MyModule: 模块已卸载\n");
    /* 在此处释放资源、注销设备等 */
}

module_init(mymodule_init);
module_exit(mymodule_exit);
关键点:
  • __init 标记:初始化函数在运行后会被内核自动丢弃,释放内存。
  • __exit 标记:退出函数在模块编译进内核(非模块)时会被忽略。
  • 初始化函数返回 0 表示成功;返回负的错误码(如 -ENOMEM)表示加载失败,内核会回滚加载操作。

下面的SVG图展示了模块从用户态命令到内核态回调的完整流程:

内核模块加载与卸载流程 内核模块加载与卸载流程 insmod / modprobe rmmod 内核模块加载器 内核模块卸载器 module_init() 初始化函数 module_exit() 退出函数 说明 加载时内核调用 module_init 注册的函数;卸载时调用 module_exit 注册的函数。 加载时:分配资源 内存、中断、定时器、设备号 卸载时:释放资源 归还内存、注销中断、删除定时器

2.3 LICENSE 与作者信息

Linux内核是GPL许可证下的开源项目。内核模块必须声明其许可证,否则内核会认为该模块污染了内核,并在加载时打印警告信息。此外,良好的模块应包含作者和描述信息,便于维护和追踪。

MODULE_LICENSE("GPL");               /* 必须:声明许可证 */
MODULE_AUTHOR("Zhang San <zhangsan@example.com>");  /* 推荐:作者信息 */
MODULE_DESCRIPTION("A sample kernel module for timer and interrupt demo"); /* 推荐:模块描述 */
MODULE_VERSION("1.0");               /* 可选:版本号 */
MODULE_ALIAS("mymodule");            /* 可选:模块别名 */
宏 参数示例 说明
MODULE_LICENSE "GPL", "GPL v2", "Dual BSD/GPL" 声明许可证类型;非GPL兼容许可证会导致内核标记为“污染”
MODULE_AUTHOR "Name <email>" 标识模块作者,便于问题反馈
MODULE_DESCRIPTION "简短描述" 说明模块功能,modinfo 命令可查看
MODULE_VERSION "1.0" 版本号,用于模块版本管理
MODULE_ALIAS "char-major-200-*" 为模块提供别名,便于自动加载
警告: 如果模块使用非GPL兼容许可证(如 "Proprietary"),内核将禁止该模块调用标记为 EXPORT_SYMBOL_GPL 的符号。这会导致许多核心内核API不可用,强烈建议驱动开发者使用 "GPL" 或 "Dual BSD/GPL"。

下面的SVG图展示了模块元数据与内核符号导出机制之间的关系:

模块许可证与符号访问关系 模块许可证与内核符号访问关系 GPL 模块 MODULE_LICENSE("GPL") 可访问所有 EXPORT_SYMBOL* 非GPL模块 MODULE_LICENSE("Proprietary") 无法访问 EXPORT_SYMBOL_GPL 内核符号导出 EXPORT_SYMBOL / EXPORT_SYMBOL_GPL 虚线表示非GPL模块对 EXPORT_SYMBOL_GPL 符号的访问被内核阻止

2.4 完整的最小模块示例

将上述所有元素组合在一起,得到一个完整的最小内核模块:

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

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello: 模块加载成功\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Hello: 模块卸载成功\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Expert <expert@kernel.org>");
MODULE_DESCRIPTION("Minimal hello world kernel module");
MODULE_VERSION("1.0");
编译与测试: 使用内核构建系统(Kbuild)编译此模块,生成 hello.ko。执行 sudo insmod hello.ko 加载,dmesg | tail 查看内核日志;执行 sudo rmmod hello 卸载,再次查看日志确认退出函数被调用。

2.5 常见错误与调试

  • 加载失败: 使用 dmesg 查看内核日志,定位初始化函数返回的错误码。
  • 卸载失败: 模块被其他模块或进程引用时无法卸载,使用 lsmod 查看引用计数。
  • 许可证污染: 内核日志出现 module license 'unspecified' taints kernel,需添加 MODULE_LICENSE("GPL")。
  • 符号未找到: 模块依赖的符号未导出或未加载,使用 modinfo 查看依赖,modprobe 自动处理。