说实话,在AI Infra这个岗位上待久了,lspci应该是我敲得最频繁的命令之一。新机器到位先敲一遍看GPU/NIC在不在域;驱动装完再敲一遍确认driver绑定;线上掉卡的时候更是要反复敲。但绝大多数时候,我们只看它的输出,很少去想Linux内核在背后做了多少事。今天这篇AI Infra每日一问的Day 11,就想把这个最常见的问题聊透:当你在终端里敲下lspci的一瞬间,内核里发生了什么?
这个问题并不只是“AI Infra八股”。lspci的数据不是凭空来的,它背后是Linux内核从固件枚举PCI设备、建立sysfs设备模型、再到通过file_operations把配置空间暴露给用户态的完整链路。搞懂这条链路,你在GPU服务器排障、内核升级、设备直通这些场景里会少走很多弯路。
这篇文章我会按一次真实执行lspci的路径,从用户态命令拆到内核的PCI子系统,再聊到file_operations动态拦截、透明加密这类安全模块为什么会出现在同一条链路上,最后附上几张我自己排障时常用的操作表。适合刚接触AI Infra的运维、做内核相关开发的同行,以及单纯想搞明白“命令背后发生了什么”的人。
1. 先搞清楚 lspci 到底在问谁
1.1 敲下命令后:用户态程序的入口
lspci是pciutils包里的工具,它自身不感知设备,只是通过libpci向内核要数据。libpci的初始化逻辑并不复杂:先扫描可用的访问方法,比如sysfs、proc/bus/pci、x86 IO端口直读,然后选择优先级最高的一个。现代主流Linux发行版上,sysfs几乎总是首选,所以我们可以把lspci等同于“读sysfs树+解析配置空间”。
普通用户执行的lspci只会显示vendor/device/class等最基本的字段,这些字段大多来自/sys/bus/pci/devices/目录下每个设备的vendor、device、class文件。完整的信息比如BAR、IRQ、扩展配置空间,则需要读取config文件,而config通常只有root或具有CAP_SYS_ADMIN的进程能打开。所以如果你在一个普通容器里执行lspci -vvv,大概率会看到“Permission denied”的警告,这是第一个值得记住的现象。
提示:看到“Permission denied”不代表内核没数据,只是sysfs节点的权限限制。生产环境建议用privileged容器或者通过主机的root执行,不要图省事chmod 777。
1.2 sysfs:内核暴露设备模型的一面镜子
在Linux里,sysfs是一个基于内存的虚拟文件系统,挂载在/sys。它的目录结构对应内核里的设备模型:/sys/bus/pci/devices/下面每个子目录都以“域:总线:设备.功能”命名,比如0000:3b:00.0,这就是一个PCIe设备的BDF地址。目录里的属性文件由内核PCI子系统注册,背后对应的是struct pci_dev的成员字段。
lspci读取vendor文件时,内核返回的是pci_dev->vendor的数值,再被lspci通过pci.ids数据库翻译成“NVIDIA Corporation”这样的字符串。这里的“读文件”不是一个比喻,而是真正的文件系统读操作,会发生路径解析、inode查找、权限检查、open/read系统调用。换句话说,lspci本质上是一个非常特殊的文本处理程序。
这个设计对AI Infra很重要。比如你想快速遍历一台8卡机器上有哪些PCIe设备,不需要自己写内核模块,用shell循环读/sys/bus/pci/devices/*/vendor就够了。这也是为什么很多GPU巡检脚本都喜欢基于sysfs做,而不是去解析lspci文本,因为文件接口稳定且不依赖locale。
1.3 从目录项到内核对象:kobject 给了名字,pci_dev 给了属性
sysfs里的一个目录,在内核中并不是一个真实的磁盘目录,而是一个kobject。每个设备在注册时会创建一个kobject,名字就是BDF。而设备本身的类型、属性、show/store回调,则由更上层的struct device和struct pci_dev描述。
sysfs的属性接口有两种:一种是设备驱动通过device_create_file注册的普通属性,另一种是总线/驱动核心注册的默认属性。PCI设备的config就是后者,它对应的show回调会读取PCI配置空间并拷贝到用户缓冲区。kobject/ktype在这里的作用是提供统一的release、uevent、属性组管理等机制。理解这一点,你就可以明白为什么用rmdir去删/sys里的设备目录不会成功,因为那根本不是你能控制的东西,它反映的是内核里的设备状态。
2. 内核里的 PCI 子系统:设备从哪来,lspci 又依赖什么
2.1 枚举:硬件树其实是固件先搭好的
在x86平台上,PCI/PCIe设备的发现顺序大体是:上电后由BIOS/UEFI固件扫描总线,为每个设备分配BDF、BAR地址、中断等资源,然后内核在启动早期读取配置空间并建立pci_dev对象。这段枚举逻辑在Linux内核源码的drivers/pci/probe.c里,核心函数是pci_scan_bus/pci_scan_single_device。内核通过访问配置空间的CF8/CFC IO端口(传统PCI)或ECAM内存映射(PCIe)来遍历总线。
有一个容易被误解的点:lspci执行时并不会触发内核重新扫描硬件。它只是把内核启动时已经枚举出来的设备树展示出来。如果你在系统运行中拔插了一张卡,然后想在不重启的情况下让内核发现它,需要主动触发rescan,比如写1到/sys/bus/pci/rescan,或者用echo 1 > /sys/bus/pci/devices/.../remove然后重新扫描。否则lspci不会看到新设备。
注意:生产环境热插拔GPU后,一定记得先确认驱动已经卸载,再写remove文件。顺序反了可能导致内核访问已移除的设备,出现AER报错甚至主机hang住。
2.2 配置空间、BAR 与 MMIO:GPU 显存怎么被 CPU 看到
每个PCIe设备至少有一个256字节的配置空间,PCIe还扩展到了4096字节。配置空间最前面是Vendor ID、Device ID、Class Code、Status、Command等公共字段,后面是BAR(Base Address Register)。BAR保存的是设备内部资源被映射到CPU物理地址空间的基地址和大小,比如显存、门铃寄存器、MSI-X表等。
当lspci -v显示“Region 0: Memory at fa000000 (64-bit, prefetchable) [size=256M]”,意思就是该设备BAR0占用了从0xfa000000开始的256MB物理地址。GPU驱动probe时会把这块区域ioremap到内核虚拟地址空间,CPU随即能通过这块地址访问显存。AI Infra中如果看到某个GPU的BAR大小为0或地址异常,通常说明固件没有正确分配资源,常见原因包括没开启Above 4G Decoding、PCIe slot供电不足、或者主板的PCIe switch有缺陷。
BAR资源的分配也非常依赖内核的pci_resource_*系列函数。驱动不应该自己去猜地址,而是调用pci_resource_start(pdev, bar)获得物理地址,再ioremap。这个“不自己猜”的原则在内核开发里是底线,因为资源可能被固件、内核realloc和IOMMU重新安排过。
2.3 总线-设备-驱动:三者如何匹配
内核设备模型用bus_type、device、driver三个核心对象描述硬件关系。PCI设备注册时,会把自己的vendor/device/class等信息挂在device上;PCI驱动注册时,用pci_device_id表声明自己支持哪些设备。内核的匹配逻辑就是遍历总线上的设备,比对ID表,匹配成功则调用驱动的probe函数。
lspci -k里显示的“Kernel driver in use: nvidia”就是匹配成功的证据。它本质上是读设备目录下driver符号链接指向的驱动名。我们日常排障时,如果lspci能看到设备但“Kernel driver in use”为空,通常有几种可能:驱动没装、驱动模块没有自动加载、或者设备被driver_override锁定了。这时候可以查看/sys/bus/pci/devices/ /driver_override文件,有时虚拟化平台会利用它强制绑定某个vfio-pci驱动。
3. 敲下 lspci 的一瞬间:一条包含 file_operations 的完整链路
3.1 从终端到内核:openat 与 VFS 的一次握手
执行lspci后,libpci会调用open()打开/sys/bus/pci/devices/0000:3b:00.0/config。这个open会转化为openat系统调用(现代glibc统一用openat),进入内核。内核VFS层根据路径逐级查找目录项,得到对应的inode,然后通过该文件系统注册的file_operations完成打开动作。
关键点在于:sysfs里看到的文件并不是普通磁盘文件,它没有块设备、没有磁盘缓存。它的inode在内存中生成,inode->i_fop指向sysfs/kernfs自己的file_operations。正因为所有sysfs文件共用一套file_operations,内核才能在open时根据属性类型分发到不同的show回调函数。这也是很多安全模块选择拦截file_operations的原因:只要把inode或struct file里的f_op替换一层,就能在不改sysfs核心代码的情况下,在所有属性读写上加钩子。
3.2 sysfs 读路径:kernfs 与 show 回调
展开看读config的路径:进程调用read()后,内核入口是ksys_read,经过VFS,到达kernfs_fop_read_iter。kernfs会把用户的缓冲区和一个seq_file缓冲区绑定,然后调用kernfs_seq_show,最终执行到属性文件注册时的show回调。对PCI config属性来说,show回调是pci_read_config,内部调用pci_user_read_config_*函数,按offset读取配置空间的字节,再拷贝给用户态。
这套多级转发看起来很绕,但它带来了一个好处:用户态从来不需要知道如何直接访问CF8/CFC端口或ECAM地址,内核把底层硬件差异全部封装了。在虚拟化平台上,config的读取甚至会被Hypervisor拦截并返回虚拟化后的数据,guest里的lspci看到的和宿主机看到的可以不一样。这对我们用SR-IOV、设备直通时理解“设备到底透传了什么”特别有帮助。
3.3 file_operations 动态替换:透明加密和审计为什么能插进来
在AI Infra场景里,sysfs的这个读路径并不一定永远“干净”。很多发行版和安全产品会通过内核模块在设备文件或sysfs文件打开时替换struct file->f_op,从而在read/write前后加入自己的逻辑。最常见的用途是透明加密:拦截对某个文件内容的read/write,自动解密/加密;也用于审计:记录谁在什么时间读了哪个设备属性。
实现原理不复杂:模块在open阶段保存原始f_op,把当前file的f_op指向自己的包装结构,在包装的read里先做检查,再调原read。但动态替换f_op在Linux内核里属于“高危行为”,需要读持有file spinlock或者通过security_file_open钩子更安全。内核社区并不是一味反对所有拦截,而是要求钩子设计必须可追溯、不破坏默认语义。我们自己在做内核改造时,也尽量优先使用LSM hook、tracepoint、fprobe这些更规范的机制,而不是一上来就替换f_op。
实操心得:我在排查一次“为什么容器里lspci读到的是加密后的设备名”问题时,最后发现是某agent在sysfs读路径上做了透明加密,只对特定进程解密。这类问题用strace看不出来,需要在kprobe里把f_op地址打出来对比。遇到诡异行为,先怀疑file_operations是否被人为替换过。
3.4 数据组装:从配置空间字节到人类可读文本
内核返回给lspci的只是一串raw bytes,比如config前64字节里的vendor=0x10de、device=0x2684。pciutils拿到这些字节后,会去/usr/share/hwdata/pci.ids或/usr/share/misc/pci.ids里查表,把ID翻译成“NVIDIA Corporation”和“GA102 [GeForce RTX 3090]”。pci.ids这个文件可以直接编辑,所以如果你在一台机器上看到厂商名显示为“Unknown”,先检查pci.ids是否存在、更新日期是否太旧。
另外,lspci显示的很多信息其实不是直接读config,而是对配置空间的解析。例如Class Code决定设备类型(0x0300代表VGA compatible controller),子系统ID从配置空间0x2C偏移读取,IRQ则可能同时来自config和内核中断子系统。理解这些字段的offset,在写监控脚本或者做性能分析时会很有用。
4. AI Infra 排障实录:lspci 常见问题与内核层面的排查
4.1 一张速查表:lspci 不对劲时先看哪里
我把自己在GPU服务器上遇到最多的问题整理成了一个表格,这也是我团队新人培训时会发的那一页。
| 现象 | 可能原因 | 排查命令/动作 |
|---|---|---|
| lspci看不到某张GPU | PCIe链路训练失败、供电不足、固件禁用 | dmesg搜“link down”,检查物理槽位,BIOS里关闭Fast Boot |
| 能看到设备但没有驱动 | 驱动未安装/module未加载 | lspci -nnk 查driver,modprobe nvidia,检查DKMS |
| BAR地址显示为0 | 固件资源分配失败 | 查BIOS Above 4G Decoding,dmesg搜resource |
| 读config时Permission denied | 非root/容器无CAP_SYS_ADMIN | 主机root执行,或在privileged容器执行 |
| lspci -vvv卡住 | 设备D3冷状态或软件锁 | strace看block在哪个fd,尝试echo 1 > /sys/bus/pci/devices/.../remove再rescan |
| 设备信息显示Unknown | pci.ids太旧 | update-pciids更新数据库 |
| 设备在lspci里重复出现 | SR-IOV的VF和PF同时列出 | lspci -nn -d 厂商ID,按BDF区分 |
这张表的价值在于:不要只盯着lspci的输出,它在很多问题里只是“侦探”,真正的线索在dmesg和系统日志里。
4.2 内核参数与固件:lspci 背后的隐形开关
很多PCIe问题并不是硬件坏了,而是内核参数或固件配置不对。比如IOMMU开启时,设备DMA地址被重新映射,lspci看到的BAR物理地址可能不再是直通的物理地址;对AI训练卡来说,这会影响是否能把GPU显存直接暴露给用户态。常见内核参数有pci=realloc(允许内核重新分配资源)、pci=noaer(关闭Advanced Error Reporting)、pci=assign-busses等。不要随便在生产环境加这些参数,一定要先理解影响面。
还有一个常被忽略的点:PCIe AER错误。当设备出现可修正错误时,内核会记录到dmesg,但lspci本身不会提示。所以排查掉卡问题时,正确姿势是同时跑lspci -vvv和dmesg -T | grep -i pci,只看一边很容易被误导。如果是NVMe或GPU出现大量AER,优先查链路速率、供电、PCIe retimer。
4.3 自己写一个最小版“lspci”:直接读配置空间
为了加深理解,建议你亲手写一个最小的读取工具。以下Python代码可以直接读取指定BDF的配置空间前64字节,解析出Vendor ID和Device ID:
import os, struct bdf = "0000:3b:00.0" path = f"/sys/bus/pci/devices/{bdf}/config" try: with open(path, "rb") as f: cfg = f.read(64) vendor, device = struct.unpack_from("<HH", cfg, 0) print(f"BDF {bdf}: vendor=0x{vendor:04x}, device=0x{device:04x}") except PermissionError: print("需要root权限读取config")运行前先确认BDF存在(ls /sys/bus/pci/devices/)。这段代码虽然简单,但把“用户态通过sysfs访问配置空间”这条路径走了一遍。如果你想复现lspci -xxx的效果,直接把config文件全部读出来,按PCI配置空间标准解析即可。写入config则要更小心,用setpci命令或ioctl,不建议新手直接改。
5. 从 lspci 延展到 AI Infra:这些内核知识到底怎么用
5.1 GPU/NIC 排障里的组合拳
lspci从来不是单独用的。我常用的一组命令:
- lspci -nnk | grep -iA3 -E 'nvidia|mellanox':快速看设备ID和内核驱动。
- lspci -vvv -s 3b:00.0:看链路状态LnkSta、带宽、MaxPayload、ASPM。
- lspci -t:看PCIe拓扑,理解设备挂在哪个root port下。
- setpci -s 3b:00.0 COMMAND=06:修改设备的Command寄存器。这个操作会暂时禁用/启用IO和内存访问,千万别在训练任务跑着的时候用。
拓扑信息对多卡通信非常关键。AI服务器里GPU和NIC的PCIe树结构直接决定跨卡通信是否经过CPU、是否需要抢占同一根root port带宽。lspci -t只能看到静态拓扑,要完全理解带宽瓶颈,还得结合nvidia-smi topo -m看实际互联矩阵。
5.2 内核版本、驱动与固件的三角关系
AI Infra日常最多的问题之一:升级内核后NVIDIA驱动失效。这是因为NVIDIA等GPU驱动以内核模块形式动态加载,模块必须和内核版本、内核配置严格匹配。新的内核可能改变了PCI子系统的内部API,老模块无法加载。lspci这时依然能显示设备,但“Kernel driver in use”会变成空,dmesg里往往有“version magic”或“unknown symbol”报错。
固件层面,设备自身的VBIOS、网卡固件也需要与驱动配合。lspci读取的Class Code、子系统ID可能与固件版本相关。遇到奇怪的资源冲突,先检查固件版本,再检查内核参数,最后才考虑硬件故障。这个顺序能省下很多无用功。
5.3 值得继续深挖的几个方向
如果你看完这篇想去读内核源码,我建议按这个顺序来:
- drivers/pci/probe.c:了解PCI设备枚举和资源分配。
- drivers/pci/access.c:了解config空间访问的各种方法。
- fs/kernfs/:理解sysfs的读写路径。
- drivers/pci/quirks.c:看真实世界里硬件bug如何被内核兼容。
另外,内核里的file_operations动态替换机制也是理解容器安全、透明加密、审计系统的一把钥匙。这些年内核社区提供了更多偏好的方式,比如LSM、fprobe、tracepoint,但底层f_op的替换思路仍然被不少产品使用,检查这类行为也成了我排查诡异问题时的保留节目。做AI Infra不只是会敲命令,能看懂内核在这一层做了什么,排障效率是真的不一样。
最后聊一点我自己的体会。lspci这个命令太常用,以至于我刚入行时也觉得它没什么可研究的。直到有次线上GPU“消失”,lspci明明看得到设备但nvidia-smi报找不到,我顺着sysfs路径一层层剥下去,发现问题根本不在PCIe枚举,而是某个agent在sysfs read路径上加了过滤,把特定设备的config读返回了空数据。那次以后我再不把lspci当黑盒。你在实际排障中如果也遇到“命令输出正常但业务异常”的情况,不妨先strace一下,再看dmesg,如果都正常,就往内核模块和钩子上想想。这套思路比背多少条lspci参数都管用。
好了,Day 11的“每日一问”就聊到这儿。如果你也在做AI Infra相关的工作,欢迎把你遇到的lspci怪事翻出来对照看看,大概率能发现一些平时被忽略的线索。