3. 硬件接口与总线
大家好,欢迎来到第三章。这一章我们聊聊内核模块的基础。说白了,驱动开发就是跟内核模块打交道。你写一个驱动,最终编译成一个 .ko 文件,然后加载进内核。这个过程看似简单,但里面的门道不少。
我记得刚入行那会儿,第一次写模块,连 module_init 和 module_exit 的顺序都搞反了,结果加载倒是成功了,卸载的时候直接 kernel panic。嗯,从那以后,我对这两个宏就格外上心。
模块加载与卸载:insmod 和 rmmod 背后发生了什么?
我们先看最直观的部分。你写好了模块,编译出 hello.ko,然后执行:
sudo insmod hello.ko
sudo rmmod hello.ko
这两个命令,背后到底干了什么?
- insmod:调用
init_module()系统调用。内核会分配内存,把.ko文件的内容映射到内核空间,然后解析符号表,解决依赖关系。最后,调用模块里注册的初始化函数。 - rmmod:调用
delete_module()系统调用。内核会检查模块的引用计数,如果为 0,就调用模块的清理函数,然后释放内存。
这里有个关键点:引用计数。如果其他模块正在使用你的模块提供的函数,引用计数就不为 0,rmmod 会失败。我遇到过这种情况,调试了半天,发现是另一个驱动在调用我的接口,忘了释放。
小技巧:用 lsmod 可以查看当前加载的模块列表和引用计数。用 modinfo 可以查看模块的详细信息,比如作者、许可证、参数等。
module_init 与 module_exit:入口和出口的约定
每个内核模块都必须有两个核心宏:module_init() 和 module_exit()。它们告诉内核,模块加载时执行哪个函数,卸载时执行哪个函数。
看一个最简单的例子:
#include <linux/module.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Kernel!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel!\n");
}
module_init(hello_init);
module_exit(hello_exit);
注意两个细节:
__init和__exit标记。加了__init的函数,在初始化完成后,内核会释放它占用的内存。这对节省内存很有帮助,尤其是嵌入式设备。- 初始化函数返回
int,返回 0 表示成功,负数表示失败。清理函数返回void。
为什么会这样设计?你想想看,如果初始化失败,内核需要知道,这样才能回滚操作。我见过有人初始化失败返回 0,结果模块虽然加载了,但功能完全不正常,排查起来非常痛苦。
注意:module_init 和 module_exit 不是函数,是宏。它们会把你指定的函数地址放到特定的 ELF 段里。内核在加载和卸载时,会从这些段里找到对应的函数指针并调用。
模块许可证与作者信息:别让你的模块变成“脏”模块
内核模块必须声明许可证。这不是形式主义,而是法律问题。GPL 许可证保证了内核的自由和开源。如果你的模块使用了 GPL-only 的符号,却没有声明 GPL,内核会拒绝加载。
常用的宏有:
MODULE_LICENSE("GPL");
MODULE_LICENSE("GPL v2");
MODULE_LICENSE("Dual BSD/GPL");
MODULE_LICENSE("Proprietary");
我个人习惯,只要是开源项目,一律用 "GPL"。如果你用的是商业内核,可能需要咨询法务。
除了许可证,还有作者信息和描述:
MODULE_AUTHOR("Your Name <your.email@example.com>");
MODULE_DESCRIPTION("A simple hello world module");
MODULE_VERSION("1.0");
这些信息可以通过 modinfo 查看。别小看这些信息,当你的模块被分发到不同客户手里,出了问题,他们可以通过作者信息找到你。我曾经接手过一个项目,模块里作者信息是空的,出了问题都不知道找谁,最后只能翻 git 历史。
模块参数传递:让驱动更灵活
写驱动的时候,你不可能把所有配置都硬编码。比如,一个串口驱动,波特率可能是 9600 或 115200。这时候,模块参数就派上用场了。
内核提供了 module_param() 宏,让你可以在加载模块时传递参数。
#include <linux/moduleparam.h>
static int debug = 0;
static char *name = "default";
module_param(debug, int, 0644);
module_param(name, charp, 0644);
MODULE_PARM_DESC(debug, "Enable debug output (0=off, 1=on)");
MODULE_PARM_DESC(name, "Device name");
加载时这样用:
sudo insmod mymodule.ko debug=1 name="mydevice"
参数类型支持:byte、short、ushort、int、uint、long、ulong、charp(字符串指针)、bool、invbool。
第三个参数是权限掩码,比如 0644。如果权限不为 0,参数会在 /sys/module/<module_name>/parameters/ 下创建对应的 sysfs 文件,运行时可以修改。
避坑指南:我曾经在调试一个网络驱动时,把参数类型写成了 int,但实际传了一个很大的值,结果发生了整数溢出,导致驱动行为异常。排查了很久才发现是参数类型不匹配。所以,参数类型一定要和实际使用的一致。
知识体系总览
下面这张图,把本章的核心知识点串了起来。你可以看到,一个内核模块从编写到加载、运行、卸载的完整流程,以及每个环节涉及的关键机制。
总结一下
这一章我们覆盖了内核模块的四个核心方面:
| 知识点 | 核心要点 | 常见坑 |
|---|---|---|
| 加载与卸载 | insmod/rmmod 对应 init_module/delete_module 系统调用 | 引用计数不为 0 时无法卸载 |
| module_init/exit | __init/__exit 标记,初始化返回 int,清理返回 void | 初始化失败返回 0 会导致模块状态异常 |
| 许可证与作者 | MODULE_LICENSE 必须声明,否则无法使用 GPL-only 符号 | 许可证不匹配导致加载失败 |
| 模块参数 | module_param 支持多种类型,权限掩码控制运行时修改 | 参数类型与实际值不匹配导致溢出或异常 |