ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PCIe Relaxed Ordering详解:排序契约、TLP属性与性能调优

PCIe Relaxed Ordering详解:排序契约、TLP属性与性能调优 1. PCIe的排序契约里藏着一个让整个行业又爱又恨的1bit聊PCIe Relaxed Ordering之前我先说一个让我半夜加班修bug的故事。有一块FPGA加速卡驱动往下发命令的路径是先写数据缓冲区的源地址寄存器再写传输长度寄存器最后写GO位。这套流程跑了几个月都正常后来为了优化队列延迟有人把这几笔MMIO写入全部加上了RO标记——结果设备开始间歇性抽风状态寄存器显示收到GO但长度寄存器还是0DMA直接搬运了0字节。查了整整两天最后用协议分析仪抓TLP头才发现根源不是信号质量而是事务顺序被悄悄换了。这个故事就是理解Relaxed Ordering的最佳入口。PCIe从PCI/PCI-X一路演进过来把一套默认的强排序规则写进了协议规范里默认情况下同一路径上的事务大致是按发起顺序送到目的地的。这个默认值看起来不起眼却是无数设备驱动能够正常工作的基础。1.1 没有排序规则驱动连个门铃都不敢按为什么排序规则这么重要因为PCIe是一个总线事务模型软件和设备跑在完全不同的时钟和流水线上双方只有通过事务到达的先后来传递隐含依赖关系。最典型的就是Doorbell门铃机制。驱动先在主机内存里准备好一段DMA描述符然后往设备的门铃寄存器写一个通知值。设备看到门铃后去读描述符、执行DMA。这中间存在一个不可动摇的依赖门铃通知到达的时刻必须在描述符数据落盘之后。如果门铃这笔写入离开了描述符的写入设备读到的一定是一份残缺的描述符。再比如网卡的接收路径。网卡要把收到的包数据DMA进主机缓冲区然后再写一个完成队列项CQE。驱动看到CQE才去读数据。如果CQE先于数据到达主机内存驱动读到的就是老数据或者空buffer。这种依赖靠什么保证靠的就是PCIe默认的强排序模型。也就是说PCIe的排序规则不是给性能较劲用的它是整个系统里软件写A再写B硬件就能保证A先到这条默契的基石。没有这个默认值每个设备驱动都得在自己的协议栈里塞满屏障指令那PCIe生态根本跑不起来。1.2 Posted、Non-Posted、Completion读懂了事务类型才看得懂排序表排序规则聊的不是CPU cache那一层的一致性而是Transaction Layer里的TLP事务层包。要理解规则先要把PCIe里的事务分个类。事务类型典型例子是否有Completion特性Posted RequestMemory Write、Message无发出即完成不要求响应吞吐高Non-Posted RequestMemory Read、IO Read/Write、Configuration有必须等Completion返回延迟大CompletionCpl、CplD无用于返回读数据或读请求的状态Posted写之所以叫Posted是因为它像寄平信投进邮筒就不管了。读请求和它的Completion则像挂号信必须有回执。这种区别直接影响了排序规则的设计posted写可以无限堆积在中间队列里不会因为等不到响应而死锁non-posted读却必须想办法回来否则发送端可能永远卡住。PCIe规范里那张著名的Transaction Ordering Table横纵轴就是上面这些事务类型。它的每个格子描述当旧事务还在传输路上时新事务能不能越过它。默认情况下不少格子是不能越过这就保证了像先写描述符、再写门铃这样的依赖关系。而Relaxed Ordering要做的就是把这其中若干条不能越过改成允许越过。1.3 面向上游和面向下游规则永远是成对出现的很多人第一次看排序规范会被绕晕因为PCIe的排序约束不是一个全局总顺序它发生在每个链路、每个交换机内部而且上游和下游方向都要遵守。一个传输路径通常要经过Root Complex、Switch、Endpoint。主机发往设备的TLP往下一跳一跳走设备发往主机的TLP往上一跳一跳走。排序规则要求在每一跳的出口队列里相对顺序不能被破坏。交换机内部会把事务拆到不同虚拟通道所以同一条路径这个前提很重要。为什么很多设备在低负载下一切正常、一压测就出事就是因为低负载时队列里没有积压天然没有乱序的机会一旦队列满了调度器开始选择跳哪些、等哪些问题才暴露出来。理解了这个背景后面再看RO就不会只把它当成一个加速选项而会意识到你是在主动放弃一部分PCIe默认提供的顺序保证去换延迟和吞吐。2. TLP头里的Attr[1]RO到底放宽了哪些限制Relaxed Ordering不是某个驱动里调一个全局开关而是在每一笔TLP上独立声明的一个属性。规范里把TLP头里几个与属性相关的bit单独拎出来其中Attr[1]就是RO位。2.1 在TLP头里找到RO位以一个典型的4DW Memory Request TLP头为例第一个32bit字Byte0到Byte3的布局大致是字节位置Bit字段备注Byte07:5 4:0Fmt Type事务类型Byte17:4TC[3:0]流量类别Byte13TDTLP结尾是否有CRCByte12EP是否标记为毒化包Byte11Attr[1]RORelaxed OrderingByte10Attr[0]NSNo SnoopByte2-3-Requester ID等事务发起者和其他字段只要Attr[1]这一位为1这笔TLP就允许接收端和中间Switch对它做一定程度的乱序处理。反过来说如果它保持0那所有中间节点都必须按照默认排序规则处理它。在调试时要注意一个小坑协议分析仪或者仿真工具里看到的字段名可能是Relaxed Ordering、RO、Attr bit1也可能是Attr1/2/3这种编码。在Wireshark的PCIe解析里Attr字段经常表示的RO和NS的组合RO1通常显示为Relaxed Ordering。不同厂商的IP生成的报告命名也不一样不要凭字段名猜含义要回到TLP头的bit定义看。2.2 RO的真正语义给系统发一张插队许可这个我要特意强调RO不是命令系统必须乱序而是允许系统乱序。打个比方排队买奶茶。默认情况下后到的人不能插队这是强排序。RO设置为1相当于你举了一个牌子写我不介意被插队后面的人可以先买。但他可以先买不意味着他必须先买更不意味着你后面所有问题都和他无关。具体到PCIe层面RO最典型的放宽场景包括带RO的non-posted读请求可以越过更早发出的posted写。原本读请求必须等前面一堆写走完现在交换机如果愿意可以先把这个读请求送出去。读请求对应的Completion在返回路径上也更容易绕过阻塞从而缩短软件发出读请求到拿到数据的延迟。在某些实现里带RO的posted写之间也允许互相超越。这一点风险最大因为软件往往对多笔写的到达顺序还有隐性依赖。RO的目的很明确解除队列头阻塞。默认强排序下只要路径上排了一串posted写后到的读请求就只能在后面等着。某些宽带宽、多TAG场景下这种等待会直接变成几十微秒的延迟而RO把必须等变成可以不等。2.3 RO、NoSnoop、IDO、TPH是四件事别混成一个很多文章会把Relaxed Ordering和No Snoop放一起讲甚至有人以为开RO就是绕过CPU cache一致性这是不对的。术语解决的问题核心机制Relaxed Ordering (RO)事务到达顺序允许TLP在路径上插入/越过排序约束No Snoop (NS)CPU cache一致性标记请求不参与总线监听在PCIe里已边缘化ID-Based Ordering (IDO)不同Requester ID之间的事务乱序允许不同发起者的事务互相超越TLP Processing Hint (TPH)对端如何处理数据给Completer提示数据是流式还是缓存友好IDO尤其容易和RO搞混。IDO放宽的是不同Requester ID之间的顺序RO放宽的是同一笔TLP对面的顺序约束。可以同时开也可以只开一个。实际调试中有些Switch的乱序能力是靠IDO实现的RO只是per-TLP上的一个许可标志。两者不是同一个字段配置方式也不一样。3. 哪些设备真的能从RO里吃到肉网卡、NVMe、GPU的收益拆解RO不是银弹我见过很多团队把RO当成性能调优的万能钥匙结果收益没看到先看到不明原因的数据错误。下面按设备类型说下我实测和跟过的场景。3.1 网卡多队列收包队列头阻塞是真实瓶颈网卡收包路径上数据面天然适合RO。尤其现在高带宽网卡都是多队列每个队列把包数据DMA到不同的内存区域这些区域的写入之间没有任何顺序依赖。如果网卡固件给所有队列的数据写入都保持强排序那当某一个队列前面的TLP因为链路拥塞卡住时后面十几个队列的数据都会被堵在原地。这就是典型的head-of-line blocking。实测中多队列网卡在深队列、大burst流量下把数据面TLP的RO打开P99延迟能改善几个百分点到十几个百分点具体取决于Root Complex和Switch是否真的利用了RO。不过注意这里有个铁律必须守CQE和门铃这类与数据存在依赖关系的TLP绝对不能开RO。网卡固件应该在内部对数据写完和CQE可读之间做屏障而不是依赖驱动层去猜。另外RDMA网卡里这个思路更激进。数据TLP开RO后CPU侧还可以配合non-temporal写入让远端数据的读延迟进一步降下来。但同时也要保证完成事件和数据的可见性不然接收端拿到完成通知去读数据读到的还是旧内容。3.2 NVMe SSD命令级依赖大于事务级依赖NVMe表面上走PCIe协议但它的命令语义里塞满了顺序依赖Submission Queue的命令要写到主机内存然后Host写门铃通知设备设备执行完后要把Completion Queue Entry写回主机内存再触发中断。大部分NVMe控制器的做法是内部硬件会维护命令执行顺序CQE必须在对应命令的DMA数据落盘之后才置为可读。这种情况下如果软件盲目给所有PCIe事务开RO反而容易打乱硬件依赖关系。所以NVMe场景的主流用法是只对具体的PRP/SGL数据搬运TLP开放RO让多个命令的数据搬运可以互相超越而门铃和CQE保持强有序。这在多队列NVMe上收益比较明显IO队列多的时候每个队列各有各的门铃跨队列之间本来就没有强顺序需求硬件可以利用RO把同一时刻到达的多个命令的数据请求合并调度提升IOPS。但如果你的设备或驱动实现不够严谨建议先把RO关掉跑熟再谈优化。3.3 GPU、FPGA加速器与P2P传输读延迟最敏感的地方GPU和FPGA加速卡的DMA引擎往往是RO最有价值的使用者。这类设备最典型的工作模式是搬一大块数据数据之间没有任何顺序要求真正要求的是整块传输完成后的中断/标志位。只要把中断/标志位那笔写保持强有序中间成千上万笔数据TLP全部开RO也不会错。在GPU P2P或者GPUDirect RDMA场景里RO尤其有用。比如GPU通过PCIe Switch直接读另一块GPU的显存如果所有读请求都要严格按序返回一旦队头阻塞整个kernel就会空等。很多厂商的硬件实际上已经把RO做成了默认属性软件层不需要干预。不过要记住PCIe的RO不能替代GPU kernel内部的memory fence。GPU写一段结果再通过一个标志位通知CPU这中间需要的是设备侧的发布/获取语义。RO只是让TLP可以插队不会替硬件维护数据依赖。两者是不同层级别混用。4. 该开的不开不该开的乱开一次数据库损坏级事故复盘前面说了这么多理论这一段是很多人真正想看的RO全开之后到底会出多大的事。我用一次真实debug经历来复盘。4.1 事故现象间歇性校验和不匹配当时是一块FPGA加速卡通过多路DMA把计算结果写到主机内存然后更新一个完成记录completion record最后向主机发送MSI-X中断。为了压测延迟团队把DMA引擎里所有TLP的RO都置成了1包括完成记录和中断使用的posted写。一开始跑小数据量完全正常压到大数据量、多通道并发时主机端开始随机报checksum mismatch。诡异的是这个错不是每次都出跑低队列深度时几个小时都不出一回一上高并发就频繁。这种偶发高负载触发的特征第一反应往往是查DMA地址对齐、查内存ECC、查PCIE链路误码这些检查全做了都没有问题。最终用逻辑分析仪和PCIe协议分析仪抓包一条清晰的因果链才浮出来完成记录的那笔posted写携带了RO路径上某个Switch认为它可以越过更早发出的数据写于是把它送到了主机内存主机看到完成记录后立即去读数据区但数据DMA的TLP还堵在链路上于是读到了全部是0或者半新半旧的数据块。软件为了校验给每个block算了CRC这一算就把数据还没到给暴露了。4.2 排查链路从抓TLP头到A/B压测复盘一下当时的排查步骤你可以照着这个思路来验证自己系统里是否存在RO引起的乱序问题先用协议分析仪抓可疑的TLP重点看每一笔带RO的TLP与前后TLP的到达顺序确认是否存在完成记录先于数据的现象。在驱动里增加一个可调参数把RO分成三档全关、数据开RO但完成记录不开、全开。对三档各跑一轮高并发压测统计校验失败次数和错误触发时的TLP顺序。把完成记录和MSI-X中断这两类TLP强制关掉RO后错误完全消失数据开RO时的性能提升依然保留。这个结果说明一个问题很多设备数据面开RO没问题真正的雷区在控制面。完成记录、门铃、中断这几个东西本质上是给软件发信号用的它们和数据之间永远存在依赖关系。信号一旦越过数据软件看到的世界就是错的。4.3 不同Root Complex和Switch对RO的处置并不一致还有一件让我特别印象深的事同一块FPGA卡插在不同厂家的主板上RO带来的乱序现象差别巨大。平台类型我观察到的行为平台A对Endpoint发来的RO基本无视所有TLP仍然按强顺序到达平台BSwitch会利用RO权限做主动乱序调度性能提升明显平台C对部分TLP类型支持RO对Completion返回路径上的RO处理不到位这个差异不是规格错误因为规范只是允许乱序并没有要求每个组件都必须利用这个权限。于是就会出现一个尴尬的现状驱动在平台B上明明性能很好换到平台A之后RO几乎没有效果而在平台C上如果驱动对RO的使用激进一点可能触发平台自身的设计缺陷。所以我的经验是凡是涉及RO的优化验收必须在目标硬件平台上做不能只看一个平台的测试结果。驱动/固件里把RO做成模块参数让现场可以随时开关这个习惯能救命。5. 驱动和FPGA设计里怎么把RO用得又稳又猛最后聊点实际的。如果你负责的是网卡、SSD、加速卡这类PCIe设备的驱动或DMA引擎设计下面这几条规则我建议直接写进设计规范。5.1 先把排序依赖画成图再决定谁可以开RO开RO之前别急着写代码先画一张数据依赖图。每个箭头表示后者必须在前者之后可见箭头两端是要写入主机内存的不同对象。典型箭头长这样DMA数据 - CQE命令描述符 - 门铃数据写 - 中断。箭头上经过的TLP老老实实保持强有序或者中间加屏障不在任何箭头上的大量数据TLP才轮到RO上场。我见过很多RO把系统搞坏的案例几乎都不是RO本身坏了而是有人把箭头上的TLP也打上了RO标记等于把系统里最重要的依赖关系给拆了。5.2 完成记录和中断默认别碰RO这是我最想强调的一条CQE、Doorbell、MSI-X中断这些TLP在默认状态下不要开RO。它们是被等待方是上下游之间的同步点。你可以让数据先行但同步点必须稳稳地排在所有前置数据之后。如果你觉得这里也想用RO来降延迟那正确做法是在设备内部先实现一个DMA写屏障确保数据面的posted写全部冲到主机内存之后再发出不带RO的完成记录。某些DMA引擎实现里没有这种屏障那就不要动这个念头。用一次强有序的读请求做flush是很多驱动常用的土办法但它对这个读请求本身有要求这笔读也必须不带RO否则它可能绕过前面的门铃等于白读。5.3 在FPGA和DMA引擎里落地RO如果你在FPGA上自研PCIe DMA引擎RO通常在TLP builder里控制。比如基于Xilinx XDMA或者自研的PCIe Hard IP你会在产生Memory Write/Read的模块里看到一个类似attr或者ro的输入信号。把它接到DMA描述符的某个bit上这样每个命令都能独立控制RO而不是全局写死。一个比较稳妥的设计是描述符里摆一个RO_EN位驱动默认置0测试脚本在压测时按通道打开逐通道验证无数据错误后再放开。不要把RO嵌死在RTL常量里否则后面想关都没地方下手。PCIe 6.0和CXL时代事务排序的基本盘还在。RO这个属性照旧存在CXL.io也是基于PCIe的排序模型而CXL.cache和CXL.mem有自己的一致性协议那是另一个层面的事。未来加速器对低延迟的要求越来越高RO的价值只会更大但它始终是个许可而非保证。我的习惯总结成一句话新板卡默认全关压测数据证明瓶颈在排序上再开开了之后盯住完成记录、门铃和中断这三类同步点一个都不要动。用这种保守的方式做RO调优既吃得到性能红利也不至于在深夜的机房里面色发青地抓TLP。
返回列表