资讯详情

手写最小PCIe驱动:从零打通内核模块与设备匹配

📅 2026/9/17 6:05:05 | 华诺云谱 👁 阅读
手写最小PCIe驱动:从零打通内核模块与设备匹配
如果你在这个系列里跟到了 Day 15前面十几天应该都在聊 PCIe 协议本身链路训练、TLP 格式、配置空间、枚举顺序、BAR 空间…… 理论铺垫了一大堆但手头始终没有一个能“摸得着”的东西。今天不一样今天写代码。标题写得也很直白——《写一个能跑的最小 PCIe 驱动》这是本系列第一次真正动代码。这一篇的核心目标很简单在 Linux 环境下从零写一个 PCIe 驱动模块加载进内核能识别出指定的 PCIe 设备能从配置空间里读出 vendor ID、device ID、class code 这些基本信息然后干净地卸载。不搞 DMA、不搞中断、不搞用户态交互就一个最朴素的骨架。但就是这一个骨架能把 PCIe 驱动开发里最关键的几条链路打通内核模块怎么编译、pci_driver 怎么注册、probe 什么时候被调用、设备 ID 表怎么匹配、sysfs 里怎么确认驱动真的绑上了设备。先说清楚这篇适合谁。如果你在 AI Infra 方向做加速器、网卡、存储卡的驱动适配或者你只是对 Linux 设备驱动感兴趣但一直没动手这篇文章都可以作为起点。PCIe 驱动的难度很大程度上来自“黑盒感”——你不知道内核什么时候调你的代码、为什么调、调了之后做什么。今天这个最小驱动就是打破黑盒的第一个锤子。1. 为什么从“最小驱动”开始而不是直接上完整功能很多朋友一上来就想写一个能跑 DMA、能处理 MSI-X 中断的完整驱动结果被 complex 的 API 和内核机制搞得焦头烂额。我自己的建议始终是第一个 PCIe 驱动一定要“小到不能再小”小到你能看清它每一行在干什么。这不是浪费时间反而是最快的路径。1.1 驱动开发的学习曲线从 hello world 到 PCIe 驱动Linux 驱动开发有一个经典的“hello world”版本就是一个最简单的内核模块加载时打印一句话。很多教程会让你从这里开始但直接跳到 PCIe 驱动中间差着好几个概念层。写一个 PCIe 驱动本质上不是“写”驱动而是“注册”驱动。你要做的是告诉内核我是谁这个驱动属于哪个 PCI 设备我能干什么驱动提供哪些回调函数我什么时候出现内核在枚举到匹配设备时会调用probe函数。理解了这个逻辑你就明白为什么最小驱动是学习的最佳切入点了。它没有复杂的业务逻辑只包含“注册 匹配 回调”这条主线。这条主线一旦通了后面加 DMA、加中断、加 ioctl都只是在这个骨架上挂肉。1.2 最小驱动的边界它到底“能跑”到什么程度我这边说的“能跑”至少包括下面几个可验证的标准模块能正常insmod加载没有报错。加载后在/sys/bus/pci/drivers/下能看到你的驱动目录。如果系统里存在匹配的设备probe函数会被内核调用dmesg里能看到你打印的信息。通过读配置空间能拿到 vendor ID、device ID、class code 等字段。rmmod卸载时remove函数被调用资源能干净释放。如果你的驱动能做到这 5 点那它就“能跑”了而且已经是一个合格的 PCIe 驱动骨架。现实中很多生产驱动的启动部分也不过是把probe里的打印换成资源申请、寄存器初始化、设备注册而已。1.3 两个前置认知设备模型与 pci_driver 入口在写代码之前有两个概念必须先建立起来否则后面容易走弯路。第一个是 Linux 设备模型。在 Linux 里设备驱动通过bus来连接设备和驱动。PCI 总线这个bus上一边挂着物理的 PCIe 设备比如网卡、GPU、NVMe SSD一边挂着驱动。当内核枚举到一个 PCIe 设备时它会把这个设备的 vendor ID、device ID、class code 等信息和设备树里的每个驱动去比对。比对靠的是驱动注册时提供的pci_device_id表。第二个是pci_driver结构体。它是驱动和内核之间的“合同”。这个合同规定了你必须提供哪些函数、可选提供哪些函数。核心成员包括name驱动名称会显示在 sysfs 里。id_table驱动支持的设备 ID 表。probe设备匹配成功的回调。remove设备移除或驱动卸载时的回调。有了这两个认知你就可以开始搭环境了。驱动开发最怕的就是环境不对导致问题根本无法复现。所以我建议先在虚拟机里搭一个可控的实验台。2. 环境准备在虚拟机里搭一个可控的 PCIe 实验台写 PCIe 驱动最大的风险是把你自己的开发机搞崩。probe里一个野指针就可能 kernel panic如果跑在主力机上轻则重启重则文件系统损坏。所以我强烈建议用虚拟机做实验。2.1 为什么推荐 QEMU 内核模块而不用实体机实体机当然更“真实”但对于学习阶段来说虚拟机的好处太明显了快照功能可以让你随时回滚不怕把内核搞挂。QEMU 可以虚拟出特定的 PCIe 设备让你不用反复拔插硬件。调试信息更清晰dmesg里能看到完整的 PCIe 枚举日志。不依赖真实硬件在公司电脑或没有空闲机器的环境下也能继续学。这里我选择 QEMU/KVM因为它既能跑 Linux 虚拟机又能给虚拟机挂虚拟 PCIe 设备。具体方案主机上用 QEMU 启动一个 Ubuntu Server 虚拟机分配 2 个 CPU、2GB 内存即可磁盘 20GB 就够。驱动编译需要内核头文件所以虚拟机里要安装linux-headers-$(uname -r)。2.2 准备一个“虚拟 PCIe 设备”作为实验对象驱动得有设备才能发挥作用。有两种方式第一种直接用虚拟机自带的虚拟 PCIe 设备比如 QEMU 的virtio-net、virtio-blk但这类设备已经有现成驱动了你的模块注册后可能匹配不上或者匹配上之后 probe 会被现有驱动占住学习体验不好。第二种给虚拟机挂一个简单的 PCIe 设备但设备上没有任何驱动绑定。这能让你的驱动“独享”这个设备调试起来非常清爽。我在实验里用的是 QEMU 自带的“edu”设备-device edu。这是一个专门为教学设计的 PCI 设备暴露了少量 BAR 空间和寄存器非常适合驱动开发练习。不过 edu 设备并不总是存在取决于 QEMU 版本和编译配置。如果没有也可以用万能的“pci-testdev”再不行就挂一个额外的 virtio-net-pci然后通过 driver_override 来强制绑定到你的测试驱动这部分后面坑位里会讲到。2.3 编译内核模块需要的最小工具链在虚拟机里你需要安装build-essential提供 gcc、make 等基础工具。linux-headers-$(uname -r)提供内核编译所需的头文件和 Makefile 片段。git后面如果要拉一些测试工具或样例代码会用得上。安装命令很简单sudo apt update sudo apt install build-essential linux-headers-$(uname -r) git装完之后可以验证一下内核头文件是否存在ls /lib/modules/$(uname -r)/build只要这个目录存在并能看到Makefile基本就没问题了。2.4 第一次编译Makefile 与内核头文件的对应关系内核模块的编译方式和普通 C 程序完全不同。它不是简单的gcc -o pci_mini pci_mini.c而是通过内核的Kbuild系统完成。你需要写一个 Makefile告诉内核模块要在哪个目录下编译。obj-m : pci_mini.o KERNEL_DIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KERNEL_DIR) M$(PWD) modules clean: $(MAKE) -C $(KERNEL_DIR) M$(PWD) clean这里最关键的一点是KERNEL_DIR必须指向内核头文件目录。很多人编译报错都是因为内核头文件版本和当前运行内核版本不一致。比如uname -r显示5.15.0-91-generic但linux-headers-$(uname -r)没装或者装的是另一个版本编译时就会出现版本魔法值不匹配。这个问题在第 4 节会有详细说明。3. 手写代码一个最小 PCIe 驱动的完整实现环境准备好之后就可以开始写代码了。下面是一份我实际测试过的完整代码你可以直接抄走。我会逐段解释确保不是背模板而是真正理解。3.1 从零开始写 pci_mini.c头文件与模块声明创建文件pci_mini.c输入以下内容#include linux/module.h #include linux/pci.h #include linux/kernel.h #include linux/init.h #define DRV_NAME pci_mini static int pci_mini_probe(struct pci_dev *dev, const struct pci_device_id *id); static void pci_mini_remove(struct pci_dev *dev); static struct pci_device_id pci_mini_ids[] { { PCI_DEVICE(0x1234, 0x5678) }, { 0 } }; MODULE_DEVICE_TABLE(pci, pci_mini_ids); static struct pci_driver pci_mini_driver { .name DRV_NAME, .id_table pci_mini_ids, .probe pci_mini_probe, .remove pci_mini_remove, }; module_pci_driver(pci_mini_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(Minimal PCIe driver example);先看头文件linux/pci.h是 PCI 子系统核心头文件驱动框架全部在这里定义。linux/module.h提供模块相关宏linux/kernel.h提供打印函数。再看module_pci_driver这个宏是 2.6 之后推荐的写法。它帮你生成了 module_init 和 module_exit 两个函数分别调用pci_register_driver和pci_unregister_driver。省掉了手写init/exit函数模板代码这是最小驱动能这么短的关键原因。3.2 pci_device_id 表驱动怎么认出自己的设备static struct pci_device_id pci_mini_ids[] { { PCI_DEVICE(0x1234, 0x5678) }, { 0 } }; MODULE_DEVICE_TABLE(pci, pci_mini_ids);pci_device_id表就是驱动和设备之间的“暗号”。内核枚举 PCIe 设备时会把设备的 vendor ID 和 device ID 拿出来和每个驱动的 id_table 比对。PCI_DEVICE(vendor, device)是一个宏展开后就是一个pci_device_id实例表示“我支持 vendor 为 0x1234、device 为 0x5678 的设备”。注意这个表必须以{ 0 }结尾表示数组结束。MODULE_DEVICE_TABLE(pci, ...)这个宏的作用不只是声明它还会把这张表导出到模块的 modinfo 信息里方便热插拔工具如 modprobe 进行设备匹配。如果没有这一行modprobe可能没法自动为你加载模块。表里的 vendor/device ID 怎么选如果你是在 QEMU 里挂 edu 设备edu 设备的 vendor ID 是0x1234、device ID 是0x5678刚好和上面的例子一致。如果你用真实设备可以通过lspci -nn查看设备对应的 ID。$ lspci -nn 00:04.0 Memory controller [0580]: Device [1234:5678] (rev 10)方括号里的1234:5678就是 vendor 和 device ID。3.3 probe 函数里做了什么读 vendor/device/class打印配置空间probe是驱动最重要的回调。当设备匹配成功内核会调用它。static int pci_mini_probe(struct pci_dev *dev, const struct pci_device_id *id) { u16 vendor, device; u32 class; vendor pci_vendor_id(dev); device pci_device_id_of(dev); class dev-class; pr_info(%s: probe device %04x:%04x, class %06x\n, DRV_NAME, vendor, device, class); return 0; }这里有几个点要注意。第一pci_vendor_id(dev)和pci_device_id_of(dev)是内核提供的方便函数分别从dev-vendor和dev-device读取。你也可以直接访问dev-vendor和dev-device效果一样。第二dev-class是 32 位整数但只有高 24 位有意义低 8 位是编程接口Programming Interface。所以打印时用%06x。比如一个 Memory Controller 设备class 通常是0x058000base class 05、subclass 80。第三pr_info是内核打印函数输出的日志会进入内核环形缓冲区通过dmesg查看。内核里一般不用printf因为它属于用户态。到这里你可能会有疑问probe里面什么事情都没做只读了一下信息这算驱动吗算。因为对一个最基本的学习驱动来说能证明“我被调用了、设备信息我拿到了”就已经完成了 80% 的目标。后续的 BAR 映射、中断注册、DMA 初始化都要在这个函数里往后加。3.4 remove 函数与模块生命周期static void pci_mini_remove(struct pci_dev *dev) { pr_info(%s: removing device %04x:%04x\n, DRV_NAME, pci_vendor_id(dev), pci_device_id_of(dev)); }remove函数会在三种情况下被调用设备从系统里被移除例如虚拟机里device_del。驱动被卸载rmmod。驱动的id_table被重新绑定。在生产驱动里remove要负责释放中断、释放 BAR 映射、销毁设备结构体、彻底停止硬件活动。这里我们只打一行日志因为根本没有申请任何资源。但它的存在很重要模块卸载时如果remove不存在内核会打一个 warning影响体验。3.5 完整代码清单把上面几段合并就是一份完整的pci_mini.c#include linux/module.h #include linux/pci.h #include linux/kernel.h #include linux/init.h #define DRV_NAME pci_mini static int pci_mini_probe(struct pci_dev *dev, const struct pci_device_id *id) { u16 vendor, device; u32 class; vendor pci_vendor_id(dev); device pci_device_id_of(dev); class dev-class; pr_info(%s: probe device %04x:%04x, class %06x\n, DRV_NAME, vendor, device, class); return 0; } static void pci_mini_remove(struct pci_dev *dev) { pr_info(%s: removing device %04x:%04x\n, DRV_NAME, pci_vendor_id(dev), pci_device_id_of(dev)); } static struct pci_device_id pci_mini_ids[] { { PCI_DEVICE(0x1234, 0x5678) }, { 0 } }; MODULE_DEVICE_TABLE(pci, pci_mini_ids); static struct pci_driver pci_mini_driver { .name DRV_NAME, .id_table pci_mini_ids, .probe pci_mini_probe, .remove pci_mini_remove, }; module_pci_driver(pci_mini_driver); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(Minimal PCIe driver example);代码总量不到 50 行但已经是五脏俱全的 PCIe 驱动骨架了。4. 编译、加载与验证让驱动真正“跑起来”代码写完了接下来是最激动人心也最容易出问题的一步编译和加载。我建议每一步都慢一点亲眼看到内核的反应。4.1 编译的三种常见报错及解决办法在模块源码目录下执行make正常情况下会生成pci_mini.ko文件。但第一次编译大概率会遇到下面几种报错。第一种linux/pci.h: No such file or directory。这是最常见的问题原因是内核头文件没装或者 Makefile 里的KERNEL_DIR指向不对。检查一下ls /lib/modules/$(uname -r)/build如果这个目录不存在说明头文件没装。如果存在但编译还是找不到头文件那就要确认KERNEL_DIR路径里是不是有include目录。还有一种情况是你安装了多个内核版本uname -r和linux-headers版本不一致。用uname -r查看当前内核版本再比对安装的头文件包名。第二种版本魔法值不匹配。报错信息大致是version magic 5.15.0-91-generic SMP mod_unload modversions should be 5.15.0-89-generic SMP mod_unload modversions这说明你在用 5.15.0-91 的内核但头文件是 5.15.0-89。解决办法很简单重启到 5.15.0-91或者安装 5.15.0-91 的头文件然后重新编译。驱动模块和内核版本必须严格一致没有半点商量余地。第三种未定义符号或者类型错误。这通常是因为内核 API 版本差异。比如某些新的内核把pci_vendor_id移到了头文件里而老内核没有这个函数。如果你用的是 5.x 以上内核上面的代码没问题。如果你用的是 4.x可能pci_vendor_id不存在直接改用dev-vendor即可。4.2 insmod 与 modprobe 的选择编译成功后加载模块有两个命令insmod和modprobe。sudo insmod pci_mini.koinsmod是“死板”地加载一个模块文件不会自动处理依赖。modprobe则会根据模块名和依赖关系自动加载。对单个测试模块来说insmod更直接出错时也能更清楚地看到问题所在。但它有个缺点如果模块里有未定义符号insmod会直接报错而不是像 modprobe 那样去依赖其他模块。模块加载成功后你可以用lsmod | grep pci_mini看到它pci_mini 16384 0第三列0表示当前没有设备使用这个模块。如果你在加载模块之前设备已经存在probe 应该已经触发过lsmod里会显示1或更大。这个数字是“正在使用模块的设备数”很有参考价值。4.3 如何在 dmesg 里“看到”驱动被调用加载成功后立刻查看内核日志dmesg | tail -20如果一切正常你会看到类似这样的输出[ 12.345678] pci_mini: probe device 1234:5678, class 058000[12.345678]是内核启动后的时间戳单位秒pci_mini:是模块名后面是本模块打印的内容。它告诉你内核在 12 秒左右枚举到匹配设备并调用了 pci_mini 的 probe 函数。这一刻你的驱动已经“跑起来”了。如果dmesg里什么都没有先不要慌继续往下看。除了 dmesg另一个重要的验证入口是 sysfs。执行ls /sys/bus/pci/drivers/pci_mini/正常情况下会看到bind、uevent、unbind等文件还会有一个以0000:00:04.0格式命名的符号链接指向实际的 PCI 设备节点。这个符号链接出现才说明驱动和设备已经绑定成功。4.4 从 dmesg 读配置空间匹配预期数值你可能注意到上面 probe 打印的信息其实都来自配置空间。PCIe 协议规定每个设备都有 4KB 的配置空间传统 PCI 是 256 字节其中的 vendor ID 位于偏移 0x00device ID 位于偏移 0x02class code 位于偏移 0x0B。内核在枚举设备时已经把配置空间读出来并填充到struct pci_dev里。所以你在驱动里访问dev-vendor和dev-device实际上就是在读配置空间的这两个字段。如果要读取配置空间里其他字段可以用pci_read_config_word/dev/dword系列函数例如u16 status; pci_read_config_word(dev, PCI_STATUS, status); pr_info(PCI status: 0x%04x\n, status);这在后续做设备诊断时会很常用。5. 踩坑实录为什么我的 probe 没被调用写代码最痛苦的不是编译报错而是“编译过了、模块加载了但 probe 没被调用”。这就像你约了人见面结果对方压根没来。整个系统没有任何报错但你预期中的日志就是没出现。我在带新人入门时几乎每个新手都会遇到这个问题。下面这几个坑我一个个说顺序按照从低到高的排查频率排。5.1 坑位一id_table 写错匹配不上这是最高频的问题。id_table里的 vendor ID 或 device ID 和设备实际的 ID 对不上probe 自然不会被触发。排查方法很简单lspci -nn看目标设备的 ID 是什么然后和黄代码里的PCI_DEVICE()参数比对。如果你是在 QEMU 里记得用-device edu或相应设备不同 QEMU 版本对 edu 设备的支持并不一致有的版本需要显式编译 edu 设备支持否则启动参数里会被忽略。一个小细节lspci -nn输出里方括号中的第一个数是 vendor ID第二个是 device ID。不要看反了。我有一次调试了半小时最后发现是把 device ID 和 vendor ID 写反了导致驱动永远匹配不上。5.2 坑位二设备已经被其他驱动 claim 了就算id_table没错probe 还是可能不会被调用因为设备可能已经被其他驱动占用了。在真实系统里网卡会被igb、e1000e等驱动接管GPU 会被amdgpu/nouveau接管。内核的 driver model 规定一个设备同一时刻只能绑定一个驱动。验证方法ls -l /sys/bus/pci/devices/0000:00:04.0/driver如果这个符号链接存在说明设备已经被某个驱动绑定。比如输出指向virtio_net那你的pci_mini驱动再想绑定就会失败。解决方案有两个方案一把当前驱动解绑再绑到你的驱动。方案二用driver_override强制指定驱动。方案二更干净。具体做法echo pci_mini /sys/bus/pci/devices/0000:00:04.0/driver_override echo 0000:00:04.0 /sys/bus/pci/drivers/pci_mini/bind这会强制设备绑定到pci_mini驱动无视原有驱动。但要注意如果原驱动已经占用了设备你需要先解绑echo 0000:00:04.0 /sys/bus/pci/devices/0000:00:04.0/driver/unbind在虚拟机里测试这个流程非常安全。真实机器上操作的话请先确认对应设备不是系统盘或关键网络设备否则会直接断连。5.3 坑位三模块加载顺序与构建依赖如果你用的是modprobe而不是insmod可能会出现模块加载成功但没有任何 probe 调用的情况。原因在于modprobe根据modules.dep文件决定加载顺序。如果你编译的模块没有被正确安装到/lib/modules/$(uname -r)/extra/下modprobe可能找到的是旧版本模块。解决办法sudo make modules_install sudo depmod -a sudo modprobe pci_minimodules_install会把模块复制到内核模块目录depmod -a重新生成依赖文件这样 modprobe 才能真正找到你的模块。如果你一直用insmod可以跳过这步但在开发后期建议还是走一遍养成好习惯。5.4 坑位四虚拟机的 PCIe 枚举机制差异QEMU 默认模拟的 PCI 拓扑比较简单可能只有一条总线、零个或多个设备。如果你在启动参数里添加了设备但虚拟机里lspci看不到那就是 QEMU 的参数没生效。常见原因设备名拼写错误比如把edu写成了edu1。设备需要额外的参数比如有的 QEMU 设备要指定addr。设备不支持当前机器类型。有一个小技巧启动参数里加上-trace eventspci*可以在 QEMU 日志里看到 PCI 枚举相关的信息可以帮助确认设备是否真的被添加。不过大多数情况下lspci看不到就直接说明参数问题先检查启动脚本。5.5 排查方法论层层剥洋葱当你发现 probe 没被调用时不要急着改代码。按照下面这个顺序排查能省下大量时间模块加载了吗lsmod | grep pci_mini。模块加载后dmesg有没有报错重点关注Unknown symbol、init_module错误。设备被枚举了吗lspci -nn里有没有目标设备。设备被其他驱动绑定吗检查/sys/bus/pci/devices/*/driver。id_table 和你lspci看到的 ID 一致吗你用的内核版本和编译头文件版本一致吗按照这个顺序排查90% 的问题在 10 分钟内定位。很多新手会跳过“设备有没有被枚举”这步直接用lspci里没设备来怀疑驱动方向就偏了。6. 从最小驱动到 AI Infra 场景下一步可以怎么扩展能跑的最小驱动只是起点。在 AI Infra 方向PCIe 驱动最终要面对的是 GPU、NPU、RDMA 网卡、NVMe SSD、FPGA 加速卡。这些设备动辄数十 GB 的带宽、数千个中断、复杂的 DMA 描述符但它们的驱动起点都是一样的先有一个能 probe、能读写配置空间的骨架。所以当你跑通今天这个最小驱动后接下来的路会非常清晰。6.1 读 BAR 映射与 MMIO 访问BARBase Address Register是 PCIe 设备暴露给软件的内存地址窗口。设备通过 BAR 把寄存器映射到系统的物理地址空间里CPU 可以直接通过 MMIO 访问这些寄存器。驱动开发中probe 之后的第一件事通常就是读取 BAR 并做映射。内核提供了pci_ioremap_bar()它把 BAR 对应的物理地址映射为内核虚拟地址。之后就可以用ioread32()/iowrite32()读写寄存器。以我调试过的某款加速卡为例它的控制寄存器组就映射在 BAR0 里几行代码就能让硬件进入初始化状态。6.2 中断处理与 MSI-XPCIe 设备最常见的性能利器是 MSI-X 中断。相比传统 INTx 引脚中断MSI-X 允许设备把中断报文直接写入内存不仅减少了 CPU 开销还能把不同队列的中断路由到不同 CPU 核心上这对 AI 训练场景特别重要。驱动里启用 MSI-X 的核心函数是pci_alloc_irq_vectors()它和设备文件配合让驱动申请指定数量的中断向量。熟悉这项技术后你会发现网卡的多队列、GPU 的完成通知机制本质上都是 MSI-X 的变体。6.3 字符设备接口与用户态交互驱动最终要服务用户态程序。PCIe 驱动通常通过字符设备接口暴露给用户态比如/dev/accel0。你用cdev_add()注册一个字符设备用.read、.write、.mmap、.ioctl等 file_operations 提供访问入口。用户态进程可以打开设备节点通过 mmap 把设备的 BAR 空间映射到进程地址空间零拷贝地操作硬件。很多 AI 框架的运行时就是这么工作的用户态通过 ioctl 下发任务描述符内核驱动解析后配好 DMA硬件完成后通过中断通知内核内核再唤醒等待中的用户态进程。这套链路里驱动部分的开端就是今天这个最小骨架。6.4 DMA 与加速器场景DMA 是 PCIe 驱动最核心的机制。简单说DMA 让设备绕过 CPU 直接读写内存这是高速数据传输的基础。Linux 内核提供了一套dma_alloc_coherent()/dma_map_single()的 API帮助驱动分配 DMA 友好的内存并在设备地址和 CPU 地址之间做映射。比如挂载在 PCIe 上的 AI 加速卡通常需要把输入数据从 CPU 内存搬运到设备显存。驱动需要先分配一块 DMA 缓冲区把用户态的数据拷进去然后告诉设备“数据在 bus address 0xXXXXXXXX”硬件就自己搬了完成后再发中断通知。这个设计模式几乎适用于所有高性能 PCIe 设备。写到这里我的 Air 里的虚拟机还在跑着pci_mini模块依然绑定在那个虚拟设备上。我个人的体会是不要小看这个 50 行的驱动它把 PCIe 驱动开发里最容易让人困惑的“匹配-绑定-回调”链路整个打通了。我当年第一次写这个驱动时卡在 id_table 和 probe 之间半天最后发现是 device ID 看反了。所以如果你的第一批代码也遇到类似问题内心不必崩溃这才是驱动开发最真实的样子。你只需要记住一件事PCIe 驱动没有黑魔法一切都是有迹可循的dmesg就是你最可靠的向导。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。