第二章:Linux内核基础——内核模块机制、设备驱动模型与模块加载流程
各位同学,欢迎来到第二章。这一章,我们聊聊Linux内核的基础设施。说白了,就是搞清楚驱动是怎么“住进”内核的,以及内核怎么跟硬件打交道。
我刚开始接触GPU驱动适配时,也踩过不少坑。比如模块加载顺序不对,系统直接panic。又比如PCIe设备枚举失败,查了半天发现是驱动没注册对。嗯,这些经验我会在这一章里分享给你。
2.1 内核模块机制:驱动就是“插件”
Linux内核是宏内核,但支持模块化。模块(Module)就是一段可以在运行时动态加载到内核的代码。GPU驱动通常以模块形式存在。
为什么不用直接编译进内核?我个人习惯是:能用模块就用模块。原因很简单——调试方便。你改一行代码,重新编译模块,insmod一下就行。要是编进内核,你得重启机器。
核心概念:模块就是内核的“插件”。它有自己的生命周期:加载、初始化、运行、卸载。
2.1.1 模块的基本结构
一个最简单的内核模块长这样:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init my_driver_init(void)
{
printk(KERN_INFO "GPU driver loaded\n");
return 0;
}
static void __exit my_driver_exit(void)
{
printk(KERN_INFO "GPU driver unloaded\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple GPU driver module");
这里要注意几个点:
__init和__exit宏:告诉内核这些函数只在初始化/退出时使用,之后可以释放内存。module_init和module_exit:注册入口和出口函数。MODULE_LICENSE:必须声明,否则内核会报“tainted”警告。
我的经验:我曾经在项目里忘了加MODULE_LICENSE,结果加载模块时内核直接拒绝。查了半天才发现是许可证问题。嗯,这种低级错误,犯一次就记住了。
2.1.2 模块参数与依赖
模块可以接受参数。比如你想指定GPU的BAR地址:
static int gpu_bar_addr = 0x10000000;
module_param(gpu_bar_addr, int, 0644);
MODULE_PARM_DESC(gpu_bar_addr, "GPU BAR base address");
加载时这样传参:
insmod gpu_driver.ko gpu_bar_addr=0x20000000
模块之间也有依赖关系。比如你的GPU驱动依赖一个PCIe核心模块。用 modprobe 会自动处理依赖,而 insmod 不会。我建议你养成用 modprobe 的习惯。
2.2 设备驱动模型:总线、设备、驱动
Linux内核的设备驱动模型,说白了就是“媒人”机制。它有三个核心角色:
- 总线(Bus):负责匹配设备和驱动。
- 设备(Device):硬件实体,挂在总线上。
- 驱动(Driver):软件实体,负责控制设备。
你想想看,当系统启动时,内核会枚举所有PCIe设备。每个设备都有一个vendor ID和device ID。驱动会声明自己支持哪些ID。总线负责牵线搭桥——找到匹配的,就调用驱动的probe函数。
核心流程:总线枚举设备 → 驱动注册 → 总线匹配 → 调用probe → 设备初始化完成
2.2.1 PCIe设备驱动模型
GPU通常挂在PCIe总线上。PCIe驱动的核心结构是 pci_driver:
static struct pci_device_id gpu_pci_ids[] = {
{ PCI_DEVICE(0x10DE, 0x1E84) }, // NVIDIA GPU示例
{ 0, }
};
MODULE_DEVICE_TABLE(pci, gpu_pci_ids);
static int gpu_probe(struct pci_dev *dev, const struct pci_device_id *id)
{
int ret;
printk(KERN_INFO "GPU found: vendor=%04x device=%04x\n",
id->vendor, id->device);
// 启用设备
ret = pci_enable_device(dev);
if (ret) {
printk(KERN_ERR "Failed to enable PCI device\n");
return ret;
}
// 请求BAR空间
ret = pci_request_regions(dev, "gpu_driver");
if (ret) {
printk(KERN_ERR "Failed to request regions\n");
pci_disable_device(dev);
return ret;
}
// 设置DMA掩码
ret = dma_set_mask(&dev->dev, DMA_BIT_MASK(64));
if (ret) {
ret = dma_set_mask(&dev->dev, DMA_BIT_MASK(32));
if (ret) {
printk(KERN_ERR "No suitable DMA mask\n");
pci_release_regions(dev);
pci_disable_device(dev);
return ret;
}
}
return 0;
}
static void gpu_remove(struct pci_dev *dev)
{
pci_release_regions(dev);
pci_disable_device(dev);
printk(KERN_INFO "GPU removed\n");
}
static struct pci_driver gpu_driver = {
.name = "gpu_driver",
.id_table = gpu_pci_ids,
.probe = gpu_probe,
.remove = gpu_remove,
};
module_pci_driver(gpu_driver);
避坑指南:我曾经在probe函数里忘了调用pci_enable_device,结果读写BAR空间时系统直接死机。记住:启用设备是第一步,否则PCI配置空间不可用。
2.2.2 Platform设备驱动模型
有些GPU(比如嵌入式GPU)不挂在PCIe上,而是作为Platform设备存在。Platform设备是内核中“虚拟总线”上的设备,常用于SoC内部集成外设。
static struct platform_driver gpu_platform_driver = {
.probe = gpu_platform_probe,
.remove = gpu_platform_remove,
.driver = {
.name = "gpu_platform",
.of_match_table = gpu_of_match,
},
};
module_platform_driver(gpu_platform_driver);
Platform驱动通常配合设备树(Device Tree)使用。设备树里描述硬件资源,驱动通过 of_ 系列API读取。
2.3 内核编译与模块加载流程
这部分我建议你亲手操作一遍。光看文档是记不住的。
2.3.1 编译内核模块
一个标准的Makefile:
obj-m += gpu_driver.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 后,会生成 gpu_driver.ko 文件。这就是内核模块。
我的习惯:编译时加上 V=1 参数,可以看到详细的编译命令。调试时很有用。
2.3.2 模块加载与卸载
常用命令:
| 命令 | 作用 | 说明 |
|---|---|---|
insmod gpu_driver.ko |
加载模块 | 不处理依赖,需要指定路径 |
modprobe gpu_driver |
加载模块 | 自动处理依赖,从标准路径加载 |
rmmod gpu_driver |
卸载模块 | 需要模块未被使用 |
lsmod |
列出已加载模块 | 查看模块状态 |
modinfo gpu_driver.ko |
查看模块信息 | 显示作者、描述、参数等 |
加载流程是这样的:
- 用户执行
insmod或modprobe - 内核调用
sys_init_module系统调用 - 内核将模块ELF文件加载到内核空间
- 解析模块的符号表和依赖
- 调用模块的
init_module函数(即你写的module_init注册的函数) - 模块进入运行状态
避坑指南:我曾经在调试时反复insmod/rmmod,结果有一次rmmod后立刻insmod,系统报“module already exists”。原因是内核有引用计数,rmmod后需要等一会儿。用 lsmod | grep gpu 确认模块确实已卸载再重新加载。
2.3.3 查看内核日志
模块的 printk 输出会进入内核日志。用 dmesg 查看:
dmesg | tail -20
也可以实时监控:
tail -f /var/log/kern.log
或者用 journalctl(systemd系统):
journalctl -kf
2.4 知识体系总览
下面这张图,是我为你梳理的本章知识结构。建议你保存下来,复习时对照着看。
2.5 本章小结
这一章我们聊了三件事:
- 内核模块机制:驱动就是模块,有固定的结构和生命周期。
- 设备驱动模型:总线、设备、驱动三者配合,PCIe和Platform是两种常见场景。
- 编译与加载流程:从Makefile到insmod,再到dmesg看日志,每一步都有讲究。
嗯,这些内容看起来基础,但GPU驱动适配的很多坑都埋在这里。比如PCIe BAR空间没正确映射,比如模块加载顺序导致符号找不到。下一章我们会深入PCIe配置空间和BAR映射,到时候你会回来感谢这一章的铺垫。
一句话总结:驱动适配,内核基础是地基。地基不稳,上层再漂亮的代码也是空中楼阁。