搞嵌入式或者服务器运维的兄弟,一定遇到过这种场景:新买的PCIe固态硬盘插上去了,系统没识别;GPU明明亮了灯,但lspci里死活找不到;或者更诡异的是,设备在系统里能看到,但跑着跑着就消失了。这种时候,大部分人第一反应是查驱动、看dmesg,但往往忽略了最基础也最有力的工具——lspci。这篇文章,我就带你从lspci出发,把PCIe设备的拓扑结构一层层扒干净,搞清楚设备到底挂在哪条总线上、链路宽度和速率协商到什么状态、以及当设备“消失”时,系统内部到底发生了什么。
这篇文章适合三类人看:刚接触Linux下PCIe设备调试的嵌入式工程师、做服务器硬件维护的运维老哥、以及搞GPU/NVMe高性能计算平台搭建的同学。我会从lspci最常见的几个用法讲起,配合实际截图场景,把BDF号、总线编号、链路能力、Switch拓扑这些概念全部串起来。看完之后,你再遇到设备识别异常的问题,至少能快速定位是“链路没协商上”还是“配置空间读不到”,这两个方向的排查路径是完全不同的。
1. 从一列输出看透整棵PCIe设备树
1.1 先搞懂BDF号和PCIe总线编号规则
lspci最基本的一条命令就是不带任何参数直接跑,输出长这样:
00:00.0 Host bridge: Intel Corporation Device 2020 00:01.0 PCI bridge: Intel Corporation Device 2021 01:00.0 Non-Volatile memory controller: Samsung Electronics Co Ltd Device a809每一行的最前面,就是PCIe设备的“门牌号”,格式是BB:DD.F,也就是Bus:Device.Function。
- Bus:总线编号,范围0-255,由系统在枚举时分配。
- Device:设备号,范围0-31,是设备在某个总线上的槽位编号。
- Function:功能号,范围0-7,同一个物理设备下最多可以拆出8个独立功能。
这里最有意思的点是:PCIe总线编号不是物理上刻死的,而是系统启动时由固件或内核动态分配的。你可能注意到,CPU直连的PCIe控制器(Root Complex)分配的总线号是0,然后它下面每挂一个PCIe Bridge或者Switch,就会分到一段新的总线编号空间。比如上面那个例子,00:01.0是一个PCIe桥(PCI bridge),它把总线往下一级扩展,分配到了bus 1,然后是01:00.0这个NVMe盘挂在bus 1上。
这个机制可以类比成快递分拨中心:Root Complex就是总仓,它下面每个PCIe Switch或Bridge就是分拨点,分拨点再往下就是一个个末端网点。lspci的职责就是把整个分拨网络的分层关系完整打印出来,让你一眼看出哪些设备挂在哪个分拨点下。
这套编号规则理解透了,你就能回答一个高频问题——“为什么我的GPU是03:00.0,而固态硬盘是01:00.0,中间隔着的02:00.0去哪了?”答案往往很简单:02:00.0可能是一个没有挂任何设备的PCIe Switch下游端口,系统给这一段总线分配了编号,但实际是空槽,所以lspci默认不打印它。
1.2 -tv参数是把“树形”拓扑打出来的关键
单纯的列表模式只能告诉你有哪些设备,看不出它们之间的父子关系。这时候要上lspci最核心的拓扑参数:
lspci -tv输出结构大致如下:
-[0000:00]-+-00.0 Intel Corporation Device 2020 +-01.0-[01-02]----00.0 Samsung Electronics Co Ltd Device a809 +-02.0-[03]----00.0 NVIDIA Corporation Device 2230 \-03.0-[04-05]----00.0 Intel Corporation Device 3702看到那个[01-02]了吗?这是lspci -tv最有价值的信息之一。中括号里表示的是这一段PCIe桥管理下的总线编号范围。比如01.0这个PCIe桥,它管理bus 1到bus 2,bus 2下面挂的是NVMe盘;02.0这个桥管理bus 3,bus 3下面挂GPU。
这个输出的缩进层级,就是设备在物理拓扑中的真实挂载关系。缩进越深,离CPU越远。搞GPU服务器的人常用这个命令看一眼就知道:这张GPU是直连CPU还是挂在PCIe Switch下面。如果是挂在Switch下面,那还涉及Switch上下行端口的带宽共享问题,后面我会展开讲。
还有一个参数是-PP,可以显示桥的二级总线号(secondary bus)和从属总线号(subordinate bus),配合-tv用能更清晰地看总线空间分配:
lspci -tvPP建议习惯性写成lspci -tvPP,因为默认的-t只画了拓扑逻辑,-PP会把每个桥的总线编号范围标得更清楚,对理解总线段落划分非常有帮助。
2. 用lspci识别设备类型和Link状态
2.1 从class code看懂设备是干什么的
lspci输出中间那段字符串,比如“Non-Volatile memory controller”、“VGA compatible controller”,就是设备的类别名。这个类别由配置空间里的Class Code字段决定,内核会把它翻译成人能读懂的字符串。
这个字段在排查时极其有用。比如你在一个嵌入式板卡上插了个PCIe设备,但设备本身没有固件、没有驱动,lspci很可能打出来一串mmo的“Device 1234”(因为厂商名和型号在数据库里查不到)。但只要类别能识别出来,比如“Ethernet controller”或者“SATA controller”,基本就能确认设备默认工作状态是正常的,只是缺少驱动或ID数据库。
如果你看到“PCI bridge”这个类别,意味着这个设备是一个桥或者Switch,它可以继续扩展下级总线。而“Host bridge”则说明这是CPU内部的Root Complex组件,一般不是独立物理设备,而是代表CPU内部的总线入口。
查看更详细的类别信息可以用:
lspci -nn输出会带上PCI vendor ID和device ID的十六进制编码,比如:
01:00.0 Non-Volatile memory controller [0108]: Samsung Electronics Co Ltd Device [144d:a809]方括号里前4位是厂商ID(vendor ID),后4位是设备ID(device ID)。这两个ID是设备驱动匹配的核心依据,驱动通过ID table来决定是否绑定这个设备。嵌入式开发中,如果你的PCIe设备在Linux下不识别,第一步就是确认lspci -nn读到的ID,和硬件设计文档里的注册ID是否一致。ID对不上,说明固件侧的配置空间初始化有问题,驱动再怎么写都白搭。
2.2 用-vvv抓取链路协商状态
lspci最强大的一个隐藏技能是-vvv,它会把每个PCIe设备配置空间的关键寄存器全部dump出来。真正排查链路问题时,我最常用的是看这几段:
lspci -vvv -s 01:00.0重点关注LnkCap(Link Capability)和LnkSta(Link Status)。一段典型输出如下:
LnkCap: Port #0, Speed 8GT/s, Width x4, ASPM not supported LnkSta: Speed 8GT/s, Width x4这里的Speed就是当前协商到的PCIe速率代际,常见的对应关系是:
- 2.5GT/s => PCIe Gen1
- 5GT/s => PCIe Gen2
- 8GT/s => PCIe Gen3
- 16GT/s => PCIe Gen4
- 32GT/s => PCIe Gen5
Width是协商到的通道数(Lane数),常见x1、x4、x8、x16。
LnkCap显示的是设备端和端口本身能支持的最大能力,LnkSta显示的是上电或复位后实际协商出来的工作状态。这两个值一旦对不上,就能发现很多隐性问题。
举个例子,我遇到过一块PCIe Gen3 x4的NVMe盘,LnkCap里写的是“Speed 8GT/s, Width x4”,但LnkSta却是“Speed 5GT/s, Width x1”。这说明链路协商失败了,实际只跑在Gen2 x1,性能损失惨重。导致这个现象的原因包括:金手指接触不良、插槽物理磨损、PCB布线过长导致信号质量差,甚至是CPU的PCIe控制器本身端口损坏。当你看到LnkSta比LnkCap低一个档次的时候,第一个动作就是把设备拔下来重新插一次,很多时候金手指接触问题导致降速的概率非常高。
还要重点看这段:
LnkSta2: Current De-emphasis Level: -6dB, Equalization Complete, Equalization Phase 3PCIe Gen3以上的链路协商依赖均衡(Equalization)机制。如果看到“Equalization Complete”说明训练成功;如果停留在Phase 1或者Phase 2,说明链路训练卡在了信号调优阶段,大概率是链路信号质量差,或对端设备兼容性不佳。
2.3 用-s定位特定设备并查链路上下游
-s参数可以按BDF号过滤,只查看某一个设备的详细信息。但要注意一个细节:-s支持的格式很灵活,可以只填总线号、只填设备号,也可以填完整BDF。常见的写法:
lspci -s 00:01.0 -vvv # 查看桥设备 lspci -s 01:00.0 -vvv # 查看末端设备排查某个设备链路异常时,习惯做法是把上游桥和下游设备都打一遍。为什么要这样做?因为PCIe链路是由两端端口共同组成的,上游端口(比如CPU Root Port或Switch上游端口)在拓扑中的角色,决定了它是否支持带宽扩展、方向反转等特性。如果上游端口自身的能力不足,下游设备再强也只能迁就低版本。
举个典型例子:一块PCIe Gen4的GPU插在主板上,但主板的物理插槽实际由某颗PCIe Gen3的Switch扩展出来。这时lspci -s 03:00.0 -vvv看GPU本身,LnkCap能显示Gen4能力,但LnkSta会显示协商到Gen3。这类问题不是因为设备坏了,而是拓扑本身限制了链路速率,属于“插错位置”导致的性能瓶颈。用lspci把每个桥的LnkCap打出来,拓扑中的短板一目了然。
3. 实战:从lspci输出构建完整拓扑图的步骤
3.1 用sysfs配合lspci确认设备父子关系
虽然lspci -tv已经给了我们树形图,但它本质上是从内核的PCI总线结构里读取的,并不是读取了硬件上的物理连线。为了完全确认设备之间的父子关系,可以结合sysfs来验证:
ls /sys/bus/pci/devices/0000:01:00.0/sysfs里会暴露几个关键目录和文件:
parent:指向该设备的父级设备(通常是PCIe Bridge端口)subordinate:仅桥设备才有,表示它管理下的最大总线号driver:设备当前绑定的驱动config:配置空间的二进制镜像,相当于lspci -xxx的数据源current_link_speed、current_link_width:当前链路协商的速率和宽度,和lspci -vvv里LnkSta一致
最实用的一个验证方法是:
cat /sys/bus/pci/devices/0000:01:00.0/current_link_speed cat /sys/bus/pci/devices/0000:01:00.0/current_link_width这两个文件是实时反映链路状态的,比lspci重新加载配置空间更直接。在排查链路降速问题时,我习惯先用这两个文件做快速判断,再用lspci -vvv对比两端的LnkCap和LnkSta。
另外一个sysfs里的隐藏帮手是/sys/kernel/debug/pci下的调试信息(需要挂载debugfs):
mount -t debugfs none /sys/kernel/debug cat /sys/kernel/debug/pci/0000:01:00.0/device这里能看到lspci不直接输出的更底层信息,包括ASPM状态、TR检错机制、可选的Completion Timeout设置等。嵌入式平台调试早期,这个目录的价值非常明显。
3.2 手把手:一张带链路速率的完整拓扑表
实际交付或者写排查报告时,我不会只给一张lspci -tv截图,因为这不够直观。我建议你按这个步骤做一张“带链路状态的拓扑表”:
第一步,先抓原始拓扑:
lspci -tvPP第二步,逐个抓关键设备链路状态:
for d in $(lspci | awk '{print $1}'); do echo "=== $d ===" lspci -s $d -vv | grep -E "LnkCap|LnkSta" done在服务器终端上跑这个循环,不要加-vvv,用-vv就够了,加上-grep过滤输出速度会快很多,几百个设备也不会太卡。
第三步,把每个设备的Speed和Width填进表格。
以一台典型双路服务器为例,整理出来的表格大概长这样:
| 设备BDF | 设备类型 | 上游桥 | 当前速率 | 当前通道数 | 最大能力 |
|---|---|---|---|---|---|
| 00:01.0 | PCIe Bridge (CPU Root Port) | - | 8GT/s | x16 | 8GT/s x16 |
| 01:00.0 | NVMe SSD | 00:01.0 | 8GT/s | x4 | 8GT/s x4 |
| 01:02.0 | PCIe Switch上游端口 | 00:01.0 | 8GT/s | x16 | 8GT/s x16 |
| 02:00.0 | 下游端口1 | 01:02.0 | 8GT/s | x8 | 8GT/s x8 |
| 02:01.0 | 下游端口2 | 01:02.0 | 8GT/s | x8 | 8GT/s x8 |
第三步的关键点在于判断瓶颈。如果上游桥是x16的通道数,下游设备全是x4的盘,那确实没有瓶颈问题;但如果下游挂的是两块GPU,每一块都要x16,而Switch下游端口只提供x8,那么每块GPU只能获得x8的带宽。这就是拓扑层面导致的带宽损失,任何驱动优化都改变不了。
这类情况在带PCIe Switch的高密度GPU服务器上非常常见。设计者为了让更多设备插入同一个CPU,牺牲了单设备的通道数,换来的是更多设备数量。用lspci的输出去反推物理拓扑设计意图,是硬件工程师和系统运维对齐信息的高效手段。
3.3 用setpci读配置空间,看更多lspci不给的信息
lspci已经做了很多解析工作,但有些原始寄存器值还是要靠setpci直接读配置空间才能看到。比如想确认某个设备的厂商ID是不是真的:
setpci -s 01:00.0 0x00.w读出来的四位十六进制就是Vendor ID。0x00.w表示从配置空间偏移0x00开始读一个word(2字节)。同理:
setpci -s 01:00.0 0x04.w这个读的是Device ID。
可能你会问,lspci -nn不是已经显示了吗,为什么还要setpci?原因有两个:
第一,lspci依赖内核的PCI子系统初始化完成,如果设备在枚举阶段就出问题,lspci输出里可能完全不显示,但setpci只要总线号、设备号、功能号存在,就能强行去读配置空间,不受驱动绑定影响。这在设备“枚举失败”但“物理链路存在”的排查场景下极为有用。
第二,lspci -xxx虽然能dump整个配置空间头部,但我想快速确认某一位寄存器(比如Link Control里的ASPM开启位)时,setpci更精准、更容易脚本化。
举个例子,LnkCap在配置空间里的偏移地址一般是0x0c(PCIe Capability结构里),但不同设备Capability的起始偏移不同,不能盲读。好在lspci -vvv已经把这些信息解析成了可读文本,日常debug优先用lspci。只有遇到lspci也解析不出、或者需要下修寄存器测试某种状态时,setpci才该出场。
4. 场景实战:从拓扑到故障,那些lspci能帮你定位的事
4.1 设备“掉卡”时怎么看拓扑和日志
很多搞AI服务器的人对“掉卡”这个词不陌生。GPU跑着跑着从系统里消失,lspci一开始能看到,后来看不到了;或者nvtop、nvidia-smi都找不到卡,然后必须重启才能恢复。这个问题在PCIe层面往往表现为链路不稳定导致Surprise Down或Link Down事件。
遇到这种时候,第一步永远是:
dmesg | grep -i "pcie"典型错误码包括:
PCIe Bus Error: severity=CorrectedPCIe Bus Error: severity=UncorrectedAER: Corrected error received: id=01:00.0nvme ... link is down
然后lspci就该派上用场了。如果设备BDF还在,但链路状态是down,lspci -vvv的LnkSta区域会显示Link Status为down。如果BDF整个消失,说明上游桥已经把这个设备从总线枚举空间里移除了,这是比链路down更严重的事件,通常代表物理链路已经高过阈值,进入了“Link Down”不可恢复状态。
定位是哪个桥出的问题,看lspci -tv就够了:消失的设备和它的上游桥的从属总线范围一对比,就能知道是哪一段物理链路断掉了。比如GPU挂在03:00.0,它上游是00:01.0这条Root Port,管理bus 3。如果lspci里00:01.0还在但03:00.0没了,那问题就发生在00:01.0 -> GPU之间,范围缩小到一根物理插槽或RISER板。
这类问题的物理排查手段通常是:重新插拔、换RISER槽位、清洁金手指、检查主板/转接线的信号完整性。lspci帮你划定了排查范围,剩下的电工活儿就得手动干了。
4.2 用lspci辅助判断热插拔问题
热插拔这个词这两年越来越热,PCIe热插拔(Hot-Plug)在服务器里是标准能力。但嵌入式设备上要启用热插拔功能,主控侧需要Root Port支持Hot-Plug能力,下游设备还需要有对应的Presence Detect机制。当热插拔表现异常时,lspci同样能扮演关键角色。
热插拔的设备在拔出后,该设备BDF应该从lspci列表里消失,同时上游桥的总线范围会收缩;重新插入后,又会重新枚举、重新分配BDF。如果拔掉设备后lspci里还能看到这个BDF,大概率是Slot的Presence Detect管脚信号异常或Hot-Plug控制器驱动没正确响应。
另外,热插拔场景下最容易遇到的是设备重新枚举后总线号变了。比如第一次插入是04:00.0,拔掉再插变成06:00.0。这本身是正常的,不表示硬件出错,但如果你的软件配置通过BDF号硬编码绑定了设备,就会出现找不到设备的情况。正确做法是用设备的厂商ID+设备ID+socket物理位置联合定位,而不是固定写死BDF。
用lspci验证热插拔是否恢复正常,只要看链路协商结果:
lspci -vvv -s 06:00.0 | grep LnkSta如果LnkSta速度和宽度都恢复到了预期值,热插拔就算成功了。
4.3 带宽和稳定性兼容性问题的lspci观察法
热搜词里反复出现“PCIe稳定性/兼容性问题”,这类问题的共性是:设备能被识别,但高负载下出错率飙升,或者性能远低于标称。lspci查LnkSta是一种静态判断,还有一种动态判断方法就是跑负载的同时反复读取current_link_speed和current_link_width。
在脚本里可以这样:
while true; do cat /sys/bus/pci/devices/0000:03:00.0/current_link_speed cat /sys/bus/pci/devices/0000:03:00.0/current_link_width sleep 1 done配合FIO压测或GPU算力压测,如果链路速率在压力下掉到Gen1或者宽度减半,说明链路的信号裕量不足,触发了PCIe的降速/降宽度机制。这种情况下,lspci成为稳定性监视器,比你抓AER日志要直观得多。
另一种兼容性问题,典型现象是板卡插到A机器能识别,插到B机器就枚举失败。这种时候把两台机器各自的lspci -tv和LnkCap打出来对比,重点看链路两端共同支持的速率代际有没有交集。如果一方只支持到Gen4,另一方最高只支持Gen3,双方协商的公共交集就是Gen3,理论上不该出现枚举失败。一旦枚举失败,多数不是速率协商的问题,而是Initialization Pattern、参考时钟(Refclk)格式、或者边带信号(PERST、CLKREQ)的时序不匹配。lspci能告诉你结果不对,但要靠硬件设计文档去推断为什么不对。
5. lspci排错中容易忽略的细节与脚本技巧
5.1 设备列表里的bridge设备别当成无关信息
很多朋友看lspci列表,注意力都在GPU和NVMe盘上,桥设备直接跳过。这其实是个坏习惯。桥设备的LnkSta和LnkCap决定了它下游所有设备的带宽上限,甚至能反映整条链路的热状态。
比如CPU Root Port的LnkCap如果是x16,但某个下游Switch上游端口的LnkSta协商成了x8,这块Switch下面所有设备的总带宽直接少了一半。桥设备的问题还会成片影响——一个Switch带8个NVMe盘,如果它的协议转换或内部仲裁逻辑出故障,8个盘都跑不动,单看某一个盘的lspci完全找不到原因。先看上游桥状态,往往能帮你少走很多弯路。
5.2 写个一键脚本:一键打印全链路拓扑和协商状态
配合嵌入式产品量产时的产测需求,我通常会把lspci的检查流程固化成脚本,便于产线工位快速校验:
#!/bin/bash # 打印所有PCIe设备的BDF、类型、速率、宽度 for d in $(lspci -D | awk '{print $1}'); do speed=$(cat /sys/bus/pci/devices/$d/current_link_speed 2>/dev/null || echo "N/A") width=$(cat /sys/bus/pci/devices/$d/current_link_width 2>/dev/null || echo "N/A") desc=$(lspci -s $d | cut -d' ' -f2-) echo "$d | $desc | $speed | $width" done这个脚本在产测里的思路是:固定型号的主板和CPU,只要lspci输出的设备数量和每个BDF的协商速率都在预期范围内,就说明PCIe树建立是健康的。比单纯看设备能不能起来要多一层验证。
5.3 别忽视ACPI和电源状态对lspci输出结果的影响
最后一个容易被忽略的点是PCIe设备电源状态。设备处于D3冷状态时,lspci -vvv里的LnkSta往往显示的是协商前的状态,甚至可能读不到完整的LnkCap。有些设备在睡眠后重新唤醒,链路协商会重新进行,速率可能和冷启动时的结果不同。
所以对比两套系统之间的lspci输出时,要确保双方设备都处于同一电源状态,最好都在D0状态、且没有ASPM在中间捣乱。否则你查到的差异可能不是硬件性能差异,而是电源状态差异带来的假象。
调试中我经常用这个命令强制设备回到D0:
echo 0 > /sys/bus/pci/devices/0000:03:00.0/power/control然后重新读lspci。如果链路状态恢复正常,说明整套电源管理和链路训练配合良好;如果还是不行,再去查硬件信号也不迟。这一步成本几乎为零,但能过滤掉大量“因为电源状态没到位导致的误报”。
文章写到这里,核心的lspci拓扑解析方法也算讲得比较全了。我自己在实际调试中最大的体会是:遇到PCIe疑难问题时,一定要先问自己两个问题——这个设备在总线上到底存不存在(枚举问题),以及它当前链路协商到了什么状态(链路问题)。前者看lspci -tv的树形结构,后者看lspci -vvv的LnkCap/LnkSta。把这两个问题搞清楚了,排错范围就缩小了一大半。最后再分享一个小经验:做嵌入式板卡调试时,建议在开机阶段就用lspci -vvv把每块板卡的链路信息保存一份归档,一旦后续现场出了问题,和归档数据一对比,是硬件老化还是批次性物料问题,基本就能看出方向了。