CPU 和 PCIe 设备对话只有两条路:CPU 主动找设备——通过 BAR 映射的 MMIO 窗口读写设备寄存器;设备主动找 CPU——用 DMA 直接读写内存,完事发个中断说"搬完了"。本关回答这三件事在内核里的真实形状:BAR 怎么变成 CPU 能访问的地址,DMA 的 ownership 契约是什么,中断怎么从"广播引脚"进化成"点对点消息"。
三个机制支点:pci_resource_start / pcim_iomap(BAR → MMIO)、dma_map_single / dma_alloc_coherent(streaming vs coherent 的所有权契约)、MSI/MSI-X(中断即内存写)。M08 讲了 CPU 收到中断后怎么处理,本关讲设备那一头的发起侧;M15 的缓存一致性会在这里以 dma_sync_* 的形式再出现一次。
PCI 配置空间里,每个 function 有 6 个标准 BAR(#0–#5)——include/linux/pci.h 里就是这么编号的(PCI_STD_RESOURCES 一组)。BAR 里写的是设备寄存器窗口的物理地址和长度,枚举时内核已经分配好。驱动要访问,先拿地址:pci_resource_start(dev, bar)(宏,本体就是 pci_resource_n(dev, bar)->start),再映射进内核地址空间:pcim_iomap(pdev, bar, maxlen)——managed 版本,driver detach 时自动 unmap,少写一段 cleanup。
映射回来的是 void __iomem *——注意那个 __iomem 注解:sparse 靠它抓"直接解引用 MMIO 指针"的 bug。读写必须用 readl / writel 系列:它们保证访问宽度和顺序语义。直接 *(u32 *)regs 解引用是错的:编译器可能合并/拆分访问(M18 的主题),CPU 也可能重排(M16 的主题)。
铁证:MSI-X 的中断向量表就住在 BAR 里。msix_map_region()(drivers/pci/msi/msi.c)从配置空间读出 PCI_MSIX_TABLE,BIR(BAR Index Register)指出表在第几个 BAR,phys_addr = pci_resource_start(dev, bir) + table_offset 再 ioremap。设备寄存器和中断表住在同一个地址空间里——这就是"用内存地址对话"。
DMA 的本质:设备绕过 CPU 直接读写内存。内核 DMA API(include/linux/dma-mapping.h,kernel/dma/mapping.c)管两件事:给设备一个它能用的地址(dma_addr_t),以及管住缓存一致性。两套 API,对应两种契约:
dma_map_single()(宏,调 dma_map_single_attrs(..., 0))、dma_map_sg()。驱动的内存平时是 CPU 的,DMA 期间所有权移交设备:dma_map_sgtable() 的注释写得直白——映射成功后 buffer ownership 转到 DMA domain,CPU 要再碰,必须先 dma_sync_single_for_cpu() 或 dma_unmap_*()。映射后偷读/偷写 CPU 侧 buffer,是 correctness bug,不是性能问题。dma_alloc_coherent()(包 dma_alloc_attrs())。分配出来就保证 CPU–设备缓存一致,驱动不用 sync。但注意边界:coherent 解决的是缓存可见性,不是操作顺序——"描述符写完再敲 doorbell"这个顺序,驱动自己要用 barrier 保证,coherent 不包这个。怎么选:描述符环、队列这种小块高频、CPU 和设备来回碰的控制结构 → coherent;收发包的大 buffer、"CPU 填好 → 设备搬走 → CPU 不再碰"的一次性搬运 → streaming。选错的代价:streaming 忘了 sync = 读到 stale 数据;coherent 乱用 = 白付 uncached 访问的性能税。
include/linux/dma-mapping.h 的注释写得清楚:dma_addr_t 是设备能用的 DMA/bus 地址,不一定等于 CPU 的物理地址,两者之间可能有转换。原因有二:设备位宽可能盖不住内存(32 位设备 + 64GB 内存),以及 IOMMU。
kernel/dma/mapping.c 的 dma_map_phys() 是个三岔路口:direct 映射、IOMMU(use_dma_iommu(dev) → iommu_dma_map_sg())、或设备的 dma_map_ops。IOMMU 这层(drivers/iommu/dma-iommu.c,文件头自称"DMA-API 到 IOMMU-API 的通用 glue layer"):给设备一个独立的 IOVA 地址空间,把设备看到的 IOVA 翻译成物理页,带 IOTLB flush 队列。两个后果:第一,隔离——设备只能碰映射给它的 IOVA,直通物理地址等于给设备整台机器的读写权;第二,连续性——IOMMU 能把不连续的物理页拼成连续的 IOVA,省掉 bounce buffer。驱动侧的声明是 dma_set_mask_and_coherent():"我这设备只能看 N 位地址",DMA 层据此选路。
传统 INTx:4 根边带引脚(INTA–INTD,PCI_NUM_INTX = 4,见 include/linux/pci.h 的 pci_interrupt_pin 枚举),电平触发、多设备共享一根线,来了中断 CPU 还要逐个问"是谁"。MSI 把中断变成一次普通的内存写:设备往约定的 (address, data) 写一次 = 一个中断。点对点、无共享、消息里天然携带向量号——"别广播,点对点叫我"就是这个意思。
内核侧(drivers/pci/msi/msi.c):
pci_msi_vec_count() 告诉你设备能要几个向量——最多 32(2^5),且按 2 的幂分配(MSI 规范就这么定的,函数注释里写了);msi_capability_init() 走完,pci_msi_set_enable() 置位。msix_capability_init() 的顺序是门道:先 MASKALL|ENABLE(注释:"Mask all the vectors to prevent interrupts coming in before they're fully set up"),映射表、建 IRQ domain、写 entry,最后 msix_mask_all() 把 stale entry 全 mask 掉(注释:"prevent stale entries from firing in a crash kernel"),再清 MASKALL。pci_write_msg_msix():按规范先 mask 该 entry,writel 写 address/data,最后 readl 把写 flush 出去——改表时设备可能正在看,不 mask 就会发出去半更新的中断消息。使能 MSI/MSI-X 的第一件事都是 pci_intx_for_msi(dev, 0):关掉 INTx。一山不容二虎,MSI 和边带引脚互斥——这是替代,不是补充。
PCI 枚举在启动期建好每台设备的 pci_dev(vendor / device / class,见 struct pci_dev)。驱动装配走 drivers/pci/pci-driver.c:__pci_device_probe() → pci_match_device() 拿驱动的 ID 表逐项匹配 → 命中 → pci_call_probe() → local_pci_probe() → 终于调到 pci_drv->probe(pci_dev, id)。
probe 里驱动按固定顺序装配:使能设备的 PCI 命令寄存器(Memory Space / Bus Master)→ pci_resource_start + pcim_iomap 拿寄存器 → dma_set_mask_and_coherent() 声明地址能力 → 分配 MSI/MSI-X 向量(2.4 那套)→ 建 DMA 描述符环(2.2 选 coherent)→ 注册到子系统。任何一步失败都要按逆序回滚——这就是 pcim_(managed)存在的原因。
为什么 DMA 要分 streaming 和 coherent,而不是统一一种?
因为代价模型不同,没有免费午餐。coherent 要求内存永远对设备可见——要么 uncached(每次 CPU 访问都慢),要么硬件维护一致性;好处是驱动不用操心 sync,适合描述符环这种"双方高频来回碰"的控制结构。streaming 允许驱动用 cacheable 内存全速访问,代价是驱动必须遵守 ownership 契约:映射期间 CPU 别碰,收回前先 sync/unmap——dma_map_sgtable 注释把这条写成了 correctness 条件。统一成一种,要么所有 DMA 内存都付 coherent 的税,要么所有驱动都背 ownership 的心智负担。分开是按需付费。
为什么 MSI/MSI-X 要取代 INTx?
INTx 是边带引脚:最多 4 根、设备间共享、电平触发、CPU 收到后还要轮询"是你吗"。MSI 把中断变成内存写:点对点(地址里天然带目标)、无共享、消息里自带向量号;MSI-X 更进一步——表放 BAR 里、每个向量独立 mask、独立 affinity,多队列网卡/NVMe 的每个队列可以有自己的中断亲缘性。铁证是互斥性:内核使能 MSI/MSI-X 时第一件事就是 pci_intx_for_msi(dev, 0) 关掉 INTx——新旧两套机制不能共存,说明这是替代不是补充。
为什么 dma_addr_t 不直接用 CPU 物理地址?
因为设备和 CPU 不一定活在同一个地址视角里:32 位设备看不见高位内存;隔离要求设备看不见整台机器。dma_set_mask_and_coherent() 让驱动先声明"我能看多少位",DMA 层在 direct / IOMMU / ops 三条路里选。IOMMU 的 IOVA 既是翻译(拼连续、降位宽)也是保安(设备只能碰自己的 domain)。直通物理地址等于把整台机器的读写权交给一块 PCIe 卡——性能上偶尔行,安全上永远不行。
本关是"设备侧"的发起端,和几关拼成完整链条:
dma_sync_single_for_cpu / dma_sync_single_for_device 是驱动手写的一致性维护——MESI 管 CPU 之间,这里管 CPU 和设备之间。pci_dev 和 BAR 是 M02 那条时间线长出来的果实。出题原则:10 题中记忆性题目不超过 3 道,其余为应用/推理/权衡类——场景预测、设计 trade-off 比较、从代码路径找问题、解释"为什么这样设计而不是那样"。难度递进,最后两道需要想一会儿。
BAR(Base Address Register)是 PCI 配置空间里声明的设备寄存器窗口:每个 function 有 6 个标准 BAR(#0–#5),存的是窗口的物理地址和长度。pci_resource_start(dev, bar) 返回该 BAR 窗口的物理起始地址(宏本体就是 pci_resource_n(dev, bar)->start)——注意它还不是 CPU 能直接访问的地址,还要经过 pcim_iomap / ioremap 映射。
三层理由。编译器:直接解引用会被当成普通内存访问,编译器可能合并、拆分或重排(M18 的主题);CPU:MMIO 访问需要确定的宽度和顺序,readl/writel 在需要的架构上带 barrier 语义;类型系统:__iomem 注解就是用来让 sparse 抓"直接解引用 MMIO 指针"的 bug 的。设备寄存器有副作用(读清中断、写触发动作),"看起来像内存"不等于"可以当内存用"。
可能读到 stale 数据(设备刚写完、还在 cache 或设备侧没回写),或者更糟——破坏 DMA 传输。原因是 ownership 已经移交:dma_map_sgtable() 的注释写得直白,映射成功后 buffer ownership 转到 DMA domain。映射期间 CPU 侧的 buffer 对驱动来说是"别人的内存",要再碰必须先 dma_sync_single_for_cpu() 或 dma_unmap_*() 收回。这不是性能建议,是 correctness 条件。
收包 buffer → streaming:大块、单向、一次性——CPU 准备好 → dma_map_single → 设备填包 → 中断 → dma_unmap/sync 后 CPU 读。用 cacheable 内存全速访问,只在交接点付 sync 代价。描述符环 → coherent:小块、高频、CPU 和设备来回碰(生产/消费索引、状态位),每次 sync 的开销和心智负担都受不了,dma_alloc_coherent 一次分配、之后不用管缓存。但记住 coherent 的边界:它只解决可见性,"描述符写完再敲 doorbell"的顺序仍要驱动自己用 barrier 保证。
dma_addr_t 是设备视角的 DMA/bus 地址,phys_addr_t 是 CPU 视角的物理地址——include/linux/dma-mapping.h 的注释明确说两者之间可能有转换。数值相等的典型情况:direct 映射 + 无 IOMMU + 设备位宽覆盖全部内存,这时 dma_map_phys() 走 direct 分支,dma_addr_t 就是物理地址。有 IOMMU 时设备看到的是 IOVA,数值一般不相等——这也是"不能把 dma_addr_t 当物理地址传给 CPU 侧函数"的原因。
两个理由都写在源码注释里。先置 MASKALL:"Mask all the vectors to prevent interrupts coming in before they're fully set up"——表还没写完、IRQ domain 还没建好,这时来中断就是野中断。最后清 MASKALL 之前先 msix_mask_all() 逐个 mask:"prevent stale entries from firing in a crash kernel"——kexec 进 crash kernel 时,旧内核写过的 entry 还残留在表里,不清掉会误触发。顺序即正确性:先全部屏蔽 → 慢慢装配 → 确认干净 → 一次放行。
dma_set_mask_and_coherent() 声明"我只能看低 4GB"后,dma_map_phys() 的三岔路口据此选路:有 IOMMU 时可以走 IOMMU 分支,把任意物理页重映射进 32 位 IOVA 窗口;没有 IOMMU 且 buffer 落在 4GB 之上时,映射会失败(或走 bounce,取决于配置)。所以驱动必须检查每次 dma_map_* 的返回值,失败就要回滚——"声明掩码"不是告诉内核"我能处理",而是让 DMA 层知道"超过这个范围的地址给我就是错的"。
共享:INTx 是 4 根边带引脚(INTA–INTD),多设备硬共享一根线;MSI 是内存写,每条消息独立,不存在"共享一根线"的概念。识别:INTx 是电平信号,CPU 收到后只知道"这根线上有设备在叫",必须逐个轮询设备"是你吗";MSI 的消息里自带向量号(data 字段),写到哪个 address 就发给哪个 CPU/APIC——身份信息在消息里,不在轮询里。这就是"别广播,点对点叫我":从"一根线大家喊"变成"每人一封写好收件人的信"。
因为 MSI-X 表在 BAR 里,设备一直在看这张表。更新一个 entry 要写三次(address_lo、address_hi、data),不是原子的——如果设备在 address 写完、data 还没写时触发中断,就会按新地址 + 旧 data(或反过来)发出去:中断被送到错误的 CPU、带错误的向量号。pci_write_msg_msix() 按规范先 mask 该 entry(vector_ctrl 置 MASKBIT),写完三件套、readl 确保 flush,再 unmask。改共享表的第一件事永远是让读者先别看。
选 MSI-X。MSI 最多 32 个向量且按 2 的幂分配,16 队列勉强够数,但 MSI 的向量共享一个 data 模式、mask 粒度粗,更重要的是 affinity 控制不如 MSI-X 精细。MSI-X 每个向量是 BAR 表里独立的 entry:独立 address/data(可指向不同 CPU)、独立 mask 位、独立 affinity——16 个队列配 16 个向量(+1 个管理队列),每个队列的中断亲缘性绑到处理该队列的 CPU,NUMA 本地化。INTx 不行:只有 4 根共享引脚,16 个队列挤 4 根线,中断一来先要轮询"是哪个队列",延迟和扩展性全崩——这正是 MSI-X 被发明出来要解决的场景。
dma_map_phys 在 direct / IOMMU / ops 三条路里选dma_addr_t 的注释必读一次 DMA 往返(streaming):
CPU 填 buffer → dma_map_single() → dma_addr_t 交给设备
→ 设备直接读写内存(CPU 别碰!ownership 已移交)
→ 设备写 MSI-X entry → 中断到达 CPU
→ 驱动 dma_sync_single_for_cpu() / dma_unmap_*() 收回
→ CPU 读结果
记住:sync/unmap 是"收回所有权"的仪式,不是性能优化。
MSI-X entry(三件套,住在 BAR 里):
+0x00 address_lo 中断消息写往哪个地址(目标 CPU/APIC)
+0x04 address_hi
+0x08 data 向量号等消息内容
+0x0C vector_ctrl MASKBIT:改表时先 mask,改完再开
表的位置:PCI_MSIX_TABLE 配置字 → BIR 指出第几个 BAR
→ pci_resource_start(dev, bir) + offset → ioremap
中断演进史:
INTx 4 根边带引脚,共享,电平触发,CPU 靠轮询认人
MSI 中断 = 一次内存写 (address, data),≤32 向量(2^5),
点对点,使能即关 INTx
MSI-X 表放进 BAR,每个向量独立 entry / 独立 mask /
独立 affinity —— 多队列设备的答案
在页面里每题写下你的答案(关键词即可),卡住的题点一下"卡住了"标记,
点"生成作答汇总"后复制、贴到聊天里——对错由我来判定,我逐题批改并据此
调整下一关的深浅;准备好了说"下一关"。
点下面按钮生成汇总文本,复制后贴到「内核二十关」侧聊里,对错由我判定。