3. PCIe读写事务机制

PCIe 的性能问题,很少是“链路带宽不够”这么简单。真正决定一次读写快慢的,是事务层如何把请求拆成 TLP、如何等待 Completion、以及配置空间访问为何天然比内存访问更慢。理解这一层,才能解释为什么同样的链路宽度,不同驱动的吞吐能差出数倍。

一、核心原理:四类事务与 TLP 结构

PCIe 事务层定义了请求者(Requester)与完成者(Completer)之间的交互。按访问目标划分,最常用的是四类:

  • Memory Read (MRd):读目标设备的内存或 BAR 空间,属于 Non-Posted 事务,必须等待 Completion 才能拿到数据。
  • Memory Write (MWr):写目标内存,属于 Posted 事务,发出后不等待确认,因此写吞吐通常高于读。
  • Configuration Read (CfgRd):读取配置空间,用于枚举和寄存器访问,同样是 Non-Posted。
  • Configuration Write (CfgWr):写配置空间,也是 Non-Posted,必须等 Cpl 返回。

每个 TLP 由 Header、可选 Data Payload 和可选 ECRC 组成。Header 中最关键的是 Fmt/Type 字段(决定事务类型与是否带数据)、Requester ID、Tag、Length 和地址字段。Tag 是读请求的“身份标识”,Completion 靠它匹配回原请求;Length 决定一次能搬多少字节,直接影响效率。

PCIe 事务层读写机制与 TLP 交互 Requester CPU / RC / EP 发起 MRd / MWr CfgRd / CfgWr Completer 目标 EP / Switch 处理请求 返回 CplD / Cpl TLP 结构 Header Data Payload ECRC(可选) Fmt/Type · Requester ID · Tag · Length · Address Tag 用于匹配 Completion,Length 决定搬运量 ① Memory Read Request (MRd, Non-Posted) ② Completion with Data (CplD, 携带 Tag 匹配) ③ Memory Write (MWr, Posted, 无需等待) ④ Configuration Read/Write (CfgRd/CfgWr, Non-Posted) ⑤ Completion (Cpl / CplD) Non-Posted 请求 Posted 请求 Completion 返回

二、方法流程:一次读事务的完整生命周期

以一次 Memory Read 为例,流程可以拆成五步:

  1. 驱动或 CPU 发起读请求,RC 生成 MRd TLP,填入 Requester ID、Tag、目标地址和 Length。
  2. TLP 经 Switch 路由到目标 EP,EP 解析 Header,判断是命中 BAR 空间还是内部寄存器。
  3. EP 准备数据,生成 CplD TLP,把原请求的 Requester ID 和 Tag 原样带回。
  4. Completion 沿原路径返回 RC,RC 用 Tag 匹配挂起的请求,把数据交给上层。
  5. 若数据量超过单次 Length,则拆成多个 TLP 或多次请求,形成流水线。
关键点:读延迟 = 请求传输 + 目标处理 + Completion 返回。写延迟几乎只有请求传输,因为 Posted 写不需要等 Cpl。这就是“读比写慢”的根本原因。

三、案例/场景:配置空间访问为何拖慢枚举

系统启动时,BIOS 或 OS 会遍历总线号、设备号、功能号,逐个读取 Vendor ID、Device ID、BAR 等配置寄存器。每次 CfgRd 都是 Non-Posted,必须等 Cpl 返回才能发下一个。若拓扑中有多层 Switch,每层都增加转发延迟,枚举时间会明显拉长。

# 查看某设备配置空间(每次读都产生 CfgRd + Cpl)
lspci -s 01:00.0 -xxx
# 输出示例
01:00.0 Ethernet controller: Intel Corporation ...
00: 86 80 12 15 07 04 10 00 ...

# 查看链路状态与协商速率
lspci -vv -s 01:00.0 | grep -i "LnkSta"
LnkSta: Speed 8GT/s, Width x4

优化枚举的常见手段是减少不必要的配置读、合并访问,或利用缓存机制避免重复读取同一寄存器。对于性能敏感路径,应尽量用 Memory Read/Write 替代配置空间访问。

四、误区与复盘:Tag 耗尽与 Completion 超时

很多性能问题并非带宽不足,而是 Tag 资源被耗尽。每个 Non-Posted 请求占用一个 Tag,若驱动同时发出大量读请求而不及时回收 Completion,Tag 用尽后新请求只能排队,吞吐骤降。典型现象是:链路利用率不高,但读延迟飙升。

事务类型 是否 Posted 是否需 Completion 典型延迟 优化方向
Memory Write (MWr) 是 否 低 合并写、增大 Length
Memory Read (MRd) 否 是 高 预取、增大 Tag 深度
Configuration Write (CfgWr) 否 是 高 减少访问频次
Configuration Read (CfgRd) 否 是 高 缓存、批量枚举
常见误区:认为“加大 Length 就一定更快”。实际上 Length 受 Max Payload Size 限制,且过大的 Payload 会增加缓冲占用和延迟抖动。应结合 MPS、MRRS 和 Tag 深度综合调优。

复盘时建议按以下清单排查:

  • 确认当前 MPS 与 MRRS 设置是否匹配设备能力。
  • 检查是否有大量小 Length 读请求,考虑合并或预取。
  • 观察 Completion 是否出现 UR/CA 等错误状态,避免重试放大延迟。
  • 用性能计数器统计 Tag 使用率,判断是否成为瓶颈。
  • 区分配置空间访问与内存访问,避免在热路径中混用。

理解事务层机制后,后续的 MPS、MRRS、Relaxed Ordering 等优化手段才有落脚点。读写性能的差异,本质上由 Posted/Non-Posted 语义和 Completion 匹配机制决定。