ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PCIe三层协议架构深度解析:事务层、数据链路层与物理层各司其职

PCIe三层协议架构深度解析:事务层、数据链路层与物理层各司其职 上个月调一块自研的FPGA加速卡卡上挂了PCIe x4的NVMe SSD通电后lspci能识别到设备链路也协商到了Gen3 x4可顺序读速度死活就是上不去。查了半天问题既不在物理层信号上也不在事务层地址映射上最后发现是数据链路层的流控信用初始值配得太保守DMA队列稍微一深就把发送方向堵住了。跟同事复盘时大家一致同意如果当初把PCIe的三层协议架构吃透这个Bug至少能少调三天。这篇是“PCIe协议解析”系列的第一篇先把最基本也最容易被忽略的协议框架讲清楚PCIe这个协议到底在解决什么问题、三层架构各自的职责边界在哪里、以及理解这些层次之后对实际调试究竟有什么帮助。不管你是搞FPGA、写驱动、做板卡硬件还是单纯想把“PCIe三层架构”这个知识点弄明白这篇都适合从头看起。1. 从并行总线的死胡同到PCIe串行架构的绝处逢生想理解PCIe为什么会是今天这个样子得先看一眼它要取代的东西有多尴尬。1.1 老PCI总线到底“老”在哪PCI是并行共享总线架构32位或64位数据线并排走所有设备挂在同一组总线上靠仲裁器轮流使用。理想很丰满但并行总线有个绕不开的物理问题频率越高信号间的时序偏差越难控制。所谓“并行”意味着每个bit都要在一个时钟沿同时被采样。走线长度稍有差异、PCB板层不等长、甚至温度变化都会让不同信号线到达目的地的时刻产生偏差这个偏差叫skew。频率低的时候一个时钟周期很长skew根本不算事频率拉到100MHz以上skew就开始吃时钟裕量了。PCI最终把频率停在33MHz/66MHz附近就是这个原因。Intel在PCI-X上尝试拉高频率到133MHz甚至更高付出的代价是布线要求极其苛刻而且所有设备共享带宽一个慢速设备可能拖累整条总线实在不优雅。除了频率瓶颈共享总线的带宽分配也是个老大难。总线带宽是全体设备分着用的设备越多每个设备分到的实际吞吐越低。服务器网卡动不动就要几个GB/s老PCI最多提供约1GB/s的总带宽根本不够塞牙缝。1.2 换赛道串行点对点为什么能解决这些问题PCIe的思路跟PCI完全相反把并行改串行把共享改独立。串行链路的每个方向只需要一对差分信号线数据在接收端通过CDR时钟数据恢复从信号边沿里提取时钟不再需要单独的时钟线跟数据对齐。既然不用考虑多根线之间的skew问题串行信号就可以把频率拉得非常高。PCIe从第一代Gen1的2.5GT/s起步到现在Gen5已经做到32GT/s这个节奏是并行总线想都不敢想的。同时PCIe采用了点对点连接。每个设备都有自己的专用链路不需要跟别人抢总线。为了防止从天而降的堵车又引入了Switch这种交换芯片让多个设备各自独享带宽。原本“一条大路所有人挤”变成了“每家都有自己的高速匝道”。1.3 GT/s不等于GB/s先把带宽这笔账算清楚围绕PCIe最常见的误区之一就是把GT/s直接当成Gb/s用。GT/s是“每秒Gigatransfer”也就是每秒传输的bit量。但物理层传输的bit不全是有效数据里面还包含编码开销。PCIe Gen1和Gen2采用8b/10b编码8bit数据会被转换成10bit符号多出来的2bit用于DC平衡和时钟恢复所以有效数据率要打八折2.5GT/s × 8/10 2.0Gbps 250MB/sGen1单lane单向到了Gen3及以后编码换成128b/130b开销大幅降到约1.5%所以单lane带宽直接跳到约1GB/s。我用一张表来总结代际速率(GT/s)编码方式单lane单向带宽x16单向带宽Gen12.58b/10b约250MB/s约4GB/sGen25.08b/10b约500MB/s约8GB/sGen38.0128b/130b约985MB/s约16GB/sGen416.0128b/130b约2GB/s约32GB/sGen532.0128b/130b约4GB/s约64GB/s这里还有一个隐藏点PCIe链路是独立的发送和接收差分对所以同一时间可以双向同时传数据。实际应用中很多人会纠结“为什么Gen3 x4的理论带宽是约4GB/s我测试只有2.5GB/s”。原因包括协议头开销、ACK/NAK反馈、流控调度以及测试时用的读写模型是半双工还是全双工。后面涉及数据链路层时这些开销来源会看得更清楚。2. 三层架构全景一份数据从CPU到外设的“通关之路”搞清楚了为什么是串行接下来进入本篇核心三层架构。2.1 三个层次的包TLP、DLLP、PLPPCIe协议栈从上到下分为事务层、数据链路层和物理层。每一层都有自己对应的协议数据单元层次协议数据单元缩写作用事务层Transaction Layer PacketTLP承载读写请求、完成包、消息等事务语义数据链路层Data Link Layer PacketDLLP链路管理、ACK/NAK反馈、流控初始化物理层Physical Layer PacketPLP物理层状态与控制信息比如电源管理和复位很多人刚接触时会把这三类包搞混。我的理解方式是TLP是货DLLP是运货过程中打的收据和催货单PLP是开关闸门和红绿灯的调度指令。三类包的形态完全不同职责也不重叠。2.2 从一次MMIO写操作看数据如何逐层“变大”拿最常见的CPU向一个PCIe设备的MMIO寄存器写一个32位数据来说流程从头到尾是这样的应用或驱动发起一条写操作事务层先构造一个Memory Write TLP。这个TLP的Header里包含目标地址、事务类型、请求者IDBus/Device/Function、Tag号后面跟着要写的4字节数据。此时它还是逻辑意义上的一个包。TLP交给数据链路层后数据链路层会在TLP前面加一个序列号Sequence Number在这个TLP后面追加LCRC校验值。为什么加这些因为数据链路层要负责可靠传输——万一包在半路被电磁干扰打断接收方的数据链路层需要能发现错误并通过ACK/NAK机制请求重传。这个包裹到了物理层时还要继续被“翻译”成物理子块能理解的形式再经过编码、串行化变成一对差分线上的高速跳变电平。这个过程反向做一遍就是接收方从物理信号恢复出比特流层层校验、剥离头部最后把原始事务提交给接收端的事务层。2.3 Root Complex、Switch、Endpoint在分层中的角色PCIe的物理拓扑一般分为三类角色。Root ComplexRC是体系中的“根”它连接CPU、内存和PCIe域所有CPU发起的PCIe事务都要经过RC。Switch负责扩展出多个下游端口让一颗RC可以连接大量设备。EndpointEP是真正干活的设备比如NVMe SSD、网卡、GPU它们既作为事务的完成者响应请求也可以主动发起DMA读写的请求者。从协议分层的视角看这三类角色都会实现完整的三层协议栈区别只在于事务层的“人设”RC既能当Requester又能当CompleterEP同样也可以同时扮演两种角色。比如NVMe SSD的DMA读操作SSD是RequesterRC是CompleterCPU读设备寄存器时反过来RC是RequesterSSD是Completer。这里先不展开RC内部如何路由、Switch怎么转发事务那是后续讲枚举和路由时需要细说的内容。现在只需要建立一个大框架无论数据走到哪里三层协议栈的处理模式是统一的。3. 事务层软件发出去的那张“快递单”上写了什么事务层是离软件最近的一层也是分析问题时最容易直接接触的一层。搞懂事务层基本上就搞懂了“PCIe设备之间到底在传什么”。3.1 TLP的最简结构Header、数据载荷、CRCTLP的完整结构是Header Data ECRCEnd-to-End CRC可选。Header的长度通常是3个双字3DW12字节或4个双字4DW16字节。需要64位地址时用4DW32位地址及大多数请求用3DW就够了。Header里最重要的字段包括Fmt/Type描述TLP的格式和事务类型比如这是Memory Read、Memory Write、Configuration Read还是CompletionLength数据载荷长度单位是DW双字4字节Requester ID发起方的BDF即Bus/Device/Function编号用于返回完成包时找到请求方Tag用于区分同一个请求方发起的多个未完成事务Address或BAR信息明确指出访问哪个地址空间数据载荷最大可以到4096字节但实际系统中默认的Max Payload Size通常限制在256或512字节。ECRC是端到端CRC它允许接收端验证整条路径上数据是否完好。大多数情况下ECRC在上游被直接丢弃或忽略属于可选功能。3.2 Posted和Non-Posted一眼判断事务需不需要“回执”事务层里最核心的概念之一是Posted和Non-Posted。Posted事务发出后不需要等待对方返回任何完成包。往审计日志里记一笔发完就干别的去了。典型代表是Memory Write和Message事务。这类事务因为不需要回执所以吞吐高、延迟低被大量用于DMA写场景。Non-Posted事务发出后必须等接收方返回一个Completion包。Memory Read、IO Read/Write、Configuration Read/Write都属于这类。这类事务天然会有“请求—完成”两个阶段Tag号就是用来把完成包和对应的请求关联起来的。一条读写流程的典型例子是CPU读设备寄存器。RC发出一个Non-Posted的Memory Read TLPEP收到后从寄存器取数构造一个Completion with Data的TLP返回给RC。如果请求本身非法或数据地址越界EP会返回Completion with Unsupported Request状态让上层知道“这事做不成”。3.3 四类地址空间不只是内存还有一个“配置宇宙”PCIe事务层定义了四种地址空间这个分类继承自PCI又做了精简强化。它们的属性和典型用途差别很大地址空间用途常见场景Posted属性Memory访问设备内存映射区BAR空间性能最高的路径DMA、MMIO寄存器写Posted读Non-PostedIO访问x86遗留的IO端口地址旧的设备控制寄存器写Posted读Non-PostedConfiguration访问设备配置空间枚举设备用它驱动初始化、BAR分配全部Non-PostedMessage带边带数据、中断、错误上报MSI/MSI-X中断、AER错误报告Posted配置空间之所以单独成一个地址空间是因为它承载了设备枚举的关键功能。上电后RC要通过配置读写遍历总线上的每一个设备读出供应商ID、设备ID、BAR大小再给设备分配合理的地址范围。这部分在热词里提到的“PCIe枚举过程”就是从这里出发的。BARBase Address Register映射可以这么理解设备内部有寄存器或存储空间软件不知道具体地址。设备在配置空间里声明自己的BAR大小和类型系统软件来分配地址并把这个映射关系告诉CPU内存控制器。之后CPU对这段CPU地址的访问就会被翻译成对应的PCIe Memory TLP真正打到设备内部。4. 数据链路层链路上真正的“隐形监督员”事务层负责定义“传什么”数据链路层负责确保“传得对、不重不漏”。这是PCIe可靠性的根基也是很多性能问题的隐藏战场。4.1 加序号、加CRC链路上怎么发现包坏了每个TLP被传递到数据链路层时都会被打上一个12位的序列号Sequence Number并计算一个32位的LCRC。序列号和LCRC都是跟着TLP一起传输的。接收方的数据链路层收到后先查LCRC是否匹配。匹配则说明链路传输过程中没有bit翻转不匹配则说明这个包在传输中损坏了需要丢弃并通知发送方重传。有人可能会问以太网也有CRC但也在传输中丢包为什么PCIe能这么可靠区别在于重传动作发生在哪一层。以太网通常情况下重传要交给TCP等上层协议完成数据链路层只做检错不主动重传。PCIe则在数据链路层就实现了ACK/NAK机制把“丢了就重发”这个动作内置在硬件里对软件完全透明。4.2 ACK/NAK重传PCIe把可靠性做在了最底层发送方每发出一个TLP都会把该TLP的副本保存在重传缓冲区里等待接收方回ACK或NAK。接收方正确收到TLP并且LCRC校验通过就返回一个ACK DLLP收到损坏或序列号不连续的包则返回NAK。发送方收到ACK后就可以把缓冲区里的副本释放了因为对方已经确认收到收到NAK则立刻重传发送缓冲区中未被确认的所有TLP。这个机制配合重传缓冲区保证了PCIe链路上TLP的最终可靠送达。我第一次调PCIe DMA时想不明白一个问题既然可靠为什么接收端还会出现数据空洞答案在于ACK是批量确认的接收方可能在ACK发送前断电或发生配置空间重设这时未确认的TLP就真的丢了。但从协议设计角度看在正常链路条件下它是尽力保证不丢的。4.3 流控信用防止缓冲区被写爆的“预付费”机制数据链路层还有一个非常关键的功能流控Flow Control机制是基于信用Credit的。接收方会在初始化阶段通过INITFC DLLP告诉发送方我的接收缓冲区每种类型能装多少数据量和多少个TLP。发送方只有在拿到足够信用的时候才能发送对应类型的TLP。信用按事务类型分开管理一般分为六类Posted Request Header、Posted Request Data、Non-Posted Request Header、Non-Posted Request Data、Completion Header、Completion Data。这么做的原因是不同事务对缓冲资源的需求差异很大混合统计容易被某种大事务饿死其他类型。我前文提到那个FPGA加速卡的Bug就出在这里。RC侧给到的Posted Header信用不太够我的DMA描述符队列却总是把一大批Posted Write同时压下去结果事务层认为“我有那么多写请求要发”数据链路层却卡在信用不足上队列瞬间积压性能从2GB/s掉到1GB/s。这种问题用示波器看不到眼图异常用lspci也看不出链路降级必须在驱动或逻辑里观察流控的情况才能定位。5. 物理层一条Lane上的电信号是怎么“活”下来的再往上走协议的所有逻辑最终都要落到一对差分线上的高低电平跳变。物理层是大多数人最容易忽略的一层因为很多问题在逻辑层看不出来反而在这层暴露得最直接。5.1 差分信号的天生优势PCIe每个方向用一对差分信号线传输一个bit通过两根线上相反的电压跳变来表示。接收端看的是两根线之间的差值而不是单根线对地的绝对电压。差分信号的好处在于抗干扰能力强。外界噪声通常以共模形式同时耦合到两根线上差值不变噪声就被抵消了。这比起单端并行总线靠地参考判断0/1的方式要稳健得多。同时差分对的回流路径清晰EMI也更可控。5.2 编码不只是“多算了几位”8b/10b到128b/130b的开销账Gen1/Gen2时期每个8bit的数据要编码成10bit符号发送这多出来的2bit是为了保证DC平衡和提供足够的跳变沿用于时钟恢复。代价很直白有效带宽打八折。5GT/s的Gen2实际数据率只有4Gbps。Gen3起引入了128b/130b编码。130bit里只有2bit冗余有效带宽从80%提升到约98.5%。所以Gen3虽然速率只是从5GT/s提到8GT/s但有效带宽几乎翻倍500MB/s直接跳到接近1GB/s。5.3 Lane与Link宽度和数据不是一回事每个Lane包含一对发送差分线和一对接收差分线Link则是由一个或多个Lane并行构成的捆绑链路。典型的Link宽度包括x1、x2、x4、x8、x16。带宽公式很简洁单Lane速率 × Lane数量 × 编码效率 有效带宽。比如Gen4 x4就是2GB/s×4约8GB/s单向。但这里有个坑Link宽度是可以按需协商和动态调整的。如果PCIe插槽是x16但设备只接了x8的边带或者金手指接触不良最终协商很可能落在x8甚至x4。你要是在带宽测试时发现数字差点意思先用lspci确认两件事协商到的速率是Gen几Lane数量是多少。5.4 链路训练的极简版LTSSM关键状态链路从硬件上电到进入正常传数据状态不是一蹴而就的而是由LTSSM链路训练和状态状态机逐步驱动。LTSSM的完整状态图非常复杂但核心状态就那么几个状态干什么卡住时的典型症状Detect检测Link两侧是否物理连接lspci看不到设备Polling建立位锁和符号锁定设备反复消失Configuration协商Lane数量和速率速率停在Gen1/Gen2L0正常传输TLP/DLLP没有正常Recovery重新协商速率或宽度忽然降速到Gen1L1/L2低功耗/关闭唤醒失败在Configuration阶段设备会确定最终的工作速率和Lane数之后进入L0状态干活。如果信号质量不好速率协商会主动降档比如Gen4设备只协商到Gen2。这是物理层给你的一道“防呆护栏”信号差没关系先能跑起来再说。6. 拿着协议栈分层去调板我的实战排查心得最后聊点实际的。既然三层架构的分工已经清楚那真正拿到一块异常板卡时怎么利用分层思维快速圈定问题范围这是我摸索出来的方法。6.1 拿到异常先问“哪一层”先看链路能不能建立。如果lspci里根本没有设备或者设备ID全零先怀疑物理层。这时候检查PCIe复位信号、供电时序、参考时钟以及连接器的焊接和差分对是否反了。物理层问题还经常表现为设备时好时坏、换一个插槽就能用、降速到Gen1能稳定但高速率时崩溃。再看链路速率和宽度对不对。lspci -vvv里能看到LnkCap和LnkSta字段。LnkCap表示设备自身能力LnkSta表示当前实际协商结果。如果LnkSta的宽度或速率不符合预期大概率是Configuration阶段没协调好属于物理层或数据链路层的边界问题。链路正常了再看误差计数和错误状态。设备寄存器或lspci输出里的URPoUnsupported Request、ECRC错误计数突然增加往往意味着TLP层面出了问题。曾经的经历某个网卡通信时好时坏我一度怀疑交换机最后通过lspci -vvv发现该设备收到了大量Unsupported Request的返回方向是RC那边发了设备能力之外的非法请求。问题根源在驱动代码把BAR地址算错了硬件卸载了一批非法请求软件还浑然不知。最后才考虑性能和流控问题。性能不够优先看数据是走Posted还是Non-Posted路径再看Flow Control Credit配置和Max Payload Size是否合理。之前一直强调很多“链路正常但吞吐上不去”的Bug真正捣乱的是数据链路层流控。6.2 几个容易踩的坑和习惯第一别只看速率不看宽度。Gen4 x1和Gen3 x8的理论带宽其实差不多但应用场景差异巨大。确认“协商到了Gen4”之后一定要再确认Lane数量。第二别把首轮带宽测试结果当真理。测试DMA吞吐时如果驱动或FPGA逻辑里用了小粒度请求比如每个TLP只带64字节有效数据协议头开销会吃掉相当大比例的有效带宽。这种情况下先别怪协议优化方向是把请求合并、加大MPS到256或512字节。第三留意电源管理状态。LTSSM进入L1/L2之后事务层任务队列还傻乎乎地等着数据驱动层面表现出来就是偶发延迟飙升。这类问题跟三层架构都有关系排查时要用逻辑分析仪抓LTSSM状态而不是死盯着信号质量。第四有条件就上PCIe协议分析仪。别小看这台设备它能同时看到物理层统计、TLP序列和DLLP回复能帮你直接把“三层架构”变成“三层证据链”。如果没有设备也尽量在FPGA里留下计数器TLP发出总数、ACK收到数、NAK重发数、Credit剩余量这些都是分层定位问题的宝藏。6.3 系列后续先把地基打牢这篇的重点是把PCIe的基本概念和三层架构讲清楚相当于盖楼前先画结构图。先搞清楚“三层各管什么、边界在哪”后面的路由机制、配置空间、枚举流程、DMA实现和错误处理才有讨论基础。下一期我打算把事务层单独拎出来逐字段拆解TLP的格式Address、Tag、Requester ID这些字段分别怎么填、什么时候填错会产生什么后果再用一个具体的DMA读描述符的例子让你能完整看懂一份TLP十六进制数据。这个内容需要点耐心但啃下来之后你再去看协议分析仪的报文会觉得自己打开了新世界。
返回列表