2. 内核模块基础:模块加载与卸载

字符设备驱动在内核中以内核模块(Kernel Module)的形式存在。模块是 Linux 内核在运行时动态加载和卸载代码的机制,无需重新编译整个内核或重启系统。理解模块的加载/卸载生命周期、入口/出口函数声明以及许可证声明,是编写任何内核驱动的前提。

2.1 模块生命周期与核心宏

每个内核模块必须向内核注册两个关键函数:初始化函数(在模块加载时执行)和清理函数(在模块卸载时执行)。这两个函数通过 module_init() 和 module_exit() 宏告知内核入口点。

宏 触发时机 典型职责 返回值
module_init(x) insmod 加载模块时 注册设备号、初始化数据结构、注册字符设备 成功返回 0,失败返回负错误码
module_exit(x) rmmod 卸载模块时 注销设备号、释放内存、删除 /proc 条目 void
关键规则:初始化函数必须返回 0 表示成功;若返回负值(如 -ENOMEM、-EBUSY),内核会认为加载失败并自动撤销所有已分配的资源。清理函数必须彻底释放初始化阶段申请的所有资源,否则会造成资源泄漏。

2.2 最小内核模块代码结构

以下是一个符合内核编码规范的“Hello World”模块,展示了 module_init、module_exit、MODULE_LICENSE 和 MODULE_AUTHOR 的标准用法:

#include <linux/module.h>      /* 模块核心宏 */
#include <linux/kernel.h>      /* printk 等级 */
#include <linux/init.h>        /* __init / __exit 标记 */

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello: module loaded.\n");
    /* 在此处注册字符设备 */
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Hello: module unloaded.\n");
    /* 在此处注销字符设备 */
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Engineer <engineer@example.com>");
MODULE_DESCRIPTION("A minimal character device driver example");
MODULE_VERSION("1.0");
编译提示:使用内核源码树中的 Makefile 编译,目标文件为 .ko(kernel object)。加载命令:sudo insmod hello.ko;卸载命令:sudo rmmod hello;查看日志:dmesg | tail。

2.3 __init / __exit 标记与内存优化

在函数声明中使用的 __init 和 __exit 标记并非可有可无。它们指示内核在特定阶段释放函数占用的内存:

  • __init:标记初始化函数。模块加载完成后,内核会释放该函数占用的代码段内存,减少运行时开销。
  • __exit:标记清理函数。如果模块被编译进内核(非模块方式),该函数会被完全丢弃,因为内置模块无需卸载。
注意:不要对 module_init() 和 module_exit() 参数中的函数名使用 static 以外的存储类修饰符。内核宏已经处理了符号导出,不需要额外加 extern。

2.4 MODULE_LICENSE 与作者信息

内核模块必须声明许可证,否则内核会在加载时打印“tainted kernel”警告。许可证声明影响内核符号的导出范围:

许可证宏 含义 能否使用 EXPORT_SYMBOL_GPL
MODULE_LICENSE("GPL") GNU General Public License v2 是
MODULE_LICENSE("GPL v2") GPL v2 专用 是
MODULE_LICENSE("Dual BSD/GPL") BSD 或 GPL 双许可 是
MODULE_LICENSE("Proprietary") 专有许可证 否(内核标记为“污染”)

其他可选元信息宏:

  • MODULE_AUTHOR("name <email>") — 作者姓名与联系方式
  • MODULE_DESCRIPTION("text") — 模块功能简述
  • MODULE_VERSION("x.y") — 版本号
  • MODULE_ALIAS("alias-name") — 模块别名,用于自动加载

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

下图展示了从用户空间执行 insmod/rmmod 到内核模块初始化/清理函数执行,再到字符设备注册/注销的完整流程:

模块加载与卸载流程 insmod hello.ko rmmod hello 内核 syscall 层 module_init / module_exit hello_init() 执行 注册字符设备 hello_exit() 执行 注销字符设备 模块状态就绪/移除 __init 标记函数加载后释放

2.6 模块加载/卸载的常见错误与排查

常见错误 1:初始化函数返回非零值,但未撤销已分配资源。例如先成功注册了设备号,后续内存分配失败返回 -ENOMEM,导致设备号泄漏。
解决方法:使用 goto 错误处理模式,在返回前统一释放已获取的资源。
常见错误 2:卸载模块时,设备文件仍被用户进程打开(open 未 close)。内核会阻止卸载并打印“Device or resource busy”。
解决方法:在 module_exit 中先检查设备引用计数,或强制关闭(不推荐生产环境)。
调试技巧:使用 lsmod 查看已加载模块列表及引用计数;使用 modinfo hello.ko 查看模块的许可证、作者、描述等元信息;使用 dmesg -w 实时监控内核日志输出。

2.7 模块参数:向内核模块传递配置

除了固定的初始化逻辑,内核模块还支持通过 module_param 宏接收用户空间传递的参数,使驱动行为可配置:

#include <linux/moduleparam.h>

static int debug_level = 0;
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0=off, 1=info, 2=verbose)");

static char *device_name = "mychar";
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(device_name, "Character device base name");

加载时传递参数:sudo insmod hello.ko debug_level=2 device_name="mydev"。参数在 /sys/module/hello/parameters/ 下以文件形式暴露,支持运行时读取(权限由第三个参数控制)。

2.8 本章知识结构总结

知识点 核心函数/宏 关键说明
模块入口 module_init() 指定初始化函数,返回 0 成功
模块出口 module_exit() 指定清理函数,释放所有资源
内存优化 __init / __exit 标记函数在特定阶段释放代码段
许可证声明 MODULE_LICENSE() 必须声明,否则内核污染
元信息 MODULE_AUTHOR 等 作者、描述、版本、别名
模块参数 module_param() 向模块传递运行时配置