ARTICLE DETAIL

资讯详情

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

从 CMIS 到 SONiC:光模块固件工程师的主机侧实战指南

从 CMIS 到 SONiC:光模块固件工程师的主机侧实战指南 1. 光模块固件工程师为什么要把 SONiC 当成主机侧说明书做光模块固件的同行大多有个共同体验CMIS 规范本身写得非常细寄存器表、状态机、时序图一应俱全但真正上手写固件的时候最难的不是规范里写了什么而是主机到底会怎么问我。规范是双方的合同可主机侧的实现千差万别有的主机上电后等 3 秒才发 Page 00h 的控制命令有的每 200 ms 就轮询一次 DataPathFirmwareState还有的在 ApSel 协商阶段就把 lane 给关了。你按规范老老实实实现结果在 A 家交换机上跑得好好的到 B 家就卡在 INITIALIZING 出不来——这种时候光看规范是找不到答案的。SONiCSoftware for Open Networking in the Cloud的价值就在这里。它是目前业界最主流的开源交换机网络操作系统之一整条光模块管理链路——从 I2C 读取、EEPROM 解析、CMIS 状态机驱动、数据路径配置到 STATE_DB 状态发布——全部以开源代码的形式摆在明面上。对固件工程师来说这相当于拿到了一份可执行、可断点、可打日志的主机侧参考实现。你在规范里读到主机应当轮询 ModuleState 直至 READY在 SONiC 里能直接看到它是隔多久轮一次、超时判多少秒、超时之后先降 LPMODE 还是先清 DataPath。这些东西是规范不写、但决定你固件能不能一次点亮的关键。更实际的一点是光模块厂商和交换机厂商之间最常见的扯皮就是是你固件的问题还是你主机配置的问题。有了 SONiC 这套参照你可以在自己实验室里跑一个虚拟交换机把模块挂上去观察 xcvrd 读到的每一个字节、每一个状态位、每一次状态机迁移。当你能拿着 STATE_DB 的 dump 和 xcvrd 的日志去和主机侧对话时沟通成本会下降一个量级。这篇文章面向的就是这类读者写过 CMIS 固件、被主机侧行为折磨过、或者正准备从 SFF-8636 迁移到 CMIS 的工程师。下面我按先讲清 CMIS 侧的地盘划分再拆 SONiC 的管理链路然后动手搭环境验证最后讲多 ASIC 拓扑配置和踩坑排查的顺序展开尽量给到可以直接抄作业的内容。2. CMIS 关键结构拆解先把自己这侧的边界划清楚2.1 页结构与寄存器地图的工程意义CMIS 的寄存器空间用页 bank的两级寻址来组织这个设计对固件工程师的意义远比表面上大。低页Page 00h是常驻页放的是模块标识、模块状态机、模块级控制位和旗标汇总任何时刻都能通过它读到你最关心的那几个字节上层页Page 01h 及以上必须先写页选择寄存器才能访问。常见的分层大致是Page 00h 放模块身份与状态Page 01h 放模块级能力与阈值Page 02h 放模块级实时监视量Page 10h 放各条 lane 的实时监视量Page 11h 放各条 lane 的阈值Page 20h 往后的若干页分别对应每条 lane 的 Advertisement 信息Page 2Fh 是 Staged Control SetPage 9Fh 那一片是 CDBCommand Data Block相关的命令通道。这里第一个容易踩的坑是页切换的原子性。主机读一个跨页的结构时通常是切页—读数据—切回低页三步走。如果你的固件在切页写时序上没有留够处理时间或者切页后立刻读返回的是旧数据主机侧拿到的就是污染过的内容。更麻烦的是多主竞争场景一个模块可能同时被 BMC、CPLD 和主机 SoC 访问页选择寄存器是全局共享的A 把页切到 0x11B 中间插进来切回 0x00A 再读就拿错了。所以实际工程里固件侧一般会做两件事一是保证切页写操作在一个 I2C STOP 之后立刻生效不要做异步延迟二是对关键的多字节结构尽量放在同一页内减少跨页依赖。第二个坑是 bank 寻址。CMIS 支持通过 bank 选择来访问多个 banked 页典型场景是多 lane 模块里每条 lane 有一份独立的 Advertisement 页。如果你的固件把 bank 语义实现错了主机在读取 lane 3 的广告能力时可能拿到 lane 0 的内容表现出来就是能识别模块但只有第一路能起来。这类问题的排查方法很直接用逻辑分析仪抓 I2C 波形看主机有没有写 bank 选择字节、写的是什么值、你的固件有没有按这个值切换内部指针。注意CMIS 不同大版本之间部分字节的语义和位置有过调整。写固件时不要拿一份几年前抄下来的 offset 表直接用对着你宣称支持的 CMIS 版本原文逐字段核对一遍尤其是 ModuleState 编码、DataPath 状态位和 VDM 相关的字段。2.2 模块状态机与数据路径状态机不是一回事很多人刚接触 CMIS 时会把 MSM 和 DPSM 混在一起理解觉得模块 READY 了就应该能出光。实际上这是两个层级完全不同的东西。模块状态机Module State Machine管的是整个模块的供电和初始化流程典型状态包括 RESET、LOWPWR、INITIALIZING、MODULE_READY、MODULE_FAULT 以及禁用态。数据路径状态机Data Path State Machine是每一条数据路径或者说每一组 host lane 与 media lane 的绑定独立维护的典型状态包括 RESET、DEACTIVATED、INITIALIZING、ACTIVATING、ACTIVATED、TX_DISABLE、TX_TURN_ON、TX_TURN_OFF、DEACTIVATING、DP_FAULT 等。理解这个分层的工程价值在于模块 READY 只代表我活着、能通信、能接受配置不代表我已经在发数据。主机要真正把业务跑起来必须走完 DPSM 的整个流程——先选择应用模式、配置 lane、使能输出、等待激活完成。反过来当你看到主机侧报模块识别正常但链路不通时第一反应应该是去查 DPSM 状态而不是怀疑模块状态机。从我实际调试的经验看DPSM 卡住的绝大多数原因集中在三处一是应用模式协商失败主机要求的能力在模块的 Advertisement 里没有对应项二是 lane 映射错位主机配的 host lane 和模块内部的 media lane 对应关系不一致三是 TX 使能顺序问题有的模块要求先配 lane 再开 TX有的要求先开 TX 再配 lane主机侧默认顺序和你的固件期望不一致就会卡住。2.3 Application Advertisement 与模式协商的实操细节CMIS 里把模块支持哪些工作模式通过 Advertisement 机制暴露出来每条 media lane 一组包含应用编码、Host Interface ID、Media Interface ID、lane 数量、速率能力等信息。主机侧读完这些广告项之后挑一个和端口配置匹配的写进 ApSelApplication Select寄存器模块收到后按这个选择去配置内部的数据路径。这部分最容易出问题的地方是能力声明和实际实现不一致。比如你在 Advertisement 里声明支持某个速率但内部固件其实没有对应的 CDR 配置主机选了这个模式之后 DPSM 就会一直停在 ACTIVATING 直到超时。还有一种情况是 Advertisement 里的 Host Interface ID 写错主机按规范去匹配时找不到合适项直接放弃初始化。实际操作上我建议的做法是先把模块支持的所有模式列一张表逐条和固件里的配置分支对齐确认每条声明都有可执行路径。然后在 SONiC 侧用虚拟环境读一遍 Advertisement看主机解析出来的候选列表是不是和你预期的一致。这个交叉验证做一次能省掉后面大量的反复。2.4 VDM 与告警阈值体系的取舍VDMVersatile Diagnostics Monitoring是 CMIS 相对 SFF-8636 一个比较明显的增强它允许模块上报更细粒度的监视量和更灵活的阈值结构。对固件工程师来说实现 VDM 的工作量主要在阈值表和采样通道的组织上模块级监视量放在 Page 02hlane 级放在 Page 10h阈值分别在 Page 01h 和 Page 11h。取舍点在于精度和资源。VDM 的采样频率、平均窗口、上报粒度的设计会直接吃 MCU 的算力和内存。如果你的模块用的是低端 MCU把所有通道都开成高频率采样可能导致 I2C 响应变慢反过来影响主机的轮询。我的经验是先保证主机实际会用到的通道通常就是光功率、偏置电流、温度这几类质量做扎实其余通道按规范最低要求提供即可不必追求全通道满配。主机侧真正告警判断依赖的往往是那几个关键量堆太多通道收益有限。3. SONiC 侧的光模块管理链路逐层拆解3.1 从 I2C 到 STATE_DBxcvrd 扮演了什么角色SONiC 里负责光模块管理的是 pmon 容器中的 xcvrd 进程。它的职责可以概括成一条流水线定期从模块 EEPROM 读取原始字节通过平台的 SFP 抽象层把字节解析成结构化字段再写入 STATE_DB 供上层命令和监控使用。这条流水线看起来简单但每一环都有值得固件工程师研究的地方。第一环是读取节奏。xcvrd 对不同类型的读取项用不同的周期静态信息厂商名、序列号、模块能力在启动时读一次或者低频刷新实时监视量温度、电压、光功率按秒级周期刷新状态和旗标则刷得更频繁一些。这个节奏直接决定了你的固件需要承受多高的 I2C 访问频率。如果你的模块在某个页切换逻辑上有 10 ms 的延迟在实验室单次读取时看不出来放到 xcvrd 的高频轮询下就会开始丢数据或者返回错值。第二环是解析层。SONiC 把字段定义和访问逻辑分开了内存映射mem map负责描述每个字段在哪个页、哪个偏移、多少位EEPROM 访问对象负责按映射去读。这个设计的最大好处是可测试——你可以脱离真实硬件把一份 EEPROM dump 直接喂给解析层验证字段解析对不对。对固件工程师来说这意味着你可以拿自己模块的 dump 去跑主机侧的解析逻辑提前发现我写的值和主机读出来的值不一致的问题。第三环是状态发布。解析出来的字段会被写进 STATE_DB 的几张表里典型的包括模块信息表、模块状态表、DOM 监视表、固件信息表键名一般是表名|端口名的形式。上层命令读到这些表之后做展示和判断。理解了这一层你在排查问题时就可以直接去 STATE_DB 里看原始值而不是只看命令的输出。3.2 CMIS API 的分层设计带来了什么便利SONiC 的光模块代码里CMIS 部分通常按通用接口 CMIS 专用实现的方式组织。通用的 SFP 基类定义了一组标准接口方法比如读取模块状态、读写 DOM、设置低功耗模式、设置 TX disable 等CMIS 专用的类则在这些接口之上补充了 CMIS 特有的操作比如读取应用广告、设置应用选择、查询数据路径状态、处理 CDB 命令等。这种分层对固件工程师有两个实际好处。一是它明确告诉你主机一定会调哪些接口你只要保证这些接口对应的寄存器行为正确基本盘就稳了。二是它暴露出主机侧的容错逻辑——比如某个字段读失败时主机是直接跳过这一项还是整个模块判定为异常这直接影响你固件的错误恢复策略设计。有些主机在单次读失败后重试三次才放弃有些一次失败就标记模块故障你的固件在这两种主机下的表现会完全不同。另外值得一提的是 CDB 通道。CMIS 的 CDB 提供了一条独立于常规寄存器的命令通道用于固件升级、模块诊断、厂商自定义命令等。SONiC 侧对 CDB 的支持程度在不同版本里有差异有的版本只实现了基础的固件版本查询有的版本支持完整的固件升级流程。如果你的模块要用 CDB 做在线升级建议先确认目标主机版本支持的 CDB 子集再决定固件里要不要做兼容降级逻辑。3.3 模块初始化流程的逐步拆解把主机侧的 CMIS 初始化流程拆开看大致会经过这么几步。第一步是上电和探测主机通过 I2C 尝试读取模块标识确认这是一个 CMIS 模块而不是老式的 SFF-8636 模块。这一步的判别通常靠标识字节和规范版本号如果这两个值和你实际实现的规范版本不一致后面的流程可能整个走偏。第二步是模块状态推进。主机写控制位让模块从 RESET 或 LOWPWR 进入初始化然后轮询模块状态等待 READY。这里的时间要求是硬指标规范里定义了各状态迁移的最大允许时间主机侧通常按这个值加一定余量设超时。如果你的固件在某个阶段需要加载大量配置、做 CDR 校准或者等内部 PLL 锁定耗时超过规范上限主机就会判定初始化失败。实际工程中常见做法是把耗时操作拆成异步状态机先推进到 READY重活放在后面按需做。第三步是应用模式选择。主机读广告、匹配端口的速率和接口类型、写 ApSel、读回确认。这一步的坑在于匹配算法不一定是完全相等有的实现会做近似匹配比如速率向上兼容。你需要确保广告项的粒度足够细让主机的匹配逻辑能落到你期望的那一项上。第四步是数据路径配置。主机按 lane 写入配置包括 lane 数量、速率、调制方式等然后触发数据路径初始化轮询 DPSM 状态到 ACTIVATED。这一步是整个流程里最容易出问题的地方因为涉及 lane 映射、TX 使能时序、CDR 锁定等多个环节。第五步是常态监视。初始化完成后主机会持续读取监视量和状态位检查告警和旗标。你的固件需要保证在这种常态化轮询下不出现数据错乱尤其是跨页读取和 bank 切换。3.4 一次完整初始化在时间轴上的样子把上面的步骤放到时间轴上大概是这么个节奏上电后几十毫秒内主机完成第一次探测读取随后几百毫秒内完成模块状态推进到 READY应用选择和数据路径配置通常在一两秒内完成具体取决于你固件的配置耗时之后进入秒级的常态轮询。整条链路从主机视角看应该在三到五秒内全部完成超出这个量级用户就会感知到模块起得慢。这个时间轴对固件优化的指导意义很直白把耗时最长的那一段找出来压下去收益最大。常见的大头是 CDR 校准和固件内部的初始化自检。如果你的平台允许把自检做成懒加载只在第一次真正要用某条 lane 时才做启动时间能明显改善。4. 动手搭一套能跑的实现对照环境4.1 虚拟交换机环境的准备思路要在没有真实硬件的情况下研究主机行为虚拟交换机镜像是最省事的路径。它的基本形态是一个容器镜像跑起来之后里面有一套完整的 SONiC 运行环境包括数据库、各个守护进程和管理命令。加载和启动的大致形态如下docker load docker-sonic-vs.gz docker run --rm -it --privileged --name sonic-vs \ -v /lib/modules/$(uname -r):/lib/modules/$(uname -r) \ docker-sonic-vs:latest需要说明的是不同版本镜像的启动参数、依赖挂载方式会有差异实际以你手上镜像附带的说明为准。跑起来之后可以先进容器看看关键进程在不在docker exec -it sonic-vs bash supervisorctl status show version如果 pmon 容器和 xcvrd 都在跑说明光模块管理链路是活的。接下来就可以用命令查看端口和模块状态。虚拟环境里通常没有真实模块所以模块相关的表可能是空的或者显示为不存在这属于正常现象我们的重点是研究链路结构而不是看具体数值。4.2 用命令行观察状态与寄存器SONiC 提供了一组命令来查看光模块信息常用的包括查看存在性、查看 EEPROM 内容、查看 DOM 数据这几类show interface transceiver presence show interface transceiver eeprom --dom sfpshow eeprom -p Ethernet0 sfputil show eeprom -p Ethernet0这些命令底层读的就是 STATE_DB 里的那几张表。想看得更底层一点可以直接查数据库redis-cli -n 6 keys TRANSCEIVER* redis-cli -n 6 hgetall TRANSCEIVER_INFO|Ethernet0 redis-cli -n 6 hgetall TRANSCEIVER_STATUS|Ethernet0STATE_DB 在 SONiC 里是 6 号库用-n 6指定。把表里的字段和 CMIS 规范里的字段对照着看你会很快建立起主机读了我哪个字节、解析成了什么值、发布成了哪个字段的完整映射。这个映射关系一旦建立起来后面看任何异常都会快很多。4.3 用离线 dump 做固件回归真实硬件不在手边的时候最有价值的做法是拿一份 EEPROM dump 离线跑主机侧的解析逻辑。思路很简单把 256 字节的字节流喂给解析层看它解析出来的字段和你固件里写入的值是否一致。下面是个简化示例展示用字节流驱动读取的基本形态class ByteReader: def __init__(self, data): self.data data def read(self, offset, size): return self.data[offset:offset size] class NullWriter: def write(self, offset, val): pass构造好读写对象之后配合内存映射对象就能做字段级验证。不同版本的构造签名可能不一样用之前先看一眼源码里的定义。这套做法的价值在于你可以把固件里每个页、每个 bank 的内容都 dump 成文件批量跑一遍解析自动比对期望值。相当于给固件加了一层主机视角的单元测试。提示dump 的时候尽量把低页和所有上层页都抓全包括 bank 切换后的内容。只抓低页的话很多能力字段和 lane 级信息是看不到的离线验证会漏掉一大块。4.4 日志与数据库的联合定位方法排查问题的时候单看日志或者单看数据库都不够。有效的方法是把两者按时间对齐看。xcvrd 的日志会记录读取动作、解析结果和状态迁移STATE_DB 里的值是最终落地的结果。当某个字段的值不符合预期时先在数据库里确认最终值再回日志里找这个值是什么时候被写入的、写入前读到的原始值是什么就能判断问题出在读取环节、解析环节还是固件侧返回的数据本身。具体操作上可以一边 tail 日志一边轮询数据库docker exec -it pmon bash -c tail -f /var/log/syslog | grep -i xcvrd同时在另一个终端里反复查询某张表观察字段值的变化。这种双窗口对比的方式对定位初始化卡在某个状态这类时序问题特别有效。5. 多 ASIC 场景下的拓扑与配置文件定义5.1 多 ASIC 环境里配置是怎么分片的多 ASIC 交换机的典型形态是一台设备里有多个交换芯片每个芯片有自己的端口集合、自己的转发数据库、自己的配置。SONiC 应对这种形态的方式是给每个 ASIC 一个独立的命名空间命名一般是 asic0、asic1 这样的形式配置上也拆成多份文件。在单 ASIC 设备上主配置通常是一份 config_db.json到了多 ASIC 设备上会变成按 ASIC 编号分片的多份文件各自对应一个命名空间。你可以通过查看网络命名空间列表来确认设备的形态ip netns list ls /etc/sonic/ | grep config_db如果看到 asic0、asic1 这样的命名空间以及 config_db0.json、config_db1.json 这样的文件那基本可以确定是多 ASIC 布局。理解这个分片结构对排查问题很重要有时候你在全局视角看到的端口和某个 ASIC 命名空间里看到的端口集合是不一样的前者是聚合视图后者才是真正落在这个芯片上的实际配置。5.2 端口与拓扑字段如何落到每个 ASIC拓扑定义的核心是端口清单通常会包含端口名、SerDes lane 编号、别名、索引、速率、FEC 模式、自协商开关等字段。一个简化后的形态大致是这样# name lanes alias index speed fec Ethernet0 0,1,2,3 Eth1/1 1 400000 rs Ethernet8 8,9,10,11 Eth1/2 2 400000 rs这里的 lanes 字段是整个配置里最关键、也最容易出错的一环。它描述的是主机侧 SerDes 的物理 lane 编号而这个编号需要和模块内部 host lane 的对应关系一致。在多 ASIC 场景下不同芯片管辖的 lane 范围不同所以端口命名和 lane 分配往往带上了 ASIC 的痕迹比如某些端口会以背板端口的命名形式出现对应芯片之间的互联通道。对光模块固件工程师来说这里的直接价值是它告诉你主机侧是怎么理解 lane 的。如果你的模块在四通道模式下工作而主机的端口配置把四条 lane 分配给了同一个端口那你的模块应该看到一次包含四条 lane 的数据路径配置请求如果主机把 lane 拆成了四个单通道端口你的模块会看到四次独立的配置请求。两种情形下固件需要走的分支完全不同弄错了就是能起来但带宽不对或者只有一路能起来。5.3 配置生成与校验的实践要点实际部署里配置文件通常不是手写的而是由模板加参数生成的。生成过程中会用到的输入包括硬件 SKU 标识、平台描述文件和端口清单。生成命令的大致形态是调用配置生成工具指定 SKU 和平台文件。生成出来的配置建议做两件事再上线一是格式校验确认 JSON 结构合法、必填字段齐全二是语义校验把端口清单和模块实际能力对一遍确认速率、lane 数量、FEC 模式这几项在模块的广告能力里有对应项。语义校验这一步经常被跳过但它是多 ASIC 环境下最容易省掉又最不该省的一环。因为多 ASIC 的配置生成链路更长、参与的角色更多端口清单和模块能力不匹配的情况相当常见。做一个简单的自动化检查脚本把端口清单里的每一项和模块 dump 里的广告项做交集判断能在部署前拦掉大部分问题。6. 常见问题与排查速查6.1 数据路径卡在初始化状态这是最高频的问题。表现是模块能识别、模块状态能到 READY但数据路径状态一直停在初始化或激活中最后超时。排查顺序建议是这样先确认应用选择写进去了没有读回 ApSel 寄存器看值是不是主机写的那个再确认模块对这个应用选择的支持是不是真的存在也就是广告项里有没有对应条目然后检查 lane 映射看主机下发的 lane 配置和你内部 media lane 的对应关系是否一致最后才怀疑 CDR 校准或者物理层问题。很多时候问题就在第二步。主机按端口的速率和接口类型去匹配广告项如果你的广告项里 Host Interface ID 或者 Media Interface ID 写得不标准匹配算法可能落到一个你没实现的模式上或者干脆匹配失败退回默认值而这个默认值你的固件里没有对应的初始化分支。6.2 应用选择协商失败的典型原因协商失败通常有几个固定的原因模式。一是广告项数量超了规范对广告项数量有上限写多了后面的项会被主机忽略甚至整个广告区被判定为无效。二是能力声明和实现不符前面提过声明了但没有对应配置路径。三是页切换时序问题主机读广告是跨 lane、跨 bank 的连续读取如果你的固件在切 bank 时有延迟主机读到的是上一条 lane 的内容匹配结果自然不对。排查这类问题最直接的办法是把主机读到的广告内容原样打印出来和为模块固件里写的原始字节做逐字节对比。只要有任何一个字节不一致就往那个方向查页和 bank 的处理逻辑。6.3 I2C 时序与页切换的隐患时序类问题是所有问题里最难定位的因为它往往不是必现。常见的隐患包括页选择寄存器写入后立即读取但固件还没完成切换、连续读时固件在字节之间插入的处理延迟超过主机容忍度、多主访问时页选择被抢占。定位手段主要是逻辑分析仪抓波形重点看三件事页选择写和后续读之间的时间间隔、读操作中间有没有异常的时钟拉伸、总线上有没有其他主的访问穿插。从固件设计角度能提前规避的做法是尽量让关键结构不跨页、把页切换做成同步操作、对多主访问场景做状态保护。如果模块会被 BMC 和主机同时访问建议在固件里对页选择寄存器做快照和恢复减少互相干扰。6.4 排查速查表现象优先怀疑方向快速验证方法模块完全识别不到I2C 地址或低页基础字段抓波形看是否有 ACK读低页首字节识别到但状态推不到 READY状态机迁移耗时超限时间戳对比规范上限READY 但数据路径不起来应用选择或 lane 映射读回 ApSel对比广告项只有部分 lane 工作bank 寻址或多 lane 配置逐 lane dump 监视量对比数值时对时错页切换时序或跨页结构反复读同一字段看一致性高频轮询下出错I2C 响应时间或缓存一致性降低轮询频率对比观察这张表里的每一条我都实际遇到过其中数值时对时错是最耗时间的因为单次测试往往正常只有长时间跑才复现。建议在固件开发阶段就加一个自检模式让模块自己连续读自己的某个字段几百次看有没有不一致的情况能在早期把这类问题挖出来。7. 一些实际做下来觉得有用的经验把 SONiC 当成参考实现来用最大的收益不是抄代码而是获得一个可以反复实验且行为一致的主机侧环境。我自己的习惯是每做一个新的 CMIS 模块先在虚拟环境里把整条链路跑通把每个阶段主机读到的字节都抓下来存成基线后面固件改版拿新版本再跑一遍和基线做 diff。这个做法能挡住很大一部分回归问题尤其是那些不影响单次读取、只在时序上出问题的改动。另外有个细节值得一提广告项的设计不要贪多。我见过为了兼容性更好把各种速率和接口类型都塞进广告区的做法结果主机匹配的时候反而选了一个非预期的组合导致数据路径起不来。广告项应该是我确定能做好的集合而不是我可能能支持的集合。少而准比多而虚要稳得多。多 ASIC 环境下的 lane 映射建议单独做一份对照文档把主机端口名、SerDes lane 编号、模块 host lane 编号、内部 media lane 编号这四层关系一一列清楚。这份文档平时看不出价值一旦出问题它能让你在几分钟内定位到是哪一层对不上而不是花几个小时在几个命名空间之间来回翻配置。踩过几次因为 lane 对不上导致只有一路出光的坑之后我是把这份表当成了项目必备交付物来维护的。
返回列表