ARTICLE DETAIL

资讯详情

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

PCIe事务层深度拆解:一次内存读请求的TLP完整旅程

PCIe事务层深度拆解:一次内存读请求的TLP完整旅程 1. 一次内存读是如何惊动整条 PCIe 链路的写代码的人可能很少会去想一句data *(uint32_t *)0x80000000;在 PCIe 的世界里到底掀起了多大的波澜。我最早接触 PCIe 事务层协议时啃的是规范的 Chapter 2看得昏昏欲睡。直到后来在调试一个 FPGA 加速卡时被迫把一次内存读请求从 CPU 发出到数据返回的整个过程完整捋了一遍才真正觉得事务层协议这四个字是有血有肉的——它不是一堆 TLP 格式的教条而是一套端到端的信用体系、路由规则和排序契约。这篇文章是系列的第二篇我们聚焦在事务层Transaction Layer。我会用一次最典型的、从 RCRoot Complex发往 EPEndpoint的 32 位内存读请求作为主线跟着这个请求从发出、中转、到达、返回的每一步走一遍。读完你会明白TLP 头部的每一个字节为什么在那里Tag 为什么不能乱用Credit 机制到底在防什么以及 Completion 报文为什么一定要按规矩回来。先说一个总体的印象一次内存读在 PCIe 上其实要经历两段独立的报文——一段是读请求Memory Read RequestMRd从 RC 走向 EP另一段是完成报文Completion with DataCplD从 EP 返回 RC。这两段报文各自都要完成路由、流控、CRC 校验等一系列动作。换句话说你读到的那 4 字节数据是对方专门派了一个快递员送回来的。整个过程中任何一环出了问题CPU 那边等到的就是一次超时轻则驱动报错重则系统挂死。所以在展开细节之前你先记住三件事后面所有内容都围绕它们展开第一PCIe 事务分为 Posted、Non-Posted 和 Completion 三类。内存读属于 Non-Posted意味着它必须有回应像一个打电话必须有人接而内存写是 Posted像发短信发出去就不管了。第二报文在链路上以 TLPTransaction Layer Packet为单位传输。TLP 不是裸的数据它有严格定义的头部Header、可选的 Data Payload、以及可选的 CRCECRC。第三TLP 能不能发出去不取决于发送方想不想发而是取决于接收方有没有提前给它授信——这就是流量控制Flow Control的核心逻辑。我们往下拆。2. 出发之前先看清楚 TLP 长什么样一次内存读报文的逐字节解剖在进入旅程之前得先把车认识清楚。一次 32 位内存读请求的 TLP头部是 12 字节注意64 位地址时头部会变成 16 字节。除此之外它没有 Data Payload——你是去读别人自己当然不带货。整个 TLP 在事务层的形态如下字段位宽取值以我们的请求为例含义Fmt[2:0]3 bit000000 表示带数据的 Non-Posted其实读请求没有数据但有专门编码Type[4:0]5 bit0000000000 表示 Memory Read Request (32-bit)TC[2:0]3 bit000Traffic Class默认 Best EffortR[0]1 bit0保留位TD1 bit0是否有 TLP DigestECRCEP1 bit0Poisoned 标记Attr[1:0]2 bit00默认属性不涉及 No-Snoop/Relaxed OrderingLength[9:0]10 bit0000000001读请求的长度单位是 DW4 字节这里读 4 字节所以 Length1Requester ID[15:0]16 bitBDF 编码发起者的 Bus/Device/Function 编号Tag[7:0]8 bit由驱动分配事务标签用于匹配完成报文Last DW BE[3:0]4 bit1111最后一个 DW 的字节使能First DW BE[3:0]4 bit1111第一个 DW 的字节使能Address[31:2]30 bit例如 0x80000000 右移 2 位32 位地址4 字节对齐你可能已经在头大了这一堆字段到底怎么协作别急我们挑几个关键点拆开讲。先看 Fmt 和 Type。PCIe 规范里把 TLP 类型编成了两组编码。Fmt 决定大类00 表示 Non-Posted10 表示 Posted01 表示带数据的 Completion10 又配合 Type 区分 Completion 的种类……这里容易绕晕我给你一个更直观的分类方法直接看报文是请求类还是完成类。请求类由 RC 或 EP 发起完成类一定是对某个 Non-Posted 请求的应答。然后是 Length 字段。注意它不是字节数而是 DW 数。我们的请求读 4 字节所以 Length11 个 DW。如果你想一次读 16 字节Length4。Length 上限是 1024 DW4KB这跟 MPSMax Payload Size不是一个概念——MPS 限制的是 Posted 写请求的 Data Payload而读请求的 Length 限制的是对方返回 CplD 时总共要带多少数据它受限于 MRRSMax Read Request Size。接着说字节使能Byte Enable字段。BE 字段控制的是这一次访问里哪些字节是有效的。我们的例子是 4 字节全读所以 First 和 Last DW BE 都是 1111。如果你的驱动软件只读 2 字节而且地址不按 4 字节对齐BE 就会变成像 0011 这样的组合读回来的数据里只有对应字节有效。PCIe 对这个字段的处理很死板它只看 BE不帮你做字节聚合所以如果读请求横跨了两个 DW就会产生两个读请求 TLP而不是一个 Length2 的请求——这点跟许多 CPU 总线的处理方式不一样写驱动的朋友请特别注意。最后看地址字段。32 位请求的 TLP 头里只有 30 bit 的地址因为低 2 位被 Address[31:2] 里的 DW 对齐要求吃掉了。这意味着 32 位 TLP 只能读 4 字节对齐的地址。如果你的访问没对齐硬件要么拆分要么直接报错。64 位地址请求头部多出 4 字节存放高 32 位地址Type 编码也不一样是 00001Memory Read Request, 64-bit。现在我们手里这辆车已经刻画清楚一个 12 字节头部、没有任何负载的 Non-Posted 请求。它在事务层生成之后会往下交给数据链路层加 Sequence Number 和 LCRC再交给物理层做编码、串行发送。但这个往下走的过程有一个前置条件——RC 必须先确认对面 Endpoint 的接收信用足够收下这个 TLP。3. 流量控制Flow Control发报文之前先看信用额度如果你用过 TCP你肯定熟悉窗口和确认。PCIe 的流量控制在思路上和 TCP 有点神似但机制完全不同——TCP 是字节流加滑动窗口PCIe 则是按 TLP 头和 TLP 数据 DP 分开管理每个 VCVirtual Channel独立记账。而且 PCIe 没有丢包重传链路层的 LCRC 错包只会被标记不会重新发送所以事务层的 Flow Control 更像是一道防洪闸在源头避免缓冲溢出。3.1 信用种类为什么头和数据要分开记在 PCIe 事务层接收方为每个 VC 维护了 6 种信用计数器按 TLP 的头和数据负载分别管理。具体是Posted HeaderPHPosted DataPDNon-Posted HeaderNPHNon-Posted DataNPDCompletion HeaderCplHCompletion DataCplD一次内存读请求属于 Non-Posted它只消耗接收方的 NPH 信用。因为读请求没有 Data Payload所以不消耗 NPD。这就是为什么一次 MRd 请求只要能借到 1 个 NPH 信用就能发出去。而内存写请求是 Posted它要消耗 PH 和 PD 两个信用——头部和负载各占一个。这套头/数据分开的设计本质上是为了让空载报文和满载报文不互相挤占缓冲。比如一个 FPGA 的接收 FIFO 可能为 TLP 头预留了较多条目但为数据负载预留了较少条目。如果头和负载混在一个池子里很可能出现头缓冲区还宽裕数据缓冲区已经满了结果一个小报文的头也进不来形成不必要的阻塞。分开管理之后调度器可以分别控制灵活性更高。3.2 初始化和更新InitFC 与 UpdateFC信用不是一开始就有的。链路两端在上电后、还没有任何 TLP 数据流通之前要完成一个信用初始化过程。具体到事务层RC 和 EP 在 Link Training 完成之后通过发送 InitFC1-P/NP/Cpl、InitFC2-P/NP/Cpl 报文把各自的接收缓冲区容量广播给对端。对端拿到这些数值后就能算出自己的发送窗口。我建议你记住一个关键公式发送方可用的信用剩余 信用上限InitFC 给出的值 - 已发送且未得到释放的信用其中释放机制也很特殊PCIe 里没有显式的ACK 报文接收方通过发送 UpdateFC-P/NP/Cpl 报文把当前已经释放的信用值告诉发送方发送方据此恢复自己的信用计数。UPDATE 报文是周期性发出的不是每个 TLP 都回一次这样可以降低链路开销。3.3 实测中容易撞上的信用问题我在调 FPGA 时遇到过一个典型的信用坑EP 侧的接收缓冲设计得过小比如 NPH 只给了 4 个 Credit而 RC 同时发出了多个 outstanding 的读请求比如深度为 8 的读队列没限制。RC 自己以为窗口很充裕前 8 个请求全部发出去了结果 EP 侧 NPH 缓冲区立刻填满后续的报文只能阻塞等待 UpdateFC。这种情况不会丢包但延迟会急剧上升。更麻烦的是如果 EP 侧驱动逻辑在缓冲区满时还要把新到的读请求 TLP挂起一旦处理不及时CPU 就会觉得读超时。所以建议你在做 FPGA EP 设计时接收缓冲至少要能够容纳协议栈允许的最大 outstanding 请求数并且 NPH、PH、CplH、CplD 的 Credit 分开规划。很多现成的 PCIe IP 核Xilinx XDMA、Synopsys DWPCIe都提供了可配置的 Credit 数值不要图省事全用默认值。另一个常见误区有朋友以为 Credit 不够时多等一会儿就会自己恢复。实际上如果对端既不消费 TLP也不发 UpdateFC你的发送端会一直卡在等待状态直到超时。所以排查链路性能问题时第一个该查的就是 Credit 是否被某个不消费报文的 Endpoint 卡死了。4. 报文上路从 RC 发出到 Switch 转发的路由与排序规则现在我们假设 RC 成功地拿到了 NPH 信用MRd TLP 正式走上链路。接下来它要经过的可能是直连 Endpoint也可能经过一个 PCIe Switch。这篇文章的主角是一次通过 Switch 中间转发的完整旅程因为它最有代表性。4.1 基于地址的路由32 位内存读怎么找到目标设备PCIe 的路由方式有三种地址路由、ID 路由、隐式路由。配置读写 TLP 用 ID 路由根据 Requester ID、Bus/Device/Function 号Message 可以用隐式路由而我们手里的这个内存读请求走的是地址路由。Switch 的每一个下游端口Downstream Port都配置有 Base Address RegisterBAR对应的地址窗口上游端口Upstream Port会把收到的 TLP 的地址字段与各下游端口的窗口做匹配。一旦匹配命中Switch 就把 TLP 转发到对应端口。如果没有任何下游端口命中Switch 会怎样它会把报文转向上游如果上游也是 Switch 端口直到到达 RC。RC 发现地址不在自己的任何窗口里就会把它当作 Unsupported RequestUR处理——这就是枚举之外地址路由找不到目标时的兜底行为。这里有一个容易混淆的点地址路由只看地址不看 BDF。也就是说一个 TLP 里的 Requester ID 是谁发的但路由依据是地址要去找谁。无论 Requester ID 是什么只要地址命中某个下游端口窗口它就会被转过去。4.2 Switch 转发时的排队与仲裁Switch 内部不是简单地把 TLP 从上游端口搬到下游端口。每个端口都有独立的接收队列和发送队列。一个 TLP 从上游进来先进入上游端口的接收缓冲区然后由 Switch 内部的仲裁器决定何时把它放进下游端口的发送队列。仲裁策略跟 TLP 的类别有关。PCIe 规范允许每个端口对 Posted、Non-Posted、Completion 三种流量设置不同的优先级。最常见的做法是Completion 最高Non-Posted 次之Posted 最低但也有不少实现把 Posted 放在最高优先级——因为 Posted 报文没有回应阻塞它会导致发送方 Credit 卡死。我个人的倾向是在 Switch 这种中转节点上尽量让 Completion 和 Non-Posted 的优先级高于 Posted否则读延迟容易被写流量拉高。当然如果上游是一个对延迟不敏感的批量写场景也可以反过来但这需要你对自己的流量模型有清晰认知。值得注意的是Switch 在转发 Non-Posted 请求时不会自己缓存数据它只是转手。所以读请求经过 Switch 时Switch 并不需要为它分配额外缓冲只需要保证能在自己的 Credit 体系内转发出去。真正消耗 Credit 的是 EP 侧接收这个请求以及 RC 侧接收返回的 CplD。4.3 排序规则Ordering到底管什么PCIe 事务层有一条非常重要的排序规则同方向、同 VC 的 TLP必须遵守一定的顺序约束但不是全序而是部分有序。举个例子一个后面发出的 Posted 写请求可以超过前面发出的 Non-Posted 读请求先到因为 Posted 和 Non-Posted 属于不同类别它们之间允许乱序。但是两个 Posted 写请求之间Publisher 和 Completer 都必须维持顺序两个 Completion 之间也必须维持顺序。这条规则的意义在于软件层面驱动经常要写一个 Doorbell 寄存器然后读一个状态寄存器来确认写已生效。如果驱动发出一个 Posted 写Doorbell紧接着发一个 Non-Posted 读状态寄存器PCIe 允许读请求超车先到达 Endpoint。如果 Endpoint 的处理逻辑是收到状态读时还没处理完 Doorbell软件就会读到旧状态。这不是协议错而是软件没有遵守正确的同步方法。正确的做法是在 Doorbell 写操作之后驱动应该再发一次针对同一地址范围的特殊读或者使用 Memory Fence 语义或者干脆把 Doorbell 也设计成 Non-Posted。我在实际驱动开发里就踩过这个坑——FPGA 端的门铃寄存器被写入了但我立刻去读状态寄存器读回来的却还是旧值排查了半天才发现是排序规则的锅。这个规则也告诉我们一个事实PCIe 并不承诺先写后读的全局顺序它只保证同类别报文之间的顺序。跨类别的顺序需要你自己想办法。这在多核 CPU 场景下尤其重要因为你无法依赖 PCIe 为你做 CPU 间的写后读同步。5. 目标设备处理请求从接收 TLP 到生成完成报文经过 Switch 的转发MRd TLP 终于到达了 Endpoint。我们假设这是一个 FPGA 加速卡里的一段 AXI 从设备逻辑地址 0x80000000 对应 AXI Slave 接口的偏移 0。5.1 EP 侧接收流程从物理层到事务层的解析EP 的接收路径和发送路径是镜像对称的。物理层收到串行比特流后做解码恢复数据链路层校验 LCRC 和 Sequence Number然后去除链路层头尾把干净的 TLP 交给事务层。事务层解析 Header根据 Fmt/Type 知道这是一个 32 位内存读请求地址 0x80000000长度 1 DWRequester ID 是 RC 的 BDFTag 是驱动分配给这次读的标签。这里有一个重要细节EP 收到 Non-Posted 读请求后如果它不能立即响应比如 AXI 总线上有等待周期它不能无限期拖着不回复。PCIe 规范给了 Completer 一个响应时限超过这个时限必须回一个 Completion 状态为 URUnsupported Request或 CACompleter Abort的报文否则 Requestor 会超时。所以 EP 侧设计一定要避免读请求进来之后长时间无响应。5.2 完成报文CplD的构造与状态字段现在 FPGA 逻辑从 AXI Slave 接口读回了 4 字节数据任务回到事务层构造一个 CplD 报文返回。CplD 的头部结构如下Fmt/Type010 00010带数据的 CompletionCompleter IDEP 自己的 BDFRequester ID从接收到的 MRd 里取出的 RC 的 BDFTag必须和 MRd 里的 Tag 保持一致Lower Address对应读起始地址的低 7 位Status[2:0]SCSuccessful Completion值为 000BCMByte Count ModifierByte Count从请求地址到返回数据结束所剩余的字节数然后是 Data Payload4 字节数据特别注意 Lower Address 和 Byte Count 这对字段。Lower Address 是请求地址的低 7 位Byte Count 是从请求地址跳到读数据末尾的字节数等于 4因为刚好读完 4 字节。这两个字段不是用来路由的而是让 Requestor 能把 Completion 和数据拼回正确的内存位置。如果一个读请求需要返回多个 CplD比如 Length 大于 MPS每个 CplD 的 Lower Address 和 Byte Count 都会不同Requestor 就是靠它们来重组数据的。Status 字段非常关键。除了 SC成功还有 UR不支持的请求和 CA完成方中止。我调试时经常看到的一种情况是EP 收到一个读请求但地址不在任何 BAR 窗口内规范要求它回一个 StatusUR 的 CplD而数据 Payload 可以是任意值。如果你在驱动里发现读回来一个值很奇怪但是没报错的数据先检查是不是收到了 UR——很多 CPU 的 PCIe 控制器会把 UR 当作读回全 1处理导致你看到 0xFFFFFFFF 这种诡异值。判断方法是查看 AER 错误寄存器Advanced Error Reporting里的 UR 状态位。5.3 CplD 能否往回走ID 路由的规则现在 CplD 要从 EP 回到 RC。这段旅程使用的不是地址路由而是 ID 路由。ID 路由的依据是 BDF。TP 里携带的是 Requester IDRC 的 BDF但 ID 路由看的不是这个——它看的是路由到哪个 Bus/Device/Function。在 Completion 报文里这个角色由 Completer ID 承担吗也不是。实际上CplD 的路由方式是顺着 Request 从 RC 到 EP 的路径原路返回Switch 根据它在内部维护的哪一个下游端口对应的设备是请求来源的映射关系来确定方向。说得更具体一点Switch 在上行方向从下游到上游维护了一张路由表当它看到 Completion 时它会根据 CplD 里的 Requester ID回想一下这个 ID 是 RC 的 BDF在路由表里找到对应的端口然后把报文往那个方向送。每个下游端口都知道自己的子树里有哪些 BDF 可达所以 ID 路由本质上是查表转发。在多级 Switch 级联的场景中ID 路由依赖各级 Switch 正确配置。如果某个 Switch 的 BDF 路由表没有被正确初始化CplD 就可能在中间节点迷路。排查这个问题时最直接的办法是用协议分析仪抓 TRACE观察 CplD 到了哪一级就停住了。6. RC 侧收尾Tag 管理、完成报文的匹配与超时处理CplD 回到 RC 之后RC 的事务层需要把它和之前发出的 MRd 对上号然后把数据写入 CPU 的寄存器或者内存。这个对上号的核心就是 Tag。6.1 Tag 分配与最大未完成读请求数每个 Non-Posted 请求在发出时都要携带一个唯一的 Tag。Tag 的取值范围是 0-255但实际可用的数量受到两个因素的限制PCIe 规范允许不同设备实现的 Tag 池大小不同RC 通常支持较多 TagEP 则可能很少。一个设备发出的所有 outstanding 的 Non-Posted 请求它们的 Tag 必须互不相同。为什么因为 CplD 返回时只凭 Requester ID Tag 来匹配是哪个请求的完成报文。如果两个读请求用了同一个 TagCplD 返回时 RC 就分不清这个数据到底该给谁。我在做驱动时有个经验在初始化阶段先读一下设备的 Device Control 寄存器里的 Max Read Request Size 和 Tag 支持位看它最多支持多少个未完成读。很多 Endpoint 为了省硬件资源只支持 1-2 个 outstanding Tag。如果 RC 侧软件一次性发出大量读请求超出 EP 的 Tag 容量EP 要么把多余的请求当作 UR 处理要么直接忽略。无论哪种都会导致软件读到错误数据或者卡在等待超时。6.2 完成的匹配规则除了 Tag 还要看什么Tag 之外CplD 和 MRd 的匹配还要看 Requester ID。你可能觉得奇怪——CplD 里的 Requester ID 不就是从 MRd 复制的吗确实是的。但在多 RC 的系统比如某些服务器平台有多个 RC 节点或者 EP 主动发起读请求的场景下Requester ID 是区分不同请求方的关键。还有一个隐藏细节如果一次读请求被拆分成了多个 CplD比如 Length 超过了 EP 的单次完成能力这些 CplD 的 Tag 都是同一个RC 要根据 Lower Address 和 Byte Count 把它们按序拼起来。RC 侧通常会维护一个期望收到的字节计数每收到一个 CplD 就扣减直到归零。如果收到一个 Byte Count 和 Lower Address 对不上的 CplDRC 会判断这是一个错误 Completion很可能上报一个 Completer Abort 或者 Unexpected Completion 错误。感叹一句这些设计比很多人想象的要重得多。很多人以为 PCIe 就是简单地把数据打进报文里发出去但一次读请求实际上是一场 Requestor-Completer 之间的状态机舞蹈双方都要精确地管理 Tag、Credit、期望字节数、超时定时器。6.3 读超时的根源在哪里最后一个常见问题RC 发出 MRd 之后如果迟迟等不到 CplD会发生什么答案是 RC 会启动一个超时定时器Timeout。超时时间因平台而异典型值在几十微秒到几十毫秒之间。超时发生后RC 会认为这次读失败返回给 CPU 的可能是全 1 或者一个 Machine Check Exception。很多工程师遇到读返回 0xFFFFFFFF的第一反应是怀疑 Endpoint 的数据本身是 0xFFFFFFFF但实际上更常见的原因是 Request 被 UR 处理或者 EP 的 Tag/协议逻辑有 bug压根没生成 CplD。排查这种问题的链路我建议按顺序做四步先用协议分析仪抓 TRACE确认 MRd 是否真的到达 EP。确认 EP 是否返回了 CplD如果返回了看 Status 字段是 SC 还是 UR/CA。如果 EP 没返回检查 EP 侧是否有足够 Credit 接收 MRd是不是被某个阻塞的 AXI 读等死了。如果 CplD 回不到 RC检查链路中间各级 Switch 的 ID 路由表。这套排查思路我每次遇到读卡死都会用基本能解决 90% 的问题。7. 写在一次读完成之后事务层设计里最容易被低估的三个细节走完这一趟旅程我想额外分享三个事务层设计里容易被低估的细节它们不会直接出现在某个 TLP 头部字段里却真真切切影响你的系统能不能跑稳。第一个是ECRC端到端 CRC。我们前面解剖 TLP 时提到了 TD 位。如果 TD1说明 TLP 带了一个 32 位 ECRC它覆盖从 Header 到 Payload 的整段内容。ECRC 最大的价值在于端到端校验——数据链路层的 LCRC 只覆盖一段链路经过 Switch 转发时 LCRC 会被重算但 ECRC 从头到尾不变。如果你的数据要从 EP 穿过两级 Switch 到 RC中间任何一级的内存位翻转LCRC 可能发现不了因为每段都重新算了但 ECRC 一定能发现。可惜的是默认情况下很多系统不开 ECRC我建议在可靠性敏感的场景务必要打开它。第二个是MRRS 和 MPS 的配合关系。读请求的 Length 受 MRRS 限制写请求的 Payload 受 MPS 限制二者是独立配置的。很多初学者会把这两个概念混在一起。举个例子你想让 EP 一次性返回 4KB 数据于是设置 MRRS4KB但 EP 的 MPS 只有 256B。这种情况下EP 不会返回一个 4KB 的 CplD而是按照它的 MPS 上限把完成报文拆成多个 256B 的 CplD。RC 侧必须把它们拼起来。如果你的 RC 实现不支持多 CplD 重组某些简化型 RC 可能会直接报错你就得把 MRRS 调低让一次读请求正好落在一个 CplD 里。第三个是VCVirtual Channel映射与 QoS。事务层的 TC 字段值是软件配置的它本身不参与路由它只决定入口端口把这个 TLP 映射到哪一个 VC。TC 0-7 可以映射到 VC0-7但通常默认只启用 VC0所有 TC 都映射到 VC0。如果你想让高优先级流量走独立的 VC需要两端都配置 TC/VC 映射表同时还要保证每个 VC 的 Flow Control 空间足够。我在实际项目里见过只开了 VC0 的系统高优先级数据和普通数据混在一起导致瞬时大流量下读延迟抖动超过 10 倍。所以如果你的系统对延迟有硬性要求认真评估 VC 划分是有价值的。回到最初那行 C 代码data *(uint32_t *)0x80000000;。这行代码的背后是一次 Non-Posted 请求的勇敢出发是一路上的地址查询、Credit 授予、接收缓冲、完成报文重组是 RC 和 EP 之间一整套沉默但精确的握手。没有哪一步是可有可无的。做完这个拆解我自己的感觉是PCIe 事务层并不神秘它只是把读一个地址这件在 CPU 看来理所当然的小事设计成了一个由无数状态机和信用体系支撑的精密协作。理解它最好的方式不是从头到尾背规范而是像我这样跟着一个 TLP 走完它的一生。下次当你看到一台服务器上的信号分析仪抓出满屏 TLP 时希望你也能像看老朋友一样认出它们那个 12 字节头部、没有负载的是去问路的 MRd那个带着 4 字节数据、低着头往回赶的是回来交差的 CplD。它们在路上跑得再快也逃不过 Credit 和 Tag 这两道无形的关卡。
返回列表