4、Linux设备驱动框架:设备驱动模型(device/driver/bus)、platform与PCI子系统、模块加载与卸载
Linux内核从2.6版本开始引入了统一的设备驱动模型(Device Driver Model),其核心目标是将系统中纷繁复杂的硬件资源、驱动代码和总线拓扑抽象为三个基本对象:device(设备)、driver(驱动)和bus(总线)。理解这一模型是编写任何现代Linux设备驱动的前提,尤其是对于PCIe这类高性能总线设备,驱动开发人员必须清楚设备是如何被内核发现、驱动是如何与设备匹配、以及模块生命周期管理的底层机制。本章将围绕设备驱动模型的核心原理、platform虚拟总线与PCI子系统的工作机制、以及模块加载与卸载的完整流程展开深入讲解,帮助读者建立从“写代码”到“理解内核如何运行代码”的完整认知。
4.1 设备驱动模型的核心原理
设备驱动模型的设计哲学是解耦:将“硬件是什么”(device)与“如何操作硬件”(driver)分离,由总线作为中介完成两者的匹配与绑定。这种设计使得同一驱动可以支持多个同类型设备,同一设备也可以在不同时间被不同驱动接管(例如PCIe设备在BIOS阶段和操作系统阶段的驱动切换)。
在内核源码中,三个核心结构体定义于 include/linux/device.h 和 include/linux/module.h。设备侧通过 struct device 描述硬件资源,包括中断号、MMIO地址、DMA掩码、电源域等;驱动侧通过 struct device_driver 描述驱动能力,核心是 probe() 和 remove() 回调。总线侧通过 struct bus_type 定义匹配规则和热插拔行为。
| 核心结构体 | 关键字段 | 作用 |
|---|---|---|
| struct device | init_name, devt, release, of_node | 描述物理设备或虚拟设备,包含资源信息 |
| struct device_driver | probe, remove, id_table, pm_ops | 描述驱动逻辑,提供操作硬件的回调 |
| struct bus_type | match, uevent, probe, remove | 定义总线协议,负责设备与驱动的匹配 |
| struct class | dev_groups, class_attrs | 在sysfs中创建设备类别,如net、input |
匹配过程的核心是 bus_type.match() 回调。以PCI总线为例,内核会遍历设备树上的每个PCI设备,将其vendor ID、device ID、subsystem ID等与驱动注册的 pci_device_id 表进行比对。一旦匹配成功,内核调用驱动的 probe() 函数,驱动在此函数中完成硬件初始化、注册中断、创建字符设备或网络接口等操作。
4.2 platform子系统:虚拟总线的价值
并非所有硬件都挂载在物理总线上。SoC内部集成的UART、I2C控制器、GPIO控制器等外设,它们直接映射在CPU地址空间上,没有枚举机制。为此内核引入了platform总线(一种虚拟总线),将这类设备抽象为 platform_device,驱动抽象为 platform_driver。
platform设备有两种注册方式:静态注册(在板级文件或设备树中定义)和动态注册(运行时通过 platform_device_register() 创建)。在现代内核中,设备树(Device Tree)已成为主要方式,设备节点通过 of_match_table 与驱动匹配。
// platform_driver 定义示例
static const struct of_device_id my_i2c_of_match[] = {
{ .compatible = "vendor,my-i2c-controller" },
{ }
};
MODULE_DEVICE_TABLE(of, my_i2c_of_match);
static struct platform_driver my_i2c_driver = {
.probe = my_i2c_probe,
.remove = my_i2c_remove,
.driver = {
.name = "my_i2c",
.of_match_table = my_i2c_of_match,
},
};
module_platform_driver(my_i2c_driver);
platform子系统与PCI子系统最大的区别在于:PCI设备有标准化的配置空间和枚举机制,驱动通过vendor/device ID匹配;platform设备则依赖设备树中的compatible属性或驱动名称进行匹配。对于PCIe设备驱动开发,理解platform的意义在于:PCIe控制器本身(如DesignWare、Cadence IP)通常作为platform设备注册,而挂载在其下的PCIe端点设备则通过PCI子系统管理。
4.3 PCI子系统:从枚举到驱动绑定
PCI/PCIe子系统是Linux中最为复杂的设备模型实现之一。其工作流程可以概括为:总线枚举 → 资源分配 → 设备注册 → 驱动匹配 → probe初始化。
- 总线枚举:PCIe控制器在启动时扫描总线,读取每个设备的Vendor ID、Device ID、Class Code等配置空间信息。
- 资源分配:为每个设备分配MMIO地址空间、I/O端口和中断(MSI/MSI-X)。
- 设备注册:内核为每个PCI设备创建
struct pci_dev,并注册到PCI总线上。 - 驱动匹配:遍历已注册的
pci_driver,调用pci_match_id()比较ID表。 - probe初始化:匹配成功后调用驱动的
probe(),驱动在此函数中启用设备、请求资源、注册中断。
// PCIe驱动核心结构
static const struct pci_device_id my_pcie_ids[] = {
{ PCI_DEVICE(0x10EE, 0x9038) }, // Xilinx FPGA PCIe
{ 0, }
};
MODULE_DEVICE_TABLE(pci, my_pcie_ids);
static int my_pcie_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
int ret;
// 1. 启用PCI设备
ret = pci_enable_device(pdev);
// 2. 请求MMIO资源
ret = pci_request_regions(pdev, "my_pcie");
// 3. 设置DMA掩码
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
// 4. 映射BAR空间
void __iomem *bar0 = pci_iomap(pdev, 0, pci_resource_len(pdev, 0));
// 5. 注册中断(MSI-X优先)
ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSIX);
// 6. 初始化硬件寄存器...
return 0;
}
static struct pci_driver my_pcie_driver = {
.name = "my_pcie",
.id_table = my_pcie_ids,
.probe = my_pcie_probe,
.remove = my_pcie_remove,
};
module_pci_driver(my_pcie_driver);
pci_enable_device() 必须在访问任何BAR寄存器之前调用,否则设备不会响应MMIO访问。同时,对于支持64位DMA的设备,务必在probe早期设置DMA掩码,否则后续 dma_alloc_coherent() 可能失败。
4.4 模块加载与卸载的完整流程
Linux驱动以模块(.ko文件)形式存在,其生命周期由内核模块加载器管理。加载流程涉及 insmod → sys_init_module() → module_init() 回调;卸载流程涉及 rmmod → delete_module() → module_exit() 回调。
在模块加载过程中,内核会依次完成:模块依赖解析(处理 MODULE_IMPORT_NS 和符号依赖)、模块参数解析(module_param)、执行构造函数(module_init)。对于PCIe驱动,module_pci_driver() 宏会自动展开为 module_init() 和 module_exit(),分别调用 pci_register_driver() 和 pci_unregister_driver()。
| 阶段 | 关键操作 | 常见错误 |
|---|---|---|
| 加载前 | 依赖模块检查、符号解析 | 未定义的符号(Unknown symbol) |
| module_init | 注册驱动/设备、初始化全局数据 | 资源泄漏、重复注册 |
| probe | 硬件初始化、中断注册、设备创建 | probe失败未清理已分配资源 |
| module_exit | 注销驱动、释放全局资源 | 忘记注销设备或释放内存 |
| remove | 停止硬件、释放中断、删除设备节点 | 并发访问未同步 |
一个典型的排障场景是:insmod 成功但 probe 未被调用。此时应检查 dmesg 输出,确认设备是否被正确枚举;使用 lspci -vvv 查看设备状态;检查 id_table 是否与设备ID匹配;确认设备是否已被其他驱动占用(lspci -k 查看当前驱动)。
probe() 中如果已经成功执行了 pci_enable_device(),后续步骤失败时必须在错误路径中调用 pci_disable_device()。否则设备将保持启用状态,可能导致系统挂起或后续驱动无法正常加载。建议使用 goto err_xxx 模式进行错误处理。
4.5 案例拆解:PCIe DMA驱动模块的加载与卸载
以一个基于Xilinx FPGA的PCIe DMA驱动为例,该驱动需要完成:设备枚举、BAR映射、MSI-X中断注册、DMA描述符环初始化。模块加载时,module_pci_driver() 宏展开后调用 pci_register_driver(),内核立即扫描PCI总线,发现FPGA设备(Vendor 0x10EE, Device 0x9038),匹配 id_table 后调用 probe()。
在 probe() 中,驱动依次执行:
static int my_dma_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct my_dma_dev *dma_dev;
int ret;
dma_dev = kzalloc(sizeof(*dma_dev), GFP_KERNEL);
if (!dma_dev)
return -ENOMEM;
ret = pci_enable_device(pdev);
if (ret) goto err_free;
ret = pci_request_regions(pdev, "my_dma");
if (ret) goto err_disable;
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
if (ret) goto err_release;
dma_dev->bar0 = pci_iomap(pdev, 0, pci_resource_len(pdev, 0));
if (!dma_dev->bar0) { ret = -ENOMEM; goto err_release; }
ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSIX);
if (ret < 0) goto err_unmap;
// 注册中断处理函数
ret = request_irq(pci_irq_vector(pdev, 0), my_dma_irq_handler, 0, "my_dma", dma_dev);
if (ret) goto err_irq;
// 初始化DMA描述符环(使用dma_alloc_coherent)
dma_dev->desc_ring = dma_alloc_coherent(&pdev->dev, RING_SIZE,
&dma_dev->desc_ring_dma, GFP_KERNEL);
if (!dma_dev->desc_ring) { ret = -ENOMEM; goto err_free_irq; }
// 写入硬件寄存器,启动DMA引擎
iowrite32(dma_dev->desc_ring_dma, dma_dev->bar0 + DMA_RING_BASE);
iowrite32(RING_SIZE, dma_dev->bar0 + DMA_RING_SIZE);
iowrite32(1, dma_dev->bar0 + DMA_CTRL);
pci_set_drvdata(pdev, dma_dev);
return 0;
err_free_irq:
free_irq(pci_irq_vector(pdev, 0), dma_dev);
err_irq:
pci_free_irq_vectors(pdev);
err_unmap:
pci_iounmap(pdev, dma_dev->bar0);
err_release:
pci_release_regions(pdev);
err_disable:
pci_disable_device(pdev);
err_free:
kfree(dma_dev);
return ret;
}
卸载时,remove() 执行相反操作:停止DMA引擎(写控制寄存器)、释放中断、释放描述符环、解除BAR映射、释放资源。整个过程必须保证顺序正确,否则可能导致内核崩溃或设备状态异常。
probe() 和 remove() 中的业务逻辑,而设备枚举、资源分配、匹配绑定等复杂工作全部由内核的PCI子系统完成。开发人员需要掌握的是:如何正确使用PCI API、如何管理资源生命周期、如何处理错误路径。
在实际项目中,模块加载失败最常见的原因是ID表不匹配或资源冲突。建议在 probe() 入口处添加 dev_info() 打印设备ID和BAR信息,在 dmesg 中确认设备是否被正确识别。同时,使用 modinfo 查看模块的 vermagic 和依赖信息,确保内核版本与模块编译环境一致。