PCIe基础学习4:事务类型与 Posted 语义

事务类型与 Posted 语义:读慢写快的根

PCIe 协议特性精讲 · 第 04 篇 | 基准:PCIe 5.0(Base Spec r5.0)| 内核:Linux v7.0.0
系列主线:39 篇,一篇一个协议特性——是什么、为什么、怎么工作、怎么测、坑在哪

03 篇把 TLP 的头字段拆完了:Fmt/Type 怎么判类型、Cpl 头里 Status 放哪、Tag 和 Requester ID 怎么配对,全讲透了。这一篇接着回答抓包时第一个冒出来的问题:这条 TLP 发出去,该不该等 Completion(完成报文)?该等的话,等回来的状态对不对?这个问题想明白了,你就能解释一个日常现象——为什么 PCIe 写比读快,还能在排障时一眼分清"设备拒绝了你"和"设备压根没搭理你"这两条完全不同的故障路径。

一、四个地址空间:事务往哪去

先立个总纲。PCIe 的事务按目标分成四个地址空间(数据出处:Base Spec r5.0):Memory、I/O、Configuration、Message。每一种事务都属于其中一个空间,往对应的地方去。四个空间的定位差别很大:

  • Memory(内存空间):数据搬运主力。读写都允许,目标是"映射到内存地址的位置"(memory-mapped location)。DMA、MMIO 寄存器访问全走这里,日常抓包九成以上是它。
  • I/O(IO 空间):老平台遗留物。x86 的 I/O 端口那套,目标是 I/O 映射位置。新设计基本不碰,规范对它的限制也最严:I/O 请求的 Length 恒为 1 DW、TC 必须是 0,想多传点数据都不行。说白了,它就是给古董设备留的口子。
  • Configuration(配置空间):控制面。设备枚举、BAR 分配、能力结构配置全靠它,承载 CfgRd/CfgWr(Type0/Type1 两种)。软件世界的"开机点名"就发生在这里。
  • Message(消息空间):信令面。从"事件通知机制到通用消息"(规范原文语义),中断、电源管理、错误上报、LTR 这类边带信号全并进 TLP 走,03 篇讲过的 INTx/PM/ERR 消息都归这。它不走地址路由(Vendor-Defined 消息例外),最常见的是发给根复合体或广播,是四个空间里最特别的一个。这个设计省掉了一大把引脚——PCI 时代中断、电源管理、错误上报都是独立的边带信号线,PCIe 全收编进消息 TLP 在链路上走,插槽的引脚数才压得下来。

承载关系一句话:Memory 和 I/O 空间由地址路由的请求(MRd/MWr、IORd/IOWr)承载,Configuration 由 ID 路由的配置请求承载,Message 由消息请求(Msg/MsgD)承载。类型、路由、头长这些 03 篇都拆过,这篇不重复,往后就看一件事——每种请求怎么处理,要不要等 Cpl。四个空间记熟了,抓包第一眼就能答两个问题:这条包去哪个空间、什么类型(Fmt/Type 判,03 篇讲过);剩下的就是本篇的主题:要不要等 Cpl、等回来什么状态。

二、三分类:等不等 Completion,一眼定

规范把事务按"是否需要 Completion 回报"分成三类:

  • Posted:发完就算完。请求离开 Requester 那一刻,事务就视为完成,永不收 Cpl。成员就两个:Memory Write(MWr)和全部消息(Msg/MsgD)。
  • Non-Posted(非 Posted):必须收 Cpl 确认状态。所有读请求(MRd/IORd/CfgRd0/1)、I/O 写(IOWr)、配置写(CfgWr0/1)、原子操作(AtomicOp)全在这边。
  • Completion(完成报文):Cpl/CplD 本身。它不是请求,是 Non-Posted 请求的答复,跟请求配成一对。

归属关系用一张表钉死:

请求类型怎么处理归属
MWr(内存写)无 Cpl,发完即走Posted
Msg / MsgD(消息)无 Cpl,路由方式看消息类型Posted
MRd(内存读)必回 Cpl:成功回一个或多个带数据的 CplD,出错回不带数据的 CplNon-Posted
AtomicOp(原子操作)必回 Cpl:Cpl 带目标位置的原始值Non-Posted
IORd / IOWr(IO 读/写)必回 Cpl:读成功带数据,写和失败的读不带数据Non-Posted
CfgRd / CfgWr(配置读/写)必回 Cpl:读成功带数据,写不带数据Non-Posted

