3. PCIe读写机制

要理解PCIe的性能基准测试,必须先看清PCIe在链路上到底“传了什么”。PCIe不是一条简单的并行总线,而是一条基于数据包的分层串行互连:上层产生事务,事务被封装成TLP(Transaction Layer Packet,事务层包),再经数据链路层和物理层发送。读写性能的差异,本质上来自事务类型、包结构以及是否需要等待完成包。

一、核心原理:请求与完成包构成一次读写闭环

PCIe的内存读写以“请求—完成”模型工作。发起方(Requester)发出请求TLP,目标方(Completer)处理后返回完成TLP。对于读操作,请求包只携带地址和长度,数据必须由完成包带回;对于写操作,数据直接随请求包发出,通常不需要返回数据。

因此,读事务的链路往返次数更多,延迟天然高于写事务。基准测试中若只测吞吐量而不区分读写比例,很容易掩盖读路径的瓶颈。

典型读写事务的关键字段:Requester ID、Tag、Address、Length、TC、Attr、Byte Enable。其中Tag用于匹配请求与完成包,是读事务并发度的核心资源。

二、TLP结构:从头部到数据负载

一个TLP由头部(Header)、可选数据负载(Data Payload)和可选TLP Digest组成。头部长度分为3DW和4DW两种,64位地址必须使用4DW头部。头部中的Fmt和Type字段决定事务类型,Length字段表示数据长度,以DW为单位。

TLP 基本结构:
+--------+--------+----------------+--------+--------+
| Header | Data   | TLP Digest     | LCRC   | Framing|
| 3DW/4DW| 0~N DW | 可选           | 4 Byte | 序列   |
+--------+--------+----------------+--------+--------+

Memory Read Request  : Fmt=001/011, Type=00000, 无数据
Memory Write Request : Fmt=010/011, Type=00000, 带数据
Completion           : Fmt=000/010, Type=01010, 可带数据

读请求的Length字段表示要读取的DW数量,最大受Max Payload Size和Max Read Request Size限制。写请求的Length必须与实际数据负载一致。完成包中则包含CplD状态、Byte Count、Lower Address等字段,用于指示返回了多少数据以及是否成功。

三、Posted与Non-Posted事务的区别

这是PCIe读写机制中最关键的分类。Posted事务指请求发出后不需要等待完成包,发起方可以继续发送后续事务;Non-Posted事务则必须等待完成包才能确认操作结束。

对比项 Posted事务 Non-Posted事务
典型代表 Memory Write、Message Memory Read、I/O Read/Write、Config Read/Write
是否等待完成包 不需要 必须等待
数据方向 随请求发出 读数据由完成包返回
延迟特征 低,流水线友好 高,存在往返延迟
对性能测试影响 吞吐易做高 受Tag数量、RTT、MPS影响明显

写操作是Posted的,所以写吞吐通常更容易接近链路带宽;读操作是Non-Posted的,必须等完成包回来,因此读延迟和读吞吐对系统参数更敏感。测试中如果发现“写快读慢”,首先要检查的就是读请求的并发度和完成包返回路径。

四、方法流程:一次读事务的完整路径

  1. CPU或DMA引擎发起Memory Read Request,填写地址、长度和Tag。
  2. 请求TLP经过Root Complex、Switch,到达目标Endpoint。
  3. Endpoint解析请求,从本地内存或设备缓冲中取数据。
  4. Endpoint生成Completion TLP,携带数据和CplD状态返回。
  5. 发起方根据Tag匹配完成包,确认数据完整后释放Tag资源。

在这个过程中,Tag数量决定了同一时间能有多少个未完成读请求。Tag耗尽后,新的读请求必须等待,形成读吞吐上限。这也是为什么高并发读测试中,增大Tag池或使用多个Requester能显著提升性能。

五、案例场景:NVMe SSD顺序读测试

以NVMe SSD顺序读为例,主机通过PCIe向SSD发出大量Memory Read Request,SSD返回Completion TLP。若测试工具只开一个队列、一个Tag,读带宽可能只有链路峰值的很小一部分;当队列深度和Tag数量增加后,读带宽才会逐步逼近上限。

# 使用 fio 观察不同队列深度下的读带宽
fio --name=read --rw=read --bs=128k --iodepth=1  --filename=/dev/nvme0n1
fio --name=read --rw=read --bs=128k --iodepth=32 --filename=/dev/nvme0n1
fio --name=read --rw=read --bs=128k --iodepth=128 --filename=/dev/nvme0n1

如果iodepth从1提升到32时带宽明显上升,说明瓶颈在未完成读请求数量,而不是链路本身。此时应进一步检查Max Read Request Size、Max Payload Size和Completion超时设置。

六、误区与复盘

常见误区一:把写带宽当作PCIe真实能力。写是Posted事务,容易跑满,但不能代表读性能。
常见误区二:忽略完成包开销。读事务的完成包同样占用链路带宽,实际有效读吞吐要扣除完成包头部和LCRC等开销。
常见误区三:只看平均带宽,不看延迟分布。Non-Posted事务的尾延迟往往比平均延迟更能暴露问题。

复盘时建议同时记录:请求TLP数量、完成TLP数量、平均完成延迟、Tag使用率、MPS/MRRS配置。只有把“请求—完成”闭环中的每一段都量化,才能判断性能瓶颈究竟在发起端、链路还是目标端。

PCIe 读写事务:Posted 与 Non-Posted 路径对比 Requester CPU / DMA / RC Completer Endpoint / SSD PCIe Link(TLP 传输通道) Posted 事务:Memory Write Write Request TLP(含数据)→ 无需完成包 Non-Posted 事务:Memory Read Read Request TLP(仅地址+Tag) Completion TLP(含数据+CplD) Tag 匹配机制:Requester 用 Tag 关联 Read Request 与 Completion Tag 数量决定未完成读请求并发度;Tag 耗尽 → 读吞吐受限

掌握上述机制后,后续做PCIe读写基准测试时,就能有意识地控制读写比例、队列深度、Tag数量和包大小,而不是只盯着一个带宽数字。