3、驱动与内核模块:Linux驱动模型简介,内核模块(.ko)的编译与加载机制,模块依赖关系
好,咱们今天聊聊驱动和内核模块。说实话,很多刚入行的朋友一听到「驱动开发」就觉得特别底层、特别难。其实没那么玄乎。你想想看,驱动说白了就是一段代码,让内核认识你的硬件设备。而内核模块(.ko文件),就是这段代码的「打包形式」。
我在项目中遇到过不少同事,把驱动直接编译进内核,结果每次改一行代码就要重新编译整个内核,一等就是半小时。嗯,这其实没必要。用模块的方式,编译快、加载灵活,调试也方便。今天我就把这块儿掰开了讲清楚。
3.1 Linux驱动模型:设备、总线、驱动
Linux内核的驱动模型,核心就三个概念:设备(device)、总线(bus)、驱动(driver)。它们之间的关系,我画了张图,你看一眼就明白了。
这张图表达的意思很简单:设备挂在总线上,驱动也注册到总线上。总线负责做「媒人」——它拿着设备的ID和驱动的ID表去比对。匹配上了,就调用驱动的probe()函数,让驱动去初始化设备。
我个人习惯把总线想象成「USB集线器」。你插一个U盘(设备),集线器(总线)会通知所有注册过的驱动:「嘿,有个新设备来了,你们谁认识它?」。如果某个驱动说「我认识」(通过vendor_id和device_id匹配),那就由它来接管这个设备。
核心要点:驱动不直接操作设备,而是通过总线来交互。这种分层设计让代码复用性极高。比如你写了一个I2C触摸屏驱动,只要挂到I2C总线上,换任何一款I2C控制器都不用改驱动代码。
3.2 内核模块(.ko)的编译与加载机制
好了,模型讲完了,咱们来点实际的。怎么把一个驱动编译成.ko文件?又怎么把它加载进内核?
3.2.1 编译一个最简单的模块
先看代码。这是一个最简的内核模块,我习惯叫它「hello world」版驱动:
// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, module loaded!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, module unloaded!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example module");
然后你需要一个Makefile。嗯,这里有个坑,我刚开始学的时候踩过:
# Makefile
obj-m += hello.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
执行 make 之后,你会得到一个 hello.ko 文件。这就是内核模块。
小提示:-C 指定的是内核源码树的路径。如果你在交叉编译环境(比如给ARM板子编译),记得把 /lib/modules/$(uname -r)/build 换成你交叉编译工具链对应的内核源码路径。
3.2.2 加载与卸载模块
加载模块用 insmod 或 modprobe。卸载用 rmmod。看操作:
# 加载模块
sudo insmod hello.ko
# 查看是否加载成功
lsmod | grep hello
# 查看内核日志
dmesg | tail
# 卸载模块
sudo rmmod hello
你执行 dmesg 应该能看到我们printk打印的那两句话。这里有个细节:printk 不是往终端打印,而是往内核日志缓冲区写。用 dmesg 才能看到。
注意:我曾经在调试一个网络驱动时,连续加载卸载模块几十次,结果内核日志缓冲区被刷爆了,导致系统卡顿。建议调试时用 dmesg -n 1 降低日志级别,或者用 dmesg -c 清空缓冲区。
3.2.3 insmod vs modprobe
这两个命令有啥区别?我简单说说:
| 命令 | 特点 | 适用场景 |
|---|---|---|
insmod |
直接加载.ko文件,不处理依赖 | 调试单个模块,你知道它没有依赖 |
modprobe |
自动处理依赖,加载所有需要的模块 | 生产环境,或者模块有依赖关系时 |
说白了,insmod 是「我就加载这一个,别的我不管」。而 modprobe 会去查 /lib/modules/$(uname -r)/modules.dep 这个依赖文件,把当前模块依赖的其他模块先加载进来。
3.3 模块依赖关系
模块之间为什么会有依赖?举个例子。你写了一个I2C触摸屏驱动,它需要调用I2C核心层提供的函数(比如 i2c_add_driver())。那么你的模块就依赖于 i2c-core 模块。
依赖关系是怎么确定的?看两个地方:
- 符号导出:模块A用
EXPORT_SYMBOL()导出一个函数,模块B调用它。那么B就依赖A。 - 编译时记录:编译模块时,内核构建系统会自动分析符号引用,生成依赖信息。
查看依赖关系,用 modinfo 命令:
# 查看模块信息,包括依赖
modinfo hello.ko
# 查看系统已加载模块的依赖
lsmod
lsmod 的输出中,第三列「Used by」就显示了依赖关系。比如:
Module Size Used by
i2c_dev 20480 0
i2c_core 65536 1 i2c_dev
这表示 i2c_dev 被 i2c_core 使用(依赖)。Used by 后面的数字是引用计数,如果大于0,你就不能卸载这个模块。
避坑指南:我曾经在调试一个音频驱动时,想卸载 snd-hda-intel 模块,结果系统提示 rmmod: ERROR: Module snd-hda-intel is in use。一查 lsmod,发现 snd-pcm 和 snd-hda-codec 都依赖它。必须先卸载依赖它的模块,才能卸载它。顺序很重要。
3.3.1 手动处理依赖
如果你用 insmod 加载一个有依赖的模块,系统会报错:
sudo insmod my_driver.ko
insmod: ERROR: could not insert module my_driver.ko: Unknown symbol in module
这个「Unknown symbol」就是告诉你,模块里引用的某个函数找不到——因为它依赖的模块还没加载。这时候你有两个选择:
- 先用
insmod加载依赖模块,再加载你的模块。 - 直接用
modprobe my_driver,它会自动搞定一切。
我个人习惯在开发阶段用 insmod,因为依赖关系简单,而且报错信息更直接。生产环境或者模块多了以后,用 modprobe 更省心。
3.3.2 依赖文件是怎么生成的?
你执行 depmod 命令,内核会扫描 /lib/modules/$(uname -r)/ 目录下所有的.ko文件,分析它们的符号引用,生成 modules.dep 和 modules.dep.bin 文件。
# 更新依赖信息
sudo depmod -a
这个命令在安装新模块后一定要执行一次,否则 modprobe 找不到新模块的依赖关系。
小技巧:如果你在嵌入式板子上开发,每次更新模块后,记得 depmod -a 一下。我见过有人折腾了半天模块加载不上,最后发现是忘了跑 depmod。嗯,这种低级错误我也犯过。
3.4 总结一下
今天咱们聊了三件事:
- 驱动模型:设备、总线、驱动三者的匹配机制。记住,总线是媒人。
- 模块编译与加载:写一个简单的.ko,用
insmod/rmmod加载卸载。Makefile的写法要记牢。 - 依赖关系:模块之间通过符号导出产生依赖。
modprobe自动处理依赖,insmod需要手动处理。
这些是驱动集成到Rootfs的基础。你想想看,如果连模块怎么编译、怎么加载、依赖怎么处理都不清楚,后面做Rootfs集成肯定会碰壁。先把这块儿练熟,后面咱们再聊怎么把驱动自动化集成到文件系统里。