尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linux PCI驱动框架核心机制解析:从总线模型到probe与DMA掩码

Linux PCI驱动框架核心机制解析:从总线模型到probe与DMA掩码 前阵子帮朋友调试一块FPGA加速卡在x86主机上反复出现设备认不到、驱动加载后马上崩掉的状况。折腾到凌晨两点最后定位到问题不是出在业务逻辑而是PCI框架层的BAR空间申请和DMA掩码设置顺序搞错了。这种经历做驱动开发的朋友应该不陌生Linux PCI驱动框架本身很成熟但如果不理解它背后的设计逻辑光靠照着网上的模板抄遇到一次复杂点的设备就容易被绕进去。这篇文章我想系统拆一下Linux PCI驱动框架的核心机制。虽然标题写着“一”内容主要聚焦在框架的通电骨架从总线模型、核心数据结构到驱动注册、设备探测、资源申请和中断机制。理解清楚这一层再往后看DMA、MSI-X、SR-IOV这些进阶话题会顺很多。文章面向的是有一定C语言和内核模块基础、想真正搞懂PCI驱动而不是只会照着例子改改的开发者。内核版本我基于长期稳定的5.15 LTS大部分接口在4.x到6.x系列里都通用个别差异我会指出来。1. 内容整体设计与思路拆解1.1 为什么Linux把PCI做成一整套框架而不是简单给几个API很多刚接触驱动开发的人会有个疑问写一个字符设备驱动好像只要实现file_operations再register_chrdev就完事了为什么到了PCI这里内核要搞出bus_type、device_driver、resource管理这么一大堆东西答案其实在于PCI设备的复杂性。一个PCI设备不像一个单纯的LED或者按键它挂在总线上有独立的地址空间有配置空间里的各种Capability有中断线路有DMA能力还可能有多功能、多个BAR、多个MSI向量。如果每个驱动都自己管理这些那光是对配置空间的解析就要重复造无数遍轮子。Linux采用的方式是分层解耦。内核的PCI子系统负责所有和总线协议相关的底层操作比如枚举设备、读取配置空间、分配资源、管理中断驱动开发者只需要关注自己设备的上层逻辑。这有点像做Web开发时框架帮你处理HTTP协议解析你只需要写路由和处理函数而不用自己拼HTTP报文。PCI框架就是驱动开发和总线协议之间的一层中间件。所以学习PCI驱动的重点不是记住某个函数怎么用而是理解框架在哪里帮你做了什么哪里需要你自己动手以及数据是怎么在各个结构体之间流转的。1.2 驱动模型里的三驾马车总线、设备、驱动要理解PCI框架首先得看懂Linux设备模型里的三个核心对象。PCI总线是bus_type的一个具体实例pci_dev是device的一个具体实例pci_driver是device_driver的一个具体实例。总线PCI Bus ├── 设备pci_dev描述“有什么” └── 驱动pci_driver描述“怎么用”总线在这里扮演的是媒人角色。设备说“我支持这些厂商号和设备号”驱动说“我支持这些厂商号和设备号”总线负责撮合。匹配成功之后驱动里的probe函数就会被调用然后驱动代码开始真正操作硬件。这种设计最大的好处是解耦。设备可以被拔出、重新枚举驱动可以动态加载和卸载两边的生命周期由总线模型统一管理。而且同一个PCI设备可以匹配多个驱动比如内核自带的驱动和out-of-tree的驱动通过优先级和modparam来控制到底谁接管。实际调试中这个“到底谁绑定了我的设备”的问题经常让人头大后面我会专门讲怎么排查。2. 核心细节解析与实操要点2.1 pci_driver结构体驱动的人口每个PCI驱动都必须定义一个pci_driver结构体。我先放一个最小化的例子然后逐个字段讲它背后的设计意图。static struct pci_driver my_pci_driver { .name my_pci_driver, .id_table my_pci_ids, .probe my_pci_probe, .remove my_pci_remove, .suspend my_pci_suspend, .resume my_pci_resume, };.name字段是驱动的名字它会在/sys/bus/pci/drivers/下面创建同名目录。这个名字不是随便起的它会被用来做驱动与设备的动态绑定/解绑操作也会出现在内核日志里。建议用和硬件功能相关的、全局唯一的字符串避免和已有驱动重名。比如内核里e1000e的就叫e1000eigb的叫igb很直观。.id_table是匹配规则它的类型是struct pci_device_id数组以空结构体结尾。这个数组决定驱动声明自己支持哪些设备。我用一个实际例子说明static const struct pci_device_id my_pci_ids[] { { PCI_DEVICE(0x10EC, 0x8168) }, { PCI_DEVICE(0x8086, 0x153B) }, { 0, } }; MODULE_DEVICE_TABLE(pci, my_pci_ids);PCI_DEVICE宏展开后就是设置vendor和device字段。除了精确匹配还可以用PCI_DEVICE_SUB匹配子系统厂商和子系统设备号、PCI_ANY_ID通配等变体。比如使用PCI_ANY_ID匹配所有vendor写起来很简单但实际产品里不要这么做否则你的驱动会把所有PCI设备都claimed包括显卡和NVMe控制器后果很灾难。我确实见过调试板卡时有人这么干结果系统里的所有PCI设备都跑到了他的驱动下面触发了大量无效的probe调用。内核里把vendor/device信息编译进模块的modinfo配合modprobe的自动加载机制系统启动时udev会根据设备的modalias自动加载对应模块。这就是为什么我们平时插入某个硬件后内核会自动加载相应驱动模块。2.2 probe函数驱动与设备的正式握手pci_driver注册后总线模型会对每一个匹配的PCI设备调用probe函数。这个函数是驱动开发的核心中的核心。原型如下static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id);第一个参数是pci_dev指向被匹配到的设备驱动后面所有硬件操作都基于它第二个参数是匹配到的id当一张驱动支持多个设备时可以通过它来判断当前到底是哪个具体型号从而做差异化初始化。probe函数的职责可以理解为“把设备用起来之前的准备工作”。典型流程包括启用设备pci_enable_device()申请资源pci_request_regions() / pci_request_region()映射BAR地址ioremap / pcim_iomap设置DMA掩码dma_set_mask_and_coherent()注册中断pci_alloc_irq_vectors request_irq / devm_request_irq初始化内部状态自旋锁、工作队列、缓冲区、字符设备或网络设备等probe返回0表示成功返回负数表示失败。失败后内核会回滚所有资源并打印日志。很多人不知道probe失败时框架做了什么其实它只负责调用你的probe并检查返回值你在前面分配的资源如果没有用devm_版本管理就全部要自己手动清理否则就会泄漏。这也是为什么我强烈建议在PCI驱动里优先使用devm_开头的资源管理接口后面我详细说。2.3 remove函数和断电时的逆向工程remove和probe正好对应。设备被拔出、驱动被解绑、模块被卸载时调用。职责就是回收一切probe里分配的东西static void my_pci_remove(struct pci_dev *pdev) { my_free_irq(pdev); pci_disable_device(pdev); pci_release_regions(pdev); }remove要注意的一个点是执行顺序。刚入行的同事常常先pci_release_regions再释放中断结果中断处理函数里还在访问BAR空间一旦硬件还在产生中断就会解引用野指针崩溃。正确顺序是先停掉业务、释放中断再释放资源映射和BAR最后pci_disable_device。热拔插的场景是remove被调用的重灾区。虽然大部分PCI设备板卡不支持热拔插但很多企业级方案里PCIe设备确实允许动态插拔例如NVMe硬盘。如果你的驱动在remove时没有做好并发保护内核在拔卡瞬间调用remove而你的中断线程还在跑就会出大问题。我自己遇到过一次PCIe SSD在被echo 1到/sys/bus/pci/devices/.../remove时触发驱动remove但驱动里的DMA回调还在等待队列里结果直接panic。从那以后我在所有中断回调里都加了严格的硬件访问状态检查。3. 实操过程与核心环节实现3.1 从零搭建一个PCI驱动的骨架代码这里我分享一个可以直接编译运行的完整骨架。它不实现任何具体业务只是把PCI驱动的骨架串起来方便理解整个生命周期。基于内核5.15模块名为pci_demo。#include linux/module.h #include linux/pci.h #include linux/kernel.h #define DEMO_VENDOR_ID 0x10EE #define DEMO_DEVICE_ID 0x0001 static int demo_probe(struct pci_dev *pdev, const struct pci_device_id *id) { int ret; ret pci_enable_device(pdev); if (ret) { dev_err(pdev-dev, pci_enable_device failed: %d\n, ret); return ret; } if (!pci_request_regions(pdev, pci_demo)) { dev_err(pdev-dev, pci_request_regions failed\n); pci_disable_device(pdev); return -EBUSY; } dev_info(pdev-dev, demo probe ok, vendor0x%x device0x%x\n, pdev-vendor, pdev-device); return 0; } static void demo_remove(struct pci_dev *pdev) { pci_release_regions(pdev); pci_disable_device(pdev); dev_info(pdev-dev, demo remove done\n); } static const struct pci_device_id demo_ids[] { { PCI_DEVICE(DEMO_VENDOR_ID, DEMO_DEVICE_ID) }, { 0, } }; MODULE_DEVICE_TABLE(pci, demo_ids); static struct pci_driver demo_driver { .name pci_demo, .id_table demo_ids, .probe demo_probe, .remove demo_remove, }; module_pci_driver(demo_driver); MODULE_LICENSE(GPL);最后那个module_pci_driver宏值得特别说一下。它把module_init和module_exit都包好了自动处理pci_register_driver和pci_unregister_driver。大多数PCI驱动可以直接用它简洁而且不容易漏。但需要注意的是如果你在init阶段除了注册PCI驱动还想做别的事比如创建额外的proc条目或者分配全局资源就不能用这个宏得手动展开module_init和module_exit。3.2 编译和加载验证框架是否正常工作写一个最简单的Makefileobj-m pci_demo.o KDIR : /lib/modules/$(shell uname -r)/build all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean编译完得到pci_demo.ko。加载前先确认设备挂在哪里lspci -d 10ee:0001如果设备存在加载模块后关注几个地方。dmesg里会出现probe的日志pci_demo 0000:03:00.0: demo probe ok, vendor0x10ee device0x0001同时/sys/bus/pci/drivers/pci_demo/目录下会多出一个0000:03:00.0的符号链接。这说明驱动和设备匹配上了。如果probe返回错误sysfs里不会有这个链接dmesg里能看到具体失败原因。实际调试中我会同时按顺序检查lspci -v的输出、/sys/bus/pci/devices/目录下的设备列表再配合dmesg就能快速判断问题的层级是设备枚举失败、资源分配失败还是驱动probe失败。加载和卸载分别用modprobe pci_demo rmmod pci_demo如果看到“Resource temporarily unavailable”这类错误多半是probe里资源申请失败后没有正确回滚模块处于不稳定状态。3.3 与硬件对话BAR空间与ioremap的精髓到了这一步驱动已经和PCI设备绑定。但真正操作硬件还需要处理BAR空间。可以把BAR想象成PCI设备向系统暴露的一扇窗口。设备内部可能有几个寄存器区域和几个DMA缓冲区它通过BAR告诉CPU“这些地址我映射到这些区域”。CPU访问BAR对应的物理地址就能触达设备内部的寄存器。PCI设备最多有6个BARBAR0到BAR5。每个BAR可能对应内存空间也可能对应IO空间。现代设备绝大多数都是memory-mappedIO空间基本快消亡了。也有的BAR不一定是常规寄存器可能是ROM或者扩展ROM或者64位BAR会占用连续两个BAR编号。内核提供了访问BAR的辅助宏unsigned long bar_start pci_resource_start(pdev, 0); unsigned long bar_len pci_resource_len(pdev, 0); unsigned long bar_flags pci_resource_flags(pdev, 0);几乎所有关于BAR的BUG根源都是三个信息没搞清楚起始地址、长度、属性。我之前遇到过一块FPGA板卡BAR0长度在配置空间里写的只有256字节但FPGA逻辑里把寄存器区域扩展到了4KB并映射了出来导致驱动访问寄存器时直接跳出BAR边界。系统没死但读回来的数据完全随机。问题很快定位到是FPGA团队更新固件后忘了同步地址映射。驱动开发中边界和长度校验永远值得多花五分钟。拿到物理地址之后CPU不能直接访问物理地址需要先ioremap把它映射到内核虚拟地址空间void __iomem *bar0_base; bar0_base ioremap(pci_resource_start(pdev, 0), pci_resource_len(pdev, 0)); if (!bar0_base) { dev_err(pdev-dev, ioremap failed\n); return -ENOMEM; }从此以后bar0_base就可以用readl/writel等访问函数来读写设备寄存器了。注意必须用readl/writel而不是直接解引用指针。PCI设备的寄存器通常要求严格按32位或64位访问有些还对对齐有特殊要求readl/writel会生成正确的访存指令而直接指针解引用在个别架构上会编译成非对齐的字节访问轻则读错数据重则总线错误。ARM平台上尤其注意这个我踩过不止一次。3.4 用devm_接口自动打理资源避免probe失败时的泄露噩梦前面提到probe失败时所有手动分配的资源都要自己释放。这在逻辑简单时没太大问题但一个真实的驱动probe可能要做十几件事任何一个中间步骤失败前面的资源都要释放。如果漏掉一个内核就会在模块卸载时报被占用资源系统越用越不对劲。这里就是devmmanaged device resources接口的价值。devm_开头的接口分配的资源在设备生命周期结束时比如设备被移除或驱动解绑自动释放devm_ioremap代替ioremapdevm_request_irq代替request_irqdevm_kzalloc代替kzallocpcim_enable_device代替pci_enable_devicepcim_iomap代替ioremap同时管理多个BAR举个例子bar0_base pcim_iomap(pdev, 0, pci_resource_len(pdev, 0)); if (!bar0_base) return -ENOMEM;配合pcim_enable_deviceprobe后面就算有十个步骤写到一半失败直接return前面的资源内核自动回收。这就免掉了一大堆goto err_out的代码驱动也更容易读、不容易漏。但devm也不是万能的。它释放资源是在设备生命周期结束时统一触发而有些资源你希望在remove阶段更早地主动释放比如中断线程你自己控制关中断的顺序比等到设备析构更安全。所以我的习惯是中断用devm_request_irq但配合pci_free_irq_vectors在remove中显式释放寄存器映射全部用pcim_iomap普通内存用devm_kzalloc。十几个项目下来这个搭配最省心。3.5 DMA掩码为什么那么重要很多PCI驱动的probe里有这么一行ret dma_set_mask_and_coherent(pdev-dev, DMA_BIT_MASK(64));看起来平平无奇但它决定了设备能访问多大的物理内存地址。这里边的逻辑是现代64位系统物理内存在几十GB到几百GB物理地址可能超过4GB32位地址上限。如果你的设备DMA引擎只能寻址32位但它被放置在高于4GB的物理内存中的buffer上那么设备写DMA时只能写一半数据甚至写错地址。dma_set_mask_and_coherent就是告诉内核我的设备DMA寻址能力是64位请把给我的DMA缓冲区放在设备能访问的地址范围内。如果设备不支持64位而驱动强行设置64位返回错误这时要降级到DMA_BIT_MASK(32)。实际调试中见过一个特别诡异的现象一个老PCI设备不是PCIe是普通PCI在32位系统上工作良好移植到64位系统后DMA数据传输偶尔会损坏数据概率大约1%。排查了快一周最后发现是驱动用了dma_set_mask_and_coherent 64位掩码而老PCI设备实际不支持64位寻址。内核给它分配了高位缓冲区设备地址位被截断后正好落到别的内存区域DMA直接踩坏了邻居数据。从那以后我对DMA掩码的判断标准是严格按硬件手册高估可以低估了性能受损高估了数据损坏。3.6 中断处理从传统INTx到MSI/MSI-XPCI设备的中断处理也是驱动必须面对的重点而且这里坑特别多。传统PCI设备用INTx中断引脚中断线路共享、CPU核之间的irq affinity不可控、触发开销大。现代PCIe设备基本都支持MSI或MSI-X。MSI是设备直接往特定内存地址写消息来触发中断MSI-X是MSI的增强版支持更多向量并且每个向量可以独立配置。Linux提供了统一的API来分配中断向量int nr_vec pci_alloc_irq_vectors(pdev, 1, 16, PCI_IRQ_MSIX | PCI_IRQ_MSI);请求至少1个、最多16个中断向量优先使用MSI-X不行退MSI再不行回退INTx。返回值是实际分配的向量数。接下来获取每个向量的irq号int irq pci_irq_vector(pdev, 0);然后把这个irq号传给request_irq/devm_request_irq注册处理函数。使用MSI-X时驱动可以给不同向量分配不同的处理函数比如一个专门处理发送完成、一个专门处理接收数据、一个处理错误事件。这是多队列网卡的基础能力。我在实际写驱动时更愿意自己声明向量用途而不是在probe里把所有irq都注册成同一个handler再在handler里做switch。多向量设备如果用单一handler中断到来后要先读寄存器判断是哪个事件这在单队列时问题不大多队列高吞吐时就成了性能瓶颈。后来我改成让MSI-X的每个向量绑定独立handler吞吐和延迟都好了很多。3.7 /sys与中断的现代实践为什么我们都在改IRQ affinity另外一个常见实际操作是调整中断的CPU亲和性IRQ affinity。对于吞吐要求高的设备比如网卡和NVMe控制器把中断绑到特定CPU上能大幅降低cache miss和锁竞争。识别设备对应的irq号cat /sys/bus/pci/devices/0000:03:00.0/irq然后写proc接口或使用irqbalance调整。也可以用驱动内代码irq_set_affinity_hint(irq, cpumask_of(cpu));不过这里有个容易被忽略的细节如果驱动在多CPU系统上给每个MSI-X向量设置affinity但后来CPU热插拔导致CPU序号变化affinity不更新中断就会落在已经离线的CPU上或者分配失衡。驱动里处理CPU热插拔的难度不低如果开始的驱动只是功能验证可以暂时不处理但生产级驱动必须考虑CPU hotplug回调否则企业客户在做CPU隔离时你的驱动就会异常。4. 常见问题与排查技巧实录4.1 设备枚举了但probe没被调用现象lspci能看到设备内核日志里也没有任何关于你驱动的报错但/sys/bus/pci/drivers/你的驱动/下面就是没有设备符号链接。这种问题90%是id_table没匹配上。先用这个命令确认设备IDlspci -n -s 03:00.0输出类似03:00.0 0200: 8086:153b前面0200是设备类别网卡8086是vendor153b是device。对比一下你代码里的id_table。如果你的匹配条件还加了subvendor/subdevice确认配置空间里的值和你写的一致。子系统ID可以通过lspci -v看。提示lspci里显示的是十六进制值而你代码里的dev_info打印的是十进制。这里经常有刚入行的朋友看混。建议直接用0x%x打印。另外一个容易被忽略的情况设备已经被别的驱动绑定了。比如你写了一个pcie通用驱动但设备已经绑定到内核自带的驱动上。可以用ls -l /sys/bus/pci/devices/0000:03:00.0/driver看当前绑定的是谁。解决方式echo -n 0000:03:00.0 /sys/bus/pci/drivers/原驱动/unbind echo -n 0000:03:00.0 /sys/bus/pci/drivers/你的驱动/bind注意使用这个机制时你的驱动必须已经注册并且id_table匹配否则bind写入时会报错。4.2 probe返回成功但读写寄存器直接卡死现象驱动加载成功但一执行readl系统就hung住有时候还能看到硬件看门狗跳动。这个问题的原因通常有两种可能第一种是设备没真正被enable。pci_enable_device不仅仅是走个形式它要处理电源状态、I/O和内存空间的开启。如果某个设备之前在BIOS里被disable了而你的驱动没调用pci_enable_device或者调用了但它在早期的probe里失败被忽略访问BAR就是访问一块禁用的区域。有些主板的PCIe slot在BIOS设置里是关闭的linux下pci_enable_device会尝试激活但如果PCIe链路本身没训练成功这一步也会失败。第二种常见原因是设备被reset了或者固件没加载BAR内部寄存器根本无法响应请求。PCIe访问超时后CPU会挂起总线在x86上表现为AICAdapter I/O Configuration错误或者直接机器重启。区分这两种情况的方法很简单先不加驱动直接用setpci读配置空间看看设备的vendor ID是否正常。如果setpci读配置空间都失败问题在网络链路层或硬件本身drivers怎么改都没用。4.3 加载驱动后中断风暴中断风暴的典型特征是insmod后系统立即被海量中断淹没甚至失去响应。最常见的场景是设备的中断mask在probe时没有及时屏蔽而驱动又没在request_irq之前把设备的中断产生条件初始化好。设备一旦上电就开始发中断但CPU还没有安装对应的handler这个中断在root级别反复触发。排查中断风暴时的做法是在probe的一开始甚至pci_enable_device之前直接往设备控制寄存器写入禁用中断的初始值。等所有初始化都完成之后最后一步开启中断。这个顺序不能乱很多开发者图省事先使能设备再初始化寄存器结果踩了中断风暴。中断风暴另一个隐蔽来源是IRQ没有正确释放。在remove时如果先释放了BAR映射而设备仍然在产生中断处理器会读取到ff0xffffffff然后当作无效中断。如果反复发生内核的spurious interrupt逻辑会逐渐将该irq禁用最终导致设备彻底失联。解决它只能靠规范remove的逆序操作。4.4 常见问题速查表现象可能原因排查方法解决方向lspci看不到设备链路训练失败/供电不足/PCIe slot关闭检查硬件连接lspci -v看link状态硬件问题BIOS里打开slotlspci看到但无probeid_table不匹配/资源被占用对比lspci -n和id_table检查driver绑定修正id_table手动bindprobe失败但没有详细报错资源申请失败dev_info没打印关键值dmesg看具体返回码依据返回码精确处理读写寄存器时挂死设备未enable/链路异常/地址越界先setpci验证配置空间再确认BAR长度补全pci_enable_device核对BARDMA数据损坏DMA掩码设置超出设备能力检查dmesg中的DMA分配地址按硬件手册设置mask不贪64位中断风暴中断使能过早/设备中断未mask检查probe顺序日志初始化完成后开中断remove时崩溃中断未释放就释放资源/并发访问code review并发测试逆序操作严格去抖释放后再清资源4.5 调试技巧usleep比什么都好用底层的硬件调试遇到奇怪问题时我几乎总是先加几个usleep试试。很多人觉得在probe里sleep很“不专业”但这恰恰是把“时序问题”和“逻辑问题”分开的好办法。设备上电后内部PLL可能还没锁固件还没加载完你立即读寄存器它给你返回的就是一个随机值或者总线错误。这种问题在PCIe SSD上特别常见固件加载需要几百毫秒。我在FPGA类PCIe设备的调试中碰到过类似情况开发板每次开机前100毫秒读BAR寄存器都会返回全F100毫秒之后一切正常。最终方案是probe里轮询一个设备ready寄存器超时上限设到5秒比盲目固定delay靠谱得多。切忌在中断上下文里做这种等待中断里sleep直接panic。5. 一些真正值得记住的经验最后聊几点我在多个项目里沉淀下来的体会新手和老手都可以对照参考。第一PCI驱动开发的核心不是写代码而是搞懂硬件。很多时候你花了三天都没调通的问题其实是板子上电时序或者复位信号导致的。拿到一个不熟悉的PCIe设备先别急着写代码把datasheet里关于电源、时钟、复位、BAR布局、中断控制器的章节全部读完就是性价比最高的一步。数据手册里最容易被忽略的是“device ready”这类状态位有没有轮询它直接决定你的驱动稳不稳。第二devm_接口能救命但别盲信。它在常规路径上非常省心可如果你在设计中断线程的退出逻辑必须想清楚什么时候该主动清理而不是等到devm帮你清理。设备生命周期结束时的自动清理是在锁都释放后的你的中断处理线程如果还在访问那个已卸载的设备它可不管你是不是devm。第三学会用tracepoint和function_graph比什么工具都好。调试PCI驱动问题时打开内核的function_graph跟踪能看清内核在probe里具体停在了哪个函数上。配合CONFIG_DYNAMIC_DEBUG直接在驱动代码里用dev_dbg动态打开调试日志比无脑printk干净得多。方法是在/sys/kernel/debug/dynamic_debug/control里echo相关规则。第四也是我吃了最多亏的地方不要在开发版上顺手把IOMMU关掉来定位DMA问题。IOMMU包括x86的VT-d和ARM的SMMU打开的状态下DMA失败通常表现为“IOMMU mapping error”这种清晰的日志一旦关掉错误类型就从“明确的失败”变成“不确定的随机内存破坏”。这两者的debug难度天差地别。我见过一个同事关掉IOMMU后调了两周最后发现是设备的DMA描述符多写了两个字节破坏了相邻物理地址上的数据。如果开着IOMMU这个问题能在半小时内看出来。硬件debug的正确姿势是尽量保持默认的IOMMU配置。第五Linux PCI相关代码本身就是最好的教材。drivers/pci下的源码、drivers/net/ethernet/intel/igb这类成熟的驱动都极其值得读。igb驱动对PCI资源管理、MSI-X、多个队列、电源管理的处理非常经典。我每次要写一个新的PCIe设备驱动都会先把igb里对应的部分找出来读一遍比看任何二手教程都高效。这篇先写到这PCI框架的骨架应该已经清楚了。下一篇我计划重点拆DMA方向的细节包括流式映射与一致性映射的区别、SG表的使用、以及ioMMU组相关的坑这些都是实际高速设备绕不开的部分。
返回列表