第2章 内核模块基础:模块概念、模块加载与卸载、模块参数传递、模块依赖关系、模块许可证
好,咱们今天聊聊内核模块。这东西说白了,就是Linux内核的「乐高积木」。你想想看,内核本身是个大块头,要是把所有功能都编译进去,那得多臃肿?而且每次加个新硬件还得重新编译内核,这谁受得了。
所以就有了模块机制。模块可以动态加载、卸载,不用重启系统。我个人觉得,这是Linux内核设计里最巧妙的地方之一。
2.1 模块到底是什么?
模块,本质上就是一段内核代码。但它不是直接编译进内核的,而是编译成一个独立的 .ko 文件(Kernel Object)。
我刚开始学的时候,总把模块和普通应用程序搞混。其实区别大了去了:
- 模块运行在内核空间,应用程序跑在用户空间
- 模块没有 main 函数,只有入口和出口函数
- 模块崩溃了,整个系统可能跟着挂掉
核心要点:模块是内核的一部分,不是独立进程。它拥有最高权限,也能搞出最大的破坏。
2.2 模块的加载与卸载
写一个最简单的模块,其实就两个函数:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init my_module_init(void)
{
printk(KERN_INFO "Hello, module loaded!\n");
return 0;
}
static void __exit my_module_exit(void)
{
printk(KERN_INFO "Goodbye, module unloaded!\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example module");
嗯,这里要注意几个点:
__init和__exit是宏,告诉内核这些函数只在初始化/退出时用,用完后可以释放内存module_init和module_exit是必须的,否则内核不知道从哪里开始执行printk是内核版的 printf,输出到内核日志
加载卸载的命令也很简单:
# 加载模块
insmod my_module.ko
# 查看已加载的模块
lsmod
# 卸载模块
rmmod my_module
小技巧:我习惯用 modprobe 代替 insmod,因为 modprobe 会自动处理依赖关系。比如模块 A 依赖模块 B,你用 insmod 得先手动加载 B,但 modprobe 会帮你搞定。
2.3 模块参数传递
有时候,你希望模块加载时能传一些参数进去。比如指定缓冲区大小、调试级别什么的。这就要用到 module_param 宏了。
#include <linux/moduleparam.h>
static int debug_level = 0;
static char *device_name = "default";
module_param(debug_level, int, 0644);
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
MODULE_PARM_DESC(device_name, "Device name string");
加载时传参:
insmod my_module.ko debug_level=2 device_name="mydev"
这里有个坑,我曾经踩过:参数类型一定要匹配。比如你声明了 int,传了个字符串进去,内核不会给你报错,但行为就不可预测了。嗯,调试起来特别痛苦。
另外,0644 是文件权限,表示这个参数在 /sys/module/ 下可见,运行时也能修改。如果你不想让别人改,设成 0 就行。
2.4 模块依赖关系
模块之间可以互相依赖。比如你写了个 USB 驱动,它肯定依赖 USB 核心模块。这种依赖关系是怎么来的?
说白了,就是符号导出。一个模块可以导出函数或变量给其他模块用:
// 在模块 A 中
EXPORT_SYMBOL(my_function);
EXPORT_SYMBOL_GPL(my_variable); // 只允许 GPL 模块使用
然后模块 B 就可以直接调用 my_function 了。加载时,系统会自动检查依赖,如果模块 B 依赖的模块 A 没加载,就会报错。
警告:循环依赖是噩梦。模块 A 依赖 B,B 又依赖 A,加载时就会死锁。我见过有人这么干,结果系统直接 hang 住。设计模块时一定要避免循环依赖。
查看依赖关系可以用 modinfo:
modinfo my_module.ko
# 输出中会显示 depends: 字段
2.5 模块许可证
最后聊聊许可证。你可能觉得这玩意儿就是个形式,其实不是。内核社区对许可证很敏感。
常用的宏有:
| 宏 | 含义 |
|---|---|
MODULE_LICENSE("GPL") |
GNU General Public License |
MODULE_LICENSE("GPL v2") |
GPL 第二版 |
MODULE_LICENSE("BSD") |
BSD 许可证 |
MODULE_LICENSE("Proprietary") |
专有许可证 |
为什么重要?因为内核里有些符号是用 EXPORT_SYMBOL_GPL 导出的。如果你的模块不是 GPL 许可证,就调用不了这些符号。说白了,这是内核社区保护开源生态的一种手段。
我记得有一次,一个客户非要我们写个闭源驱动,结果发现调不了几个关键的内核 API。最后没办法,要么开源,要么重写整个逻辑。嗯,这就是现实。
我的建议:除非你有特殊原因,否则一律用 GPL。省心,也符合内核社区的规矩。
小结
这一章我们聊了模块的五个核心概念:
- 模块是什么——内核的乐高积木
- 怎么加载卸载——insmod/rmmod 走起
- 怎么传参数——module_param 搞定
- 依赖关系怎么处理——EXPORT_SYMBOL 和 modprobe
- 许可证为什么重要——GPL 是王道
下一章,我们会深入模块的编译过程,聊聊 Makefile 怎么写,以及怎么调试模块。到时候见。