1. 项目概述:从敲下lspci到看到设备列表,这不到半秒里内核到底干了什么?
你有没有在终端里敲过lspci?回车一按,几毫秒后,一长串设备信息就刷出来了:显卡、网卡、USB控制器、SATA主控……整齐排列,带总线号、设备号、功能号,甚至还有厂商ID和设备ID的可读字符串。整个过程快得像呼吸一样自然。但正是这种“理所当然”,掩盖了背后一场精密到微秒级的内核级协作。今天我们要拆解的,不是lspci这个用户态工具本身,而是它敲下去那一瞬间,在 Linux 内核深处真正被触发、被调度、被执行的完整链路——这才是真正决定你能不能“看见”硬件的底层逻辑。
核心关键词lspci、内核、PCIe、sysfs、pci_dev,它们不是孤立的名词,而是一条贯穿用户空间与内核空间、硬件寄存器与内存数据结构的完整生命线。lspci只是那个轻轻推倒第一块多米诺骨牌的人;真正轰然倒塌、层层递进、最终呈现给你一张清晰设备地图的,是内核中那套早已预设好、严丝合缝的PCIe 枚举与设备发现机制。它不依赖任何外部驱动加载,也不需要你手动配置,从系统加电那一刻起,这套机制就已经在 BIOS/UEFI 的协助下悄然启动,并在内核初始化阶段完成了最关键的基础设施搭建。而lspci的作用,本质上就是去“读取”这个早已构建好的、静态的、只读的设备视图。
这个过程的价值远超“查个设备”。它直接关系到你能否正确加载驱动(pci_dev是所有 PCI 驱动的锚点)、能否进行 DMA 映射(pci_dev->dma_mask决定能访问多少物理内存)、能否实现热插拔(pci_rescan_bus()的触发时机)、甚至影响 PCIe AER(Advanced Error Reporting)错误日志的捕获精度。如果你在调试一个网卡无法识别的问题,或者想搞懂为什么某块 FPGA 加速卡的 BAR 空间在lspci -vv里显示为Memory at <ignored>,那么理解lspci背后的内核路径,就是你手里的第一把手术刀。它适合所有想穿透 Linux 设备模型表层、直抵硬件抽象核心的嵌入式工程师、驱动开发者、性能调优师,以及那些不满足于“会用”,而执着于“为什么能用”的技术实践者。这不是教科书里的理论推演,而是我过去十年在服务器固件、GPU 驱动和智能网卡项目里,无数次printk打点、kgdb单步、perf追踪后,亲手验证过的内核心跳节律。
2. 内容整体设计与思路拆解:为什么lspci不需要自己扫描总线?
要理解lspci敲下去发生了什么,第一步必须颠覆一个常见误解:lspci本身并不执行任何硬件扫描操作。它既不向 PCIe 配置空间发送任何 TLP(Transaction Layer Packet),也不去轮询任何总线上的设备。它的全部工作,就是打开一个早已存在的、由内核维护的“设备档案馆”,然后把里面整理好的卡片抄录下来。这个“档案馆”,就是 Linux 内核的PCI 子系统,其核心数据结构是struct pci_dev,而它的物理载体,是内核在初始化早期就通过pci_scan_root_bus()等函数,配合固件(BIOS/UEFI)提供的 ACPI 表或硬编码的根复合体(Root Complex)信息,一次性完成的全系统 PCIe 设备枚举与注册。
这个设计思路,是 Linux 内核“一次枚举、多次查询”哲学的典型体现。它的优势极其明确:确定性、高效性、一致性。想象一下,如果每次lspci都要重新扫描一遍整个 PCIe 树,那不仅会带来不可预测的延迟(尤其在大型服务器上,可能有上百个设备),更会引发严重的竞态问题——就在你扫描的过程中,另一个 CPU 核心可能正在加载某个设备的驱动,而驱动加载的第一步,恰恰就是修改该设备的配置空间(比如使能 Memory Space)。两个操作同时发生,结果就是数据错乱,轻则lspci输出错误,重则导致内核崩溃。因此,内核选择在系统启动的“安全窗口期”——即所有 CPU 已上线、内存管理已就绪、但大部分驱动尚未加载——完成一次彻底、原子的枚举。此后,所有用户态工具(lspci,lshw,hwinfo)和内核驱动,都共享同一份pci_dev数据结构。lspci的角色,就是一个高权限的“档案管理员”,它通过sysfs这个标准化的、面向文件系统的接口,将内核内存中的pci_dev字段,以人类可读的文本格式,映射到/sys/bus/pci/devices/下的每一个子目录里。
这个思路也决定了lspci的能力边界。它只能看到内核“知道”的设备。如果你的设备因为固件 Bug 没有被正确报告给内核,或者你的内核配置禁用了CONFIG_PCI,又或者你使用的是一个极度精简的实时内核(RT Kernel)且裁剪掉了 PCI 支持,那么lspci将永远为空。它不会、也不能“强行唤醒”一个内核根本没意识到的设备。这解释了为什么在某些嵌入式板卡上,明明硬件存在,lspci却查无此物——问题一定出在固件或内核初始化阶段,而不是lspci工具本身。所以,当你面对一个lspci查不到的设备时,你的排查起点,应该立刻跳转到dmesg | grep -i "pci\|acpi",去看内核启动日志里,那场关键的“初次枚举”是否成功完成。
3. 核心细节解析与实操要点:lspci的三重数据源与内核响应链
lspci的输出并非来自单一源头,而是融合了内核内存、硬件寄存器和用户态数据库的三层数据。理解这三层如何协同,是掌握其行为的关键。我们以一条典型的输出为例来拆解:
00:01.0 PCI bridge: Intel Corporation Xeon E3-1200 v3/4th Gen Core Processor PCI Express x16 Controller (rev 06)这一行信息,实际上是由三个独立的、但高度同步的数据源拼接而成:
3.1 第一层:内核内存中的pci_dev结构体(主干)
这是最核心、最权威的数据源。当内核完成枚举后,为系统中的每一个 PCI 设备(包括桥)都分配了一个struct pci_dev实例。这个结构体定义在include/linux/pci.h中,是一个庞杂的“设备信息中心”,包含了:
bus:指向所属总线的指针(struct pci_bus *),用于构建树状拓扑。devfn:设备功能号(Device Function Number),编码了设备号(Device Number)和功能号(Function Number),01.0中的01和0就来源于此。vendor/device:16位的厂商ID和设备ID,0x8086(Intel)和0x0c01(Xeon E3-1200 v3 PCIe Controller)就存储在这里。class:设备类代码(Class Code),0x060400表示 PCI-to-PCI Bridge。hdr_type:配置头类型,区分普通设备、桥、CardBus等。resource[PCI_BRIDGE_RESOURCES]:描述了该桥所管理的下游总线地址空间范围(I/O, Memory, Prefetchable Memory)。
lspci在运行时,首先会通过libpci库,调用pci_scan_bus()的变体(实际是pci_get_device()或pci_get_class()),在内核的全局pci_devices链表中遍历查找。这个链表的头节点是pci_root_buses,它由所有已知的根总线(Root Bus)组成。lspci的遍历逻辑,本质上就是在模拟内核的pci_walk_bus()函数,只不过它不修改任何状态,只读取。
提示:你可以用
cat /proc/bus/pci/devices来直接窥探内核pci_dev结构体的原始二进制快照(虽然可读性差),或者用ls /sys/bus/pci/devices/来列出所有已注册的pci_dev对应的 sysfs 目录。每一个目录名(如0000:00:01.0)就是pci_dev->bus->number、PCI_SLOT(pci_dev->devfn)和PCI_FUNC(pci_dev->devfn)的组合。
3.2 第二层:硬件配置空间(Config Space)的实时读取(血肉)
pci_dev结构体中的很多字段,比如subsystem_vendor(子系统厂商ID)、subsystem_device(子系统设备ID)、irq(中断号),在枚举时只是被“快照”了一次。但lspci的-vv(verbose verbose)选项,会要求它去实时读取硬件的配置空间。这是因为配置空间是设备芯片上的一组标准寄存器,位于每个设备的0x0000到0x0fff地址范围内,可以通过 CPU 的inb/outb指令(x86)或ioread32()(通用)直接访问。
例如,lspci -vv中显示的Capabilities: [40] Power Management version 3,就是lspci通过向该设备配置空间的0x40偏移处读取一个 32 位值,然后根据 PCI 规范解析出这是一个 PM(Power Management)Capability 结构,版本为 3。这个过程是动态的,意味着如果你在lspci -vv运行的同时,用setpci工具修改了某个寄存器,下一次lspci -vv就会反映出新的值。这也是为什么lspci能告诉你设备当前的电源状态(D0/D3hot)、是否支持 MSI-X 中断等“活”的信息。
注意:对配置空间的读取需要内核提供相应的访问接口。
lspci并不直接使用inb/outb,而是通过libpci调用内核的sysfs接口(如/sys/bus/pci/devices/0000:00:01.0/config)或/proc/bus/pci/下的文件。这些接口由内核的pci-sysfs.c文件实现,它封装了底层的pci_read_config_*()函数,确保了访问的安全性和一致性。
3.3 第三层:用户态 ID 数据库(灵魂)
lspci输出中最“人性化”的部分——Intel Corporation Xeon E3-1200 v3/4th Gen Core Processor PCI Express x16 Controller——这个长长的字符串,并非来自内核或硬件,而是来自一个纯用户态的、名为pci.ids的文本数据库。这个文件通常位于/usr/share/hwdata/pci.ids或/var/lib/pciutils/pci.ids。它是一个巨大的、由社区维护的映射表,将vendor_id:device_id的十六进制组合,翻译成可读的厂商名和设备名。
内核本身完全不关心这个字符串。pci_dev->vendor和pci_dev->device永远是冰冷的数字。lspci在拿到这两个数字后,会去pci.ids文件中进行线性搜索(或哈希查找,取决于实现),找到匹配项,再将其拼接到输出中。这就是为什么lspci的输出可以如此友好,而内核日志(dmesg)里却永远只显示0000:00:01.0: [8086:0c01]这样的原始码。更新pci.ids文件(例如sudo update-pciids)就能让lspci立刻识别出新发布的设备,而无需重启内核或重新编译任何东西。
这三层数据源的分离,体现了 Unix “KISS”(Keep It Simple, Stupid)哲学:内核只做最核心、最稳定的事(管理硬件、提供抽象),用户态工具负责易用性(格式化、翻译、交互),而社区则负责知识沉淀(ID 数据库)。它们各司其职,却又无缝协作,共同构成了lspci这个看似简单、实则精妙的工具。
4. 实操过程与核心环节实现:从main()到pci_read_config_dword()
现在,让我们把镜头拉近,跟随lspci的代码,走完从你按下回车键,到屏幕上出现第一行字符的完整旅程。我们以pciutils项目的lspci.c源码(v3.10.0)为蓝本,结合内核源码(Linux v6.6),进行一次端到端的追踪。
4.1 用户态:lspci的启动与初始化
lspci的main()函数(lspci.c:1975)首先会调用pci_slot_name_init()初始化内部的设备命名规则,然后进入核心的lspci_main()。这里的关键一步是pci_init(&pacc),它会创建一个struct pci_access实例pacc,并为其设置默认的“方法”(Method)。pciutils支持多种访问硬件的方式,按优先级排序为:
sysfs方法(最高优先级):通过读写/sys/bus/pci/devices/*/config文件。proc方法:通过读写/proc/bus/pci/XX/YY.Z文件。direct方法:直接使用inb/outb指令(仅限 x86,且需要 root 权限)。
在绝大多数现代发行版中,sysfs方法是默认且首选的。pci_init()会探测/sys/bus/pci/devices/是否存在,如果存在,则pacc->method被设为SYSFS。这意味着,lspci后续的所有硬件访问,都将转化为对 sysfs 文件的open()、read()、close()系统调用。
4.2 内核态:sysfs接口的注册与响应
lspci的sysfs方法之所以能工作,是因为内核在drivers/pci/pci-sysfs.c文件中,为每一个pci_dev结构体,都注册了一套完整的 sysfs 属性。这个过程发生在pci_create_sysfs_dev_files()函数中,它在设备被pci_bus_add_device()添加到总线后被调用。
其中,最关键的一个属性是config。它的定义如下:
static const struct bin_attribute pci_config_attr = { .attr = {.name = "config", .mode = 0644}, .size = PCI_CFG_SPACE_SIZE, .read = pci_read_config, .write = pci_write_config, };这个bin_attribute结构体告诉内核:在/sys/bus/pci/devices/0000:00:01.0/目录下,创建一个名为config的二进制文件,大小为 4096 字节(PCI 配置空间标准大小),当用户read()它时,调用pci_read_config()函数。
当lspci执行read(fd, buf, 4096)时,内核的 VFS(Virtual File System)层会将这个请求路由到pci_read_config()。这个函数的逻辑非常清晰:
- 它首先从
file->private_data中获取到对应的struct pci_dev *dev指针。 - 然后,它调用
pci_user_read_config_*()系列函数(如pci_user_read_config_dword(dev, pos, &val))。 pci_user_read_config_dword()是真正的“门卫”,它会检查pos(偏移量)是否在合法范围内(0x0000-0x0fff),并检查该设备是否真的存在(dev->bus不为 NULL)。- 最终,它调用
raw_pci_ops->read(),这是一个函数指针,其具体实现取决于你的平台架构。在 x86 上,它指向pci_direct_conf1_read(),该函数会组装出正确的CF8/CFC端口 I/O 指令,向硬件发出读取请求。
整个调用栈可以简化为:
lspci (user) -> read() syscall -> VFS -> pci_read_config() -> pci_user_read_config_dword() -> raw_pci_ops->read() -> pci_direct_conf1_read()4.3 硬件层:PCIe 配置事务的物理执行
pci_direct_conf1_read()的执行,标志着指令已经抵达硬件边缘。它的工作是构造一个标准的 PCI 配置事务地址。这个地址由CF8端口(Configuration Address Port)和CFC端口(Configuration Data Port)共同完成。CF8端口接收一个 32 位的地址,其格式为:
Bit 31: Enable Bit (must be 1) Bit 30-24: Reserved (0) Bit 23-16: Bus Number (8 bits) Bit 15-11: Device Number (5 bits) Bit 10-8: Function Number (3 bits) Bit 7-0: Register Number (8 bits, shifted left by 2 for dword alignment)例如,要读取0000:00:01.0设备的0x00寄存器(Vendor ID),CF8会被写入0x80000040(0x80000000启用 +0x00总线 +0x01设备 +0x00功能 +0x00寄存器),然后从CFC端口读取一个 32 位值。
这个 I/O 操作会触发 CPU 的北桥(或现代 SoC 中的 Root Complex)生成一个 Configuration Read TLP,并通过 PCIe 链路发送出去。TLP 的 Header 中包含了目标Bus:Device:Function,Root Complex 会根据这个地址,将请求路由到正确的下游端口(Downstream Port),最终到达目标设备。设备收到后,会将自己的 Vendor ID(0x8086)放入响应包(Completion TLP)中发回。整个过程在纳秒级别完成,CPU 会等待这个响应,然后将数据返回给内核,再经由read()系统调用,最终回到lspci的缓冲区中。
实操心得:如果你想亲眼看到这个过程,可以用
strace -e trace=read,open,close lspci -s 00:01.0 -vv。你会看到lspci打开了/sys/bus/pci/devices/0000:00:01.0/config,然后执行了多次read(),每次读取 4 字节(一个 dword),对应着配置空间的不同偏移。这就是lspci如何“一页一页”地翻阅设备的硬件说明书。
5. 常见问题与排查技巧实录:当lspci不再“诚实”
在真实的工程实践中,lspci的输出有时会“撒谎”,或者干脆“失语”。这些问题往往不是lspci的 bug,而是内核、固件或硬件层面的深层信号。以下是我在多个项目中总结出的、最具代表性的五类问题及其排查路径。
5.1 问题:lspci完全没有输出,或只显示0000:00:00.0(Host Bridge)
现象:在一台新装的服务器或嵌入式板卡上,lspci命令执行后一片空白,或者只有一行0000:00:00.0。
排查路径:
- 检查内核启动日志:
dmesg | grep -i "pci\|acpi\|efi"。这是黄金法则。如果日志里完全没有PCI: Probing PCI hardware或ACPI: PCI Root Bridge这样的字样,说明内核的 PCI 子系统压根就没启动。 - 确认内核配置:
zcat /proc/config.gz | grep CONFIG_PCI(如果启用了CONFIG_IKCONFIG_PROC)。必须确保CONFIG_PCI=y或=m。对于嵌入式系统,CONFIG_PCI_DOMAINS_GENERIC=y也至关重要,它提供了多域支持。 - 检查固件设置:进入 BIOS/UEFI 设置,寻找
PCI Express Configuration、Above 4G Decoding或Resizable BAR等选项。将它们设置为Enabled。某些老旧的 BIOS 会默认关闭 PCIe 支持,或者将 PCIe 设备错误地归类为“Legacy PCI”,导致内核无法识别。 - 检查硬件连接:对于 PCIe 插卡,确保金手指完全插入插槽,挡板螺丝已拧紧。一个松动的插卡,其
PERST#信号可能无法被正确拉低,导致设备无法完成复位和初始化。
独家技巧:在dmesg日志中,重点关注PCI: Cannot allocate resource region这类错误。它通常意味着内核的资源管理器(pci_resource_alignment())在为设备分配 BAR(Base Address Register)空间时失败了,原因往往是 BIOS 分配的地址空间与内核期望的不一致。此时,可以尝试在内核启动参数中加入pci=assign-busses,realloc,强制内核重新分配总线号和资源。
5.2 问题:lspci能看到设备,但lspci -vv显示Memory at <ignored>或I/O ports at <ignored>
现象:设备出现在列表中,但其内存或 I/O 地址空间显示为<ignored>,这意味着驱动无法对其进行内存映射(ioremap())。
排查路径:
- 检查设备状态:
lspci -vv -s 00:01.0 | grep -A5 "Region"。看Region 0的flags字段。如果显示IO或MEM位为 0,说明设备的Command Register中的I/O Space Enable或Memory Space Enable位没有被置位。这通常是驱动未加载或加载失败的标志。 - 检查驱动绑定:
lspci -k -s 00:01.0。查看Kernel driver in use:和Kernel modules:字段。如果前者为空,后者有模块名,说明驱动已存在但未绑定。可以手动modprobe <module_name>尝试加载。 - 检查资源冲突:
cat /proc/iomem和cat /proc/ioports。看设备所需的地址范围是否已被其他设备占用。一个经典的例子是,某些集成显卡会抢占大量的0xe0000000-0xffffffff地址空间,导致后续的 PCIe 设备没有足够的空间可分配。
独家技巧:<ignored>有时是内核的一种“保护性忽略”。例如,当一个设备的 BAR 大小为 0,或者其Base Address寄存器被 BIOS 错误地初始化为0xffffffff时,内核的pci_setup_device()函数会主动将其标记为 ignored,以避免后续的ioremap()导致系统崩溃。此时,你需要检查 BIOS 更新,或在内核启动参数中加入pci=nomsi(禁用 MSI)来绕过某些与中断相关的初始化障碍。
5.3 问题:lspci显示设备,但dmesg中没有任何关于该设备的驱动加载日志
现象:lspci清晰地列出了一个网卡,但dmesg里找不到igb 0000:01:00.0: Intel(R) Gigabit Ethernet Network Connection这样的日志,ip link也看不到对应的eth0。
排查路径:
- 检查驱动模块是否可用:
lsmod | grep <driver_name>。如果没输出,说明模块没加载。find /lib/modules/$(uname -r) -name "*<driver_name>*.ko*"看看模块是否存在。 - 检查模块依赖:
modinfo <driver_name>。查看depends:字段。例如,igb依赖ptp(Precision Time Protocol)模块。如果ptp没加载,igb也会加载失败,但modprobe igb不会报错,只会静默失败。 - 检查设备 ID 匹配:
modinfo <driver_name> | grep alias。对比lspci -n -s 00:01.0输出的1539:0085(厂商:设备ID),看是否在驱动的alias列表中。如果不在,说明这是一个新设备,需要更新驱动或内核。
独家技巧:使用udevadm monitor --subsystem-match=pci在另一个终端运行,然后modprobe <driver_name>。你会实时看到 udev 发送的add事件,以及内核触发的bind事件。如果只看到add而没有bind,那问题一定出在驱动的probe()函数里。此时,你需要在驱动源码的probe()函数开头添加pr_info("Probe started for %s\n", dev_name(&pdev->dev));,然后重新编译安装,再看dmesg。
5.4 问题:lspci输出中,设备的Latency(延迟)值异常高(如255)
现象:lspci -vv显示某个设备的Latency: 255,这是一个非法值,标准范围是0-255,但255通常表示“未配置”。
排查路径:
- 理解
Latency Timer的作用:这是一个 8 位寄存器,用于控制一个设备在获得总线所有权后,最多可以连续占用总线多少个时钟周期。值越大,设备的“霸道”程度越高,但也越容易导致其他设备饿死。 - 检查 BIOS 设置:某些 BIOS 会将
Latency Timer默认设为0(禁用),而lspci在读取到0时,会将其显示为255(因为0在某些上下文中被解释为“无限”)。 - 检查驱动行为:一些老的、不规范的驱动,在
probe()时会错误地将Latency Timer写为0xff(255),而不是一个合理的值(如64)。
独家技巧:你可以用setpci工具手动修改它:sudo setpci -s 00:01.0 latency_timer=b0(将b0十六进制写入0x0d偏移)。但请务必谨慎,错误的值可能导致系统不稳定。更安全的做法是,在驱动的probe()函数中,添加pci_write_config_byte(pdev, PCI_LATENCY_TIMER, 64);进行修正。
5.5 问题:lspci在虚拟机中无法看到直通(Passthrough)的 PCIe 设备
现象:在 KVM/QEMU 虚拟机中,宿主机上lspci能看到设备,但客户机(Guest OS)的lspci却看不到。
排查路径:
- 检查宿主机 IOMMU 状态:
dmesg | grep -i iommu。必须看到AMD-Vi: IOMMU performance counters supported或DMAR: IOMMU enabled。没有 IOMMU,PCIe 直通无法工作。 - 检查设备是否被 VFIO 驱动接管:
lspci -k -s 00:01.0。Kernel driver in use:应该是vfio-pci,而不是igb或nvidia。如果不是,需要先echo "0000:00:01.0" > /sys/bus/pci/devices/0000:00:01.0/driver/unbind,再echo "0000:00:01.0" > /sys/bus/pci/drivers/vfio-pci/bind。 - 检查 QEMU 启动参数:必须包含
-device vfio-pci,host=00:01.0,id=hostdev0,bus=pci.0,addr=0x8。addr=0x8指定了它在客户机 PCI 总线上的位置。
独家技巧:在客户机中,如果lspci看到了设备,但驱动无法加载(dmesg报No such device),很可能是客户机内核缺少CONFIG_VFIO_PCI或CONFIG_IOMMU_API支持。此时,你需要为客户机编译一个启用了这些选项的内核,或者使用一个预编译的支持 VFIO 的发行版内核(如 Ubuntu 的linux-image-virtual)。
6. 深度延展:lspci背后的内核数据结构全景图
lspci的输出,是内核中一套庞大而精巧的数据结构网络的“投影”。要真正驾驭它,就必须理解这张网络的拓扑。这张图的核心,是struct pci_dev,但它绝非孤岛,而是深嵌在一个四层嵌套的树状结构之中。
6.1 第一层:struct pci_bus—— 总线的骨架
每个pci_dev都属于一个struct pci_bus。这个结构体定义了总线的物理和逻辑属性:
number:总线号(00,01,02...),lspci输出的00:01.0中的00就是它。parent:指向其上游总线的指针。根总线(Root Bus)的parent为NULL。children:一个struct list_head,链接了所有挂载在此总线上的子总线(即下游的 PCI-to-PCI Bridge)。devices:一个struct list_head,链接了所有挂载在此总线上的终端设备(Endpoint)。
pci_bus是整个 PCI 树的“脊椎”。lspci的遍历,就是从pci_root_buses(一个全局的struct list_head,包含了所有根总线)开始,递归地调用pci_bus_read_config_*(),从而覆盖整棵树。
6.2 第二层:struct pci_dev—— 设备的心脏
这是lspci的核心数据源。它包含了设备的一切静态和动态信息:
bus:指向所属pci_bus的指针,建立了与第一层的关联。devfn:设备功能号,编码了设备和功能。vendor/device:ID 信息。class:类代码,决定了设备的类型和行为。hdr_type:头类型,决定了pci_dev结构体中哪些字段是有效的(例如,桥设备有subordinate字段,而终端设备没有)。resource[PCI_NUM_RESOURCES]:一个数组,描述了设备的六个 BAR(Base Address Register)所申请的资源范围。lspci的Region 0、Region 1等信息,就来源于此。driver:指向struct pci_driver *的指针,记录了哪个驱动正在管理此设备。
pci_dev的生命周期由内核严格管理。当设备被发现时,pci_scan_single_device()创建它;当设备被移除(热插拔)时,pci_stop_and_remove_bus_device()销毁它。lspci的每一次查询,都是对这个生命周期内某个稳定快照的读取。
6.3 第三层:struct pci_driver—— 驱动的契约
struct pci_driver是内核驱动与 PCI 子系统之间的“合同”。它定义了驱动的元信息和回调函数:
name:驱动名称,lspci -k输出的Kernel driver in use:就是它。id_table:一个struct pci_device_id数组,列出了该驱动所能支持的所有vendor:device组合。lspci的Kernel modules:字段,就是根据这个表反向查找出来的。probe():当一个新设备被发现,且其 ID 匹配id_table时,内核会调用此函数,将struct pci_dev *作为参数传入,驱动由此开始初始化设备。remove():设备被移除时的清理函数。
lspci本身不调用probe(),但它输出的信息,是probe()函数成功执行的前提和结果。一个pci_dev只有在probe()成功返回后,才会被标记为“已绑定”,lspci -k才会显示其驱动名称。
6.4 第四层:struct pci_ops—— 硬件的翻译官
这是整个链条的