ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Linux PCI驱动框架详解:从设备匹配到probe回调的完整流程

Linux PCI驱动框架详解:从设备匹配到probe回调的完整流程 开门见山聊点实在的。做Linux驱动开发字符设备、platform总线这些玩熟了之后迟早要面对一个绕不开的大块头PCI驱动框架。别被它吓住PCI子系统在Linux内核里算是最规整、最“教科书化”的一脉了。把它的骨架拆开看懂了你会发现它跟平时写的I2C、SPI、USB设备驱动在思路上是同构的——都是“总线-设备-驱动”模型下的具体实践。这篇是第一篇我不扯虚的直接围绕框架本身讲透PCI设备是怎么被内核发现的驱动该怎么写才能被匹配上匹配成功之后内核又会调用你哪些函数。这里面有概念、有流程、有代码骨架也有我实际调试中踩过的坑争取让你看完就能上手写个可用的PCI驱动雏形。1. 内容整体设计与思路拆解1.1 为什么先搞懂PCI框架而不是直接上手写驱动很多新手拿到一个PCIe网卡或者FPGA加速卡第一反应是打开内核树找类似驱动抄。这个路子不算错但容易抄出问题。PCI驱动不是纯粹的字符设备逻辑它的核心在于“设备”怎么出现在你的probe回调里。换句话说你写的不是“驱动一个设备”的代码而是“应答内核枚举结果”的代码。PCI设备不是你在代码里new出来的对象它是硬件拓扑里真实存在的实体。CPU通过PCI总线枚举机制在系统启动时对PCIe链路做扫描把每个设备的功能Function都分配一个总线号、设备号、功能号组合BDF并读出它的配置空间。Linux内核的PCI子系统在启动阶段会做同样的事情扫描总线、读取vendor ID/device ID、建立pci_dev对象然后把它们挂到PCI总线上。接下来内核蹬着“设备-驱动匹配”机制去找合适的驱动。驱动能做的只是提前声明“我支持哪些设备”然后等pci_register_driver()之后内核帮你把匹配成功的设备回调给probe()。所以你需要理解的第一件事PCI驱动的入口不是open/read/write而是probe()函数。你所有的资源初始化、寄存器映射、中断申请都应当在probe()里完成对应的释放动作放在remove()里。这个思维转变非常关键。写字符设备时我们习惯在open()里拿资源但PCI驱动不是这样——设备在系统里是常驻的驱动只要匹配上就会被probe跟应用层是否打开设备节点没有关系。1.2 框架拆解的层次总线、设备、驱动三层模型Linux设备模型的基础是“总线-设备-驱动”三层。PCI总线是其中一条具体的bus_type实例。PCI设备结构体是struct pci_devPCI驱动结构体是struct pci_driver。总线负责把设备和驱动牵到一起。这三个角色对应到代码层面需要关注的数据结构是struct pci_bus代表一条PCI/PCIe总线组织成树形结构struct pci_dev代表一个PCI设备或PCIe功能struct pci_driver代表一个PCI驱动。这三者之间的关系一句话概括驱动通过pci_driver里的id_table声明自己支持哪些设备总线拿这个表跟树上的每个pci_dev做比对比对成功就调用驱动的probe()同时把pci_dev指针作为参数传进去。之后驱动与设备的绑定关系就建立了。这套机制跟我之前反复强调的platform总线机制一模一样无非是名称换了一下。想清楚了这层关系你就能理解为什么驱动的probe()入参是struct pci_dev *pdev而不是inode、file之类的字符设备对象。在拆解过程中我建议你脑子里始终带着一个目标最终要写出来的pci_driver包含的成员其实就那么几个——name、id_table、probe、remove外加电源管理和错误处理相关的回调。框架的复杂不在结构体本身而在匹配流程和资源管理上。1.3 为什么说PCI框架“规整”一切皆有迹可循接触过Linux网络驱动或V4L2驱动的朋友应该有体会那些框架里经常有一些“未文档化”的行为比如某些回调如果没有注册内核会走一个很隐晦的默认路径。但PCI框架不是这样它从匹配到资源分配都有非常标准的流程。这主要是因为PCI总线在硬件层面就有标准配置空间里的vendor ID、device ID、class code等字段是规范写死的Linux内核的PCI子系统的枚举逻辑也严格遵循这个规范。所以学习PCI框架本质上是在学习“如何跟一个高度标准化的硬件总线协作”。之前看驱动的匹配逻辑就像对暗号——驱动和设备的暗号就是vendor ID和device ID。系统里每一个PCI设备都有这组ID。驱动只要说“我认得0x10EC这个vendor下的0x8168这个device”那么当内核枚举到一块型号为RTL8168的网卡时就会把你唤醒。这套“对暗号”机制有一个变体有些设备支持多个子型号或者厂商用子设备ID做了区分那么id_table里可以放多个条目。还有一种情况是设备没有确定的ID比如FPGA逻辑动态加载的平台这时可以用PCI_CLASS来匹配按设备类别匹配。不过这种用法在正规产品里不多见调试时可以临时用用。2. 核心细节解析与实操要点2.1 pci_driver结构体驱动的主心骨写一个PCI驱动第一步就是声明一个struct pci_driver实例。我直接给你看一个最小但完整的定义static struct pci_driver mypci_driver { .name mypci, .id_table mypci_ids, .probe mypci_probe, .remove mypci_remove, };这里.name是驱动名字会出现在/sys/bus/pci/drivers/目录下.id_table是设备ID匹配表.probe和.remove就是前面说的核心回调。有的驱动还会设置.shutdown、.suspend、.resume等回调服务于电源管理和系统重启流程。第一版驱动不需要关注这些先把probe/remove写正确就已经覆盖了90%的场景。这里有个细节容易踩坑name字段务必起得独特一些。内核里PCI驱动众多如果name起得过于通用比如叫“test”在某些调试工具里很容易混淆。同时name会决定你在sysfs里的目录名后续定位驱动绑定情况都要靠它尽量见名知意。2.2 pci_device_id匹配表驱动和设备的暗号id_table的类型是struct pci_device_id数组它定义在Linux头文件里。常见的字段是vendor、device、subvendor、subdevice、class、class_mask以及一个可选的驱动私有数据driver_data。拿一个实际网卡的匹配表举例static const struct pci_device_id mypci_ids[] { { PCI_DEVICE(0x10EC, 0x8168) }, { } }; MODULE_DEVICE_TABLE(pci, mypci_ids);PCI_DEVICE这个宏展开后就是填充vendor和device两个字段其余字段默认值为PCI_ANY_ID也就是通配。这张表的意思是凡是vendor为0x10EC、device为0x8168的PCI设备都被我这个驱动认领。MODULE_DEVICE_TABLE这行很关键。它的作用是当驱动编译为内核模块时depmod工具会读取这个表生成模块别名。这样当系统检测到新设备时modprobe就能根据设备ID自动加载对应的驱动模块。别小看这行少了它设备会出现即使有驱动、内核也找不到、需要手动insmod的情况。再补充一点如果你希望一个驱动支持多个设备就在id_table里多放几个条目。每个条目还可以单独带driver_data字段。probe的时候可以用pci_match_id()找到匹配的条目然后从entry-driver_data里拿到设备专属的配置参数。这个用法在你用一个驱动管理同一厂商的多款功能相近的设备时特别有用。2.3 匹配流程内幕从pci_bus到probe()之间发生了什么这部分值得展开讲讲。内核中负责PCI设备和驱动匹配的函数在drivers/pci/bus.c里核心是pci_match_device()和pci_match_one_device()。我把关键流程拆成三步第一步确认设备不是“已经被其他驱动绑定”的状态。如果设备已经被某个驱动claim了总线就不会再尝试匹配除非你手动解绑。第二步遍历驱动id_table里的每一个条目调用pci_match_one_device()。这个函数会把你id_table里的字段逐个跟设备的实际配置空间信息做比对。只有vendor、device、subvendor、subdevice、class这些字段全部满足条件才认为匹配。如果不满足且你的class_mask为0则视为完全不匹配。第三步如果匹配成功总线子系统会调用device_attach()在设备模型层面为设备绑定驱动然后触发驱动的probe()回调。有个细节要注意class字段的匹配不是直接相等而是按位比较。只有当“设备class class_mask (id-class class_mask)”时才算匹配。因此class_mask可以用来忽略某些子类别字段实现灵活匹配。不过普通驱动很少走class匹配这条路知道有这回事就行。2.4 sysfs与pci_dev驱动运行时的“后台”驱动匹配成功后pci_dev对象会被关联到pci_driver此时在/sys/bus/pci/drivers/mypci/目录下会出现一个符号链接指向该设备对应的pci_dev路径。这个链接本身就是驱动与设备绑定的可视化表示。手动解除绑定也在这里操作echo设备BDF /sys/bus/pci/drivers/mypci/unbind然后echo 1 /sys/bus/pci/devices/0000:01:00.0/remove可以移除设备但这些操作一般用于热插拔模拟测试。pci_dev里保存了设备的所有关键资源信息。最常用的两个是struct resource resource[DEVICE_COUNT_RESOURCE]表示设备的BAR空间struct pci_bus *bus指向设备所在总线里面保存有总线号和段号。调试的时候通过dev-resource[i]的值就能查到设备的BAR0对应了什么物理地址、分配了多大空间。打印起来也很方便dev_info(pdev-dev, BAR0: start%llx len%llx\n, (unsigned long long)pci_resource_start(pdev, 0), (unsigned long long)pci_resource_len(pdev, 0));这行代码在probe里加一下配合dmesg就能看到硬件给BAR0分配的资源情况非常直观。2.5 必要准备配置空间、BDF与lspci工具速览写驱动前建议先用命令摸一下设备底细。Linux系统里最常用的PCI调试工具就是lspci。lspci -nn它会列出系统所有PCI设备并显示vendor/device ID。比如输出“01:00.0 Ethernet controller [0200]: Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller [10ec:8168]”。这里的“01:00.0”是设备的BDF中括号里的[0200]是class code[10ec:8168]就是vendor:device。想看得更细可以用lspci -vvv -s 01:00.0这会输出设备的详细配置空间信息包括各BAR的地址、大小中断引脚PCIe链路能力等。驱动开发过程中lspci会是你最常敲的命令之一。比如你发现设备没有被驱动正确绑定第一件事就是用lspci -nn确认设备ID是否在你的id_table里资源分配异常就用lspci -vvv核对BAR值。这些信息对排查问题帮助极大。3. 实操过程与核心环节实现3.1 一个可运行的最简PCI驱动骨架理论说再多不如代码实在。下面这个驱动骨架是我实际验证过的最小可行实现。它注册了pci_driver匹配指定设备在probe里申请资源并打印信息。把它编译成模块加载后再去绑定设备就能看到完整流程。#include linux/module.h #include linux/pci.h #include linux/io.h #define DRV_NAME mypci static int mypci_probe(struct pci_dev *pdev, const struct pci_device_id *id) { unsigned long bar0_start, bar0_len; void __iomem *bar0; int ret; dev_info(pdev-dev, mypci probe: vendor%04x device%04x\n, pdev-vendor, pdev-device); /* 1. 使能设备 */ ret pci_enable_device(pdev); if (ret) { dev_err(pdev-dev, pci_enable_device failed: %d\n, ret); return ret; } /* 2. 请求BAR资源 */ ret pci_request_regions(pdev, DRV_NAME); if (ret) { dev_err(pdev-dev, pci_request_regions failed: %d\n, ret); pci_disable_device(pdev); return ret; } /* 3. 获取BAR0资源信息 */ bar0_start pci_resource_start(pdev, 0); bar0_len pci_resource_len(pdev, 0); dev_info(pdev-dev, BAR0: start%lx len%lx\n, bar0_start, bar0_len); /* 4. 映射BAR0到CPU虚拟地址空间 */ bar0 pci_iomap(pdev, 0, bar0_len); if (!bar0) { dev_err(pdev-dev, pci_iomap failed\n); pci_release_regions(pdev); pci_disable_device(pdev); return -ENOMEM; } /* 这里就可以通过bar0指针读写设备寄存器了 */ /* 例如: ioread32(bar0 0x00); */ pci_set_drvdata(pdev, bar0); return 0; } static void mypci_remove(struct pci_dev *pdev) { void __iomem *bar0 pci_get_drvdata(pdev); pci_iounmap(pdev, bar0); pci_release_regions(pdev); pci_disable_device(pdev); dev_info(pdev-dev, mypci removed\n); } static const struct pci_device_id mypci_ids[] { { PCI_DEVICE(0x10EC, 0x8168) }, { } }; MODULE_DEVICE_TABLE(pci, mypci_ids); static struct pci_driver mypci_driver { .name DRV_NAME, .id_table mypci_ids, .probe mypci_probe, .remove mypci_remove, }; module_pci_driver(mypci_driver); MODULE_LICENSE(GPL); MODULE_DESCRIPTION(Minimal PCI driver example);module_pci_driver这个宏等价于module_init module_exit它帮我们自动注册和注销pci_driver。驱动加载时调用pci_register_driver()卸载时调用pci_unregister_driver()。这个宏的引入省去了写样板代码的麻烦是当前内核推荐的写法。需要特别注意的是probe里做好事也要“留后路”——每一步都可能失败失败时必须把之前申请的资源一个个还回去。这个资源管理的原则在驱动开发里是铁律。我在实际项目里见过不少驱动probe申请了多个寄存器映射之后某一步失败时直接return导致后面remove的时候只释放了部分资源再绑定时就报resource busy。这种问题在断电重启之前根本发现不了等发现了往往已经是设备无法再次绑定的诡异状态。3.2 参数选择详解pci_enable_device到底做了什么可能有的朋友会疑惑为什么probe里第一件事通常是pci_enable_device()这个函数主要完成以下几件事打开设备的存储器空间和I/O空间访问权限配置空间里的Command寄存器启用总线主控Bus Master如果设备需要做DMA这步必须有处理一些平台相关的电源状态切换结合ACPI分配中断资源相关的准备工作。换句话说在这之前设备的BAR空间虽然分配了地址但CPU不一定能正常访问。只有执行了pci_enable_device()设备才算真正“上电苏醒”。有些驱动偷懒不调用这个函数偶尔在模拟环境里也能跑通但拿到真实硬件上大概率会翻车。所以这个调用不能省。在实际项目中有些设备还需要按需执行pci_set_master()。这个函数是pci_enable_device()内部逻辑的补充它显式设置Command寄存器里的Bus Master位。对于一些需要做DMA的简单设备建议在pci_enable_device()之后再显式调用一次pci_set_master(pdev)语义更清晰。3.3 中断申请时机与方式从INTx到MSIPCI设备最常用的中断机制有两种传统INTx和MSI/MSI-X。前者是老式的中断引脚后者是消息信号中断。在驱动层面二者的申请API不同。传统INTx方式直接调用request_irq(pdev-irq, handler, IRQF_SHARED, DRV_NAME, dev_id)。注意PCI设备的INTx是支持共享的因此申请时务必添加IRQF_SHARED标志否则中断可能申请失败。MSI方式需要先调用ret pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);这个函数是内核推荐的申请中断向量的接口。它支持MSI和MSI-X参数分别表示最小和最大向量数以及允许的类型。成功后用pci_irq_vector(pdev, 0)拿到中断号再request_irq注册handler。用MSI的好处是每个设备有自己的中断线不需要共享处理函数里少一些判断同时MSI在性能上比INTx更优。条件允许的话优先用MSI。关于中断我还想补一个实战细节在probe阶段申请中断之前要确认设备不会在驱动程序完全初始化前就产生中断。否则会发生中断处理函数访问未初始化资源的竞态问题。稳妥的做法是先映射BAR、初始化设备内部状态然后再申请中断或者在申请中断后、但在完全初始化前使用enable/disable相关函数暂时屏蔽设备中断。3.4 实操流程复盘加载、绑定与验证代码准备好之后整个验证流程分三步。第一步编译模块。如果你的驱动在Linux内核源码树外用标准的Makefile即可obj-m mypci.o然后执行make -C /lib/modules/$(uname -r)/build M$(pwd) modules。第二步加载模块。insmod mypci.ko。此时pci_register_driver()触发匹配如果系统里刚好有对应ID的设备probe()立刻被调用。用dmesg查看打印信息。第三步如果驱动编译为模块时系统里没有对应设备等设备后续插入时也会自动触发probe。调试期间不需要反复加载卸载模块来“寻找设备”反而可以用手动绑定来触发probeecho 0000:01:00.0 /sys/bus/pci/drivers/mypci/bind但注意在bind之前如果设备已经被其他驱动绑定要先unbind。这种操作在调试阶段非常常用。我在实际调试中还有一个习惯在驱动加载前先看一遍lspci -nn的输出确认BDF和ID加载后马上再看一眼lspci -k确认驱动绑定状态从“driver not found”变成了“Kernel driver in use: mypci”。这一步比看dmesg还直观特别适合现场排查。4. 常见问题与排查技巧实录4.1 “Resource temporarily unavailable”与probe失败这个报错是pci_request_regions()失败时最常见的返回值。它的含义是你要请求的BAR资源已经被其他驱动或别的PCI设备占用了。排查思路分三步。第一步用lspci -vvv确认设备的BAR是不是显示了I/O地址或内存地址而不是“disabled”。如果BAR显示disabled状态说明设备可能没有正确使能而不是资源冲突。第二步检查系统里有没有其他驱动已经绑定了这个设备。有些设备是复合功能设备多功能多个驱动共享同一个物理设备的不同功能这时候资源分配必须小心。用lspci -k查看当前驱动绑定情况。第三步检查代码里是否调用了pci_enable_device()。如果设备BAR在配置空间里没有被分配地址pci_request_regions自然拿不到资源。这种情况在BIOS没有为设备分配资源的主板上偶尔出现。我在实际开发中还见过一种情况同一个驱动被加载两次第一次加载时设备已经被绑定到了旧驱动实例第二次加载时旧驱动没有正确卸载导致新实例的probe被拒绝。这种情况用rmmod把旧驱动卸载干净或者重启系统即可解决。4.2 中断申请失败与irq vector问题中断申请失败在PCI驱动里也比较常见。如果你请求的是MSI要检查函数返回值。一个常见的坑是设备支持MSI但平台BIOS关闭了MSI使能或者设备在配置空间里报告的MSI能力不完整。这时pci_alloc_irq_vectors()返回错误。解决方式是回退到INTxret pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI); if (ret 0) ret pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_INTX);这种“先MSI、失败降级INTx”的策略在真实的网卡驱动里大量使用。功能上没毛病只是INTx共享中断处理函数里需要比对自己的设备是否真的产生了中断。另一个坑在request_irq的时候如果注册handler时忘记传IRQF_SHARED标志而该设备走的是INTx共享中断那么request_irq会直接失败。很多新手在真实台式机上遇到这个问题误以为设备坏了。PCI设备的INTx共享是常态不是例外代码里要默认加IRQF_SHARED。4.3 寄存器读回全是0xFF设备像是“死”了这个现象我印象很深。有一次拿一块FPGA板卡调试BAR0读回来全是0xFF读别的寄存器也毫无反应。第一反应是设备没复位完等了几秒再读还是0xFF。最后排查是“设备没有完全使能”——我跳过了pci_enable_device()因为当时参考的某个老驱动没调这个函数也能跑。后来补上之后一切正常。这就是我为什么要多次强调pci_enable_device(). 它对设备Command寄存器的配置直接影响BAR访问的成败。所以要记住读到0xFF时不要瞎怀疑硬件先把Command寄存器翻出来看一眼确认Memory Space Enable和Bus Master Enable位都置1了。在此之后才考虑硬件复位、供电、链路训练等问题。另一种情况是读回来全是0。这种一般不是访问违例而是设备的BAR映射到的物理地址在系统里根本没有对应内存或IO资源。用lspci -vvv对照一下BAR值再对比cat /proc/iomem的输出看资源是否真的存在。4.4 驱动绑定了但没有调用probe检查id_table有一种看似诡异的情况lspci -k显示驱动已经被绑定但dmesg里没有任何probe打印。这通常意味着设备在驱动注册之前就已经被其他驱动绑定了或者id_table里根本没有匹配到设备。比如你把PCI_DEVICE(0x10EC, 0x8168)写在表里但实际设备ID是0x10EC:0x8161那么驱动自然不会绑定。核对ID是效率最高的第一步。其次如果设备是多功能设备确认你要匹配的功能号是否正确。BDF里的function号不同设备ID可能不同匹配表里也就需要不同的条目。还有一种容易被忽略的问题id_table最后忘记加空条目{ }。如果数组没有以空条目结尾内核遍历id_table时就会越界行为不可控。这个细节我曾经看到过多个驱动翻车。检查pci_device_id数组时第一个看末尾第二个看字段。如果你的表里最后一个条目是实际设备条目必须把空条目补上。4.5 常见问题速查表为了方便快速定位问题我把前面提到的典型问题整理成一张速查表适合在调试时对照。现象可能原因排查/解决手段probe未调用id_table不匹配lspci -nn核对vendor/device确认表末尾有空条目probe报pci_request_regions失败BAR资源被占或未使能lspci -vvv查BAR状态确认pci_enable_device已调用BAR读回全0xFF设备未使能/复位未完成检查Command寄存器调用pci_enable_device等待复位BAR读回全0BAR无实际资源cat /proc/iomem核对映射检查BIOS分配request_irq失败(INTx)缺少IRQF_SHARED添加IRQF_SHARED标志MSI申请失败平台/BIOS未使能MSI回退到pci_alloc_irq_vectorsPCI_IRQ_INTX驱动绑定状态异常设备已被其他驱动绑定lspci -k查看unbind后重新bind卸载模块后设备无法重新绑定remove中资源释放不完整按probe逆序释放检查pci_iounmap/pci_release_regions是否成对这张表是我在调试各种PCI设备时沉淀出来的大部分问题在半小时内都能定位。排障的核心思路是先确认设备状态再确认驱动匹配最后确认资源申请顺序。5. 从框架到实战后续扩展方向与个人心得5.1 这一步学完接下来该补什么第一篇内容讲了最核心的匹配与资源申请PCI驱动的框架就算是立住了。但真实世界的PCI驱动远不止这点东西。接下来你按顺序可以研究四个方面DMA与流式映射。大多数高性能PCI设备网卡、加速卡、采集卡都需要做DMA。这块涉及dma_alloc_coherent、dma_map_single、dma_map_sg以及一致性映射和流式映射的区别。中断下半部。网卡驱动的NAPI框架、视频采集驱动的vb2 buffer队列都是在probe之后围绕中断与数据处理建立的流水线。电源管理与运行时PM。PCI设备在系统挂起/恢复时需要保存恢复寄存器状态suspend/resume回调里要做的事比probe还繁琐。IOMMU/SMMU与设备直通。在虚拟化场景下PCI设备要能正确参与地址翻译和直通分配这也是现代PCIe设备驱动的复杂来源之一。我个人的建议是把第一篇的驱动骨架下载到本地编译加载跑通probe/remove流程然后自己往里加中断和DMA代码。内核文档里写作流程跟实际摸设备有本质差别只有上手了才懂“调试驱动最耗时间的地方是理解设备行为不是写代码”。5.2 实战中的心法先有工具链再有代码我接触过一些工程师一上来就翻芯片手册从寄存器定义开始啃然后再写驱动。这个顺序在我看来效率偏低。更高效的做法是先在Linux用户态用devmem、setpci这样的工具把设备寄存器摸清楚再回来写内核驱动。devmem可以读写物理地址对应的虚拟映射setpci可以直接读写PCI配置空间。举个例子新板卡拿回来你可以先用setpci读出设备Command寄存器再手动写一个值测试寄存器可写性用devmem读出BAR0映射过来的寄存器看看复位值跟手册对不对得上。这些操作不需要写一行内核代码但能让你对设备的脾气有个底。等真正写驱动时你对“哪个寄存器该写什么值”已经有感性认知踩坑概率会低很多。另外工具链还包括tracepoint和ftrace。内核的PCI子系统自带不少trace事件比如pci_enable_device、pci_set_master都可以通过tracefs跟踪。调试时开一下能看出来内核在哪些函数上耗时、有没有重复调用。这些是普通文档不会教你的技巧但实际排查时很救命。5.3 个人体会框架学习最快的方式是读旧代码说个不太“高端”但很实际的经验想快速掌握某个内核框架与其硬啃内核文档不如找一份年代稍久远但功能完整的驱动代码比如某个老网卡芯片的Linux驱动然后一行行读下来。老驱动往往没有太多框架封装逻辑直白。比如看看它是怎么处理probe里的错误回滚的看看它怎么分配RX/TX描述符的看看它怎么处理中断共享的。这些细节就是框架的最佳注解。PCI框架学完后你再回去看V4L2驱动框架或网络驱动框架会发现很多相似的概念都是“子系统负责枚举与抽象驱动负责回应回调与资源管理”。这种“框架套框架”的层级关系正是Linux内核驱动开发的常态。在我看来PCI驱动是整个Linux驱动体系里最适合“精读”的一脉因为它涉及的硬件行为有标准可循不像某些soc内部外设那样千奇百怪。把PCI搞定了等于拿到了一把可以打开后续许多框架门的钥匙。下一篇我会继续讲DMA映射与数据路径的搭建也是在真实设备上最难调试的部分。这篇先把地基夯实代码能用、流程能通、问题能查就足够了。
返回列表