(示意图,数据出处:Base Spec r5.0)

几个容易记混的点,单独拎出来:

  • 读一定 Non-Posted,写不一定。Memory Write 是 Posted,但 I/O 写和配置写是 Non-Posted——同样是"写",待遇完全不同。
  • AtomicOp 是"写"语义,却归 Non-Posted。FetchAdd/Swap/CAS 三型(编码 0 1100 / 0 1101 / 0 1110)都要改内存,但完成报文要带回操作前的原始值,软件拿它做无锁同步,必须等。
  • 消息全 Posted。中断、电源管理这些消息发完即走,谁都不等谁。

记住一句话口诀:读必等,Memory 写不等,IO/Config 写必等,消息不等

三、为什么这么分:Posted 免往返,就是写快的根

分类规则看着简单,背后的设计逻辑值得掰开讲,因为这直接回答"为什么 PCIe 写比读快"。

先看读。PCIe 用的是 Split Transaction(拆分事务)协议:请求和完成彻底分离,目标设备收到请求后,等自己就绪了再单独回一个 Completion。这比 PCI 总线时代强多了——老 PCI 处理慢目标用的是 wait-state 和延迟事务(retry),请求方得反复试探目标好了没;PCIe 不用试探,目标好了自己回。作为代价,一次读至少两个 TLP 在路上:请求往上游走,完成往下游回。而且一个读请求可能拆成多个 CplD 回来(载荷上限 4 KB,设备实际常用更小的 MPS),读的延迟就更明显了。

这个模型还带来一个附带好处:目标不用一次只伺候一个请求,可以同时收多个请求、分别响应;Requester 那边也能同时挂多个 outstanding 读,靠 Tag 区分谁是谁——这正是读吞吐的来源,也是 03 篇讲 Tag 宽度寄存器时说的"延迟带宽积"的出处。

(示意图,数据出处:Base Spec r5.0)

再看写。Memory Write 被设计成 Posted,意思是:请求离开 Requester 的那一刻,写就视为完成了。讲 Posted 的语义,业界常用一个邮局比喻,很贴切:寄信就是把信投进邮筒,投进去你就当它送到了,不会站在邮筒边等回执。MWr 发完即走,不占往返时间,也不占返回方向的带宽——返回方向全留给读的 CplD。这就是写快的根:读是"一来一回 + 可能拆包",写是"单程票"

代价呢?错误不可见。信可能寄丢,MWr 也可能根本没到、或者到了但写失败,Requester 一概不知道——没有 Cpl 可等。错误只能靠 AER 消息兜底:对端发现异常,发一条 ERR_ 消息上来,软件再查。这个"发完即走、错了后补"的取舍,规范认为值得:写失败的概率小,为它牺牲每次写都要等确认的时延,不划算。这坑我见得多了——抓包看到 MWr 后面没有 Cpl,先别当故障报,那是设计如此

那为什么 I/O 写和配置写非要 Non-Posted?因为这两类写影响设备行为:改一个配置寄存器、踢一次 I/O 端口,设备接下来的行为就变了。软件必须确认"到了、没报错",才能放心走下一步。这层意思说白了:内存写可以接受"数据迟早会到",I/O 写不行——下一步动作必须等确认到达之后才能做。配置写错了,设备行为全乱;I/O 写丢了,外设状态错位,后面所有读写全跟着错。控制面的事,慢一点也要确认。这是控制面的纪律:快留给数据面,确认留给控制面。这么设计是有原因的。

还有一条很多人不知道的义务在接收端:Posted Request Acceptance Limit——正常工况下,Endpoint 收到 Posted 请求后必须在 10 μs 内接受并返还对应的流控信用。Posted 快不是发送方单方面快,接收端也不能赖着不收,两边都有规矩。

四、Completion 语义:回来的完成报文怎么读

Non-Posted 请求发出去了,Cpl 回来,怎么判断对不对?三个层次:状态对不对、是不是回给我的、回齐了没有。

第一层:Status 状态值。 03 篇讲过 Cpl 头里 Status 字段的位置,这里讲语义——什么场景该回什么值:

  • SC(成功):正常完成。读带数据(CplD),写不带数据。
  • UR(不支持请求):地址没命中 BAR、请求类型不支持、路由出错。抓包看到 Cpl(UR),先查地址归属和类型支持,这是"被明确拒绝"。
  • CRS(配置重试):目标设备忙,让软件过会儿再读。条文上它只允许出现在配置请求的完成里,实践中基本只见于配置读(CfgRd)——CRS 出现在非配置请求的响应里属于非法,接收方可选上报 Malformed。
  • CA(Completer Abort,被拒):请求被 Completer 终止(比如目标功能不可用),跟 UR 的区别在语义上,抓包判读看 AER 报的是哪一类。

第二层:是不是回给我的。 完成报文的关联键是 Requester ID + Tag:Completer 回 Cpl 时原样带回请求里的这对值,Requester 拿它找自己挂起的未完成请求。找得到,正常;找不到对应请求的 Cpl,就是 Unexpected Completion 错误——多半是 Tag 冲突、对端重复回包或者完成方发错。

第三层:回齐了没有。 拆几个 CplD 回来,由 Completer 决定:它按自己的 MPS 把请求的数据切成若干块,每块一个 CplD,读 4 KB 数据、MPS 256 字节的设备,就是 16 个 CplD 排着队回来。规则是:

  • 同 Tag 的多个 CplD 必须按地址顺序返回,中间不能乱(同 Tag 保序)
  • 第一个 CplD 的 Lower Address 由请求的 First DW BE 算出来;后续拆分 CplD 的 Lower Address 恒为 0——这是正常设计,抓包看到拆分包 Lower Address 全 0 别当 bug(唯一例外是 RC 的 RCB=64B 时 bit6 按 64B 对齐翻转)
  • 出错即终止拆包:任何一个 Cpl 出错(Status 非 SC),这个 Cpl 就是该请求的最后一个完成报文,Completer 不得再发后续 CplD。抓包看到 Cpl 数量少于请求折算的预期数,先看 Status——出错少包是正常,没出错少包才是真丢
  • 收齐才算完成:多 Cpl 的请求必须收齐所有 Cpl,收了一半就判定成功是软件 bug

这里必须把两条故障路径分清楚,排障全靠它:

现象含义AER 上报
等不到任何 Cpl,超时目标没收到 / 无响应 / Cpl 丢了 → CTO(完成超时)AER CTO 位
收到 Cpl 且 Status = UR目标收到了,明确拒绝 → UR(不支持请求)AER UR 位

这是两个不同的 AER 位、两条不同的故障路径:CTO 是"没搭理你",UR 是"拒绝了你还告诉你为什么"。驱动日志里看到 Unsupported Request 和 completion timeout,先分清是哪个,再决定往哪查。

五、版本差异:6.4 加了三个新东西

本系列基准钉在 5.0。6.4 在事务分类上动了三处:

维度r5.0(基准)r6.4验证含义
请求类型MRd/MWr/IORd/IOWr/CfgRd0/1/CfgWr0/1/Msg/MsgD/Cpl/CplD/AtomicOp新增 DMWr(Deferrable Memory Write,可延迟内存写)DMWr 是 Non-Posted 请求,每次都回 Completion(SC/RRS/UR/CA)——共享工作队列场景
UIO 请求新增 UIOMWr/UIOMRd,仅 Flit Mode 下使用需要新的流控配置,验证用例增加
Posted 集合MWr + Msg/MsgD同左;Flit Mode 下 NOP TLP 也属 Posted抓包新增 NOP 包识别,过滤规则更新
CRS 命名Configuration Request Retry Status改名 Request Retry Status(RRS)抓包/驱动日志关键字更换,语义不变

另外 6.4 在 Flit Mode 下完成关联规则也有扩展(完成带 TC 参与匹配、AtomicOp 头格式变化),做 6.0 产品时,完成关联的测试逻辑和抓包匹配器要跟着升级。

7.0 在事务分类上和 6.4 没有功能变化(数据出处:Base r7.0:6.4 + 128 GT/s + 编辑性修订)。所以 5.0 产品的知识直接能用,做 6.0 产品时把上表当升级清单逐行对照。

六、三表:故障、测试、判断

故障模式(Posted/Non-Posted 相关的五类典型现象):

症状可能根因可观测证据
Posted 写后无任何确认正常——MWr 无 Cpl,错误只能走 AER 消息抓包只见 MWr 无 Cpl;出错时对端发 ERR_ 消息(易误判为故障)
Non-Posted 请求无 Cpl,超时Completer 未收到 / 无响应 / Cpl 丢失dmesg completion timeout;AER CTO 上报
Cpl 状态 = UR地址未命中 BAR / 请求类型不支持 / 路由错抓包 Cpl(UR);dmesg Unsupported Request
读请求多个 Cpl 数据错序多 CplD 拆包顺序错(违反同 Tag 保序)抓包同 Tag 多 CplD 地址顺序乱;数据校验失败
Unexpected CompletionTag 冲突 / 重复 Cpl / 完成方错发抓包 Cpl 无对应请求;AER Unexpected Completion

测试方法(四招够用):

手段命令/工具预期
Posted/Non-Posted 行为对比分析仪分别抓 MWr 与 MRdMWr 无 Cpl;MRd 必有 Cpl(SC 或错误状态)
Cpl 状态注入对 BAR 外地址发起 MRd(devmem2 越界)返回 Cpl(UR) 而非 CTO——区分"拒绝"与"无响应"
CTO 与 UR 判别停目标响应 vs 目标回 UR无 Cpl = CTO(AER CTO 位);有 Cpl UR = AER UR 位
读拆包顺序验证大读请求(MRRS 上限)+ 分析仪同 Tag 的 CplD 按地址顺序返回;乱序 = 违例

判断依据(抓包判读五把尺子):

判据期望值/特征反例特征
Posted 收 Cpl永不发生(MWr/Msg 无 Cpl)收到 Cpl = 对端违反 Posted 规则(Malformed/UR 风险)
Non-Posted 必有 CplMRd/IORd/CfgRd/AtomicOp 等全部有 Cpl无 Cpl 超时 = CTO 故障路径
Cpl Status 合法值SC / UR / CRS(仅 CfgRd 可回)/ CACRS 出现在非配置请求响应 = Malformed
Completion 与请求关联Requester ID + Tag 唯一匹配无匹配请求的 Cpl = Unexpected Completion 错误
读 CplD 保序同 Tag 多 CplD 按地址顺序返回地址乱序 = 违例(数据可能错)

七、观测命令:把 Posted 和 Non-Posted 抓出来

1. MWr vs MRd 行为对比(最直观的一课)

分析仪挂在 Root Port 口上,驱动分别做一次内存写和一次内存读(写 BAR 一个寄存器、再读回来):

  • MWr:只看到上行一条包,没有下行 Cpl。写完成。
  • MRd:看到上行 MRd,然后下行一个或多个 CplD。读完成。

两趟抓完,Posted 和 Non-Posted 的差别就刻在脑子里了:写是单程票,读是往返票。

2. 越界读:让目标回 UR

devmem2 0xffffffff   # 对 BAR 外地址发起读

设备枚举正常、BAR 已配的前提下,对 BAR 外地址发 MRd,目标会回 Cpl(UR)。动手前先 lspci -vvv 看 BAR 范围,把越界地址算准再打。这一条的意义在于验证"拒绝"和"无响应"是两回事:有 Cpl(UR) 回来,说明地址解码链路是活的,只是这个地址没归属。

3. 停目标:复现 CTO

把目标设备的响应停掉(拔卡、把目标功能禁掉、或者用支持错误注入的测试设备),再发 MRd。这次什么都等不到,超时后 dmesg 报 completion timeout,AER 里是 CTO 位。和上一条对比着做,CTO 与 UR 的判别就牢了:有 Cpl 看状态,无 Cpl 看超时

4. 大读拆包:验证保序

把 MRRS 配大(DevCtl 里配置,默认 512 字节,03 篇讲过),发一个大块读。分析仪上能看到一个 MRd 对应多个 CplD,同 Tag 的 CplD 按地址顺序返回,后续拆分的 Lower Address 是 0。看到这些,说明拆分和保序都正常。


系列首发:CSDN「PCIe 协议特性精讲」| 基准 PCIe 5.0(Base Spec r5.0)| 内核 Linux v7.0.0
数字均以 Base Spec r5.0 为准(6.4/7.0 差异单列),错误欢迎评论区指出,勘误会更新在文末。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值