news 2026/9/3 2:13:46

Linux内核识别GPU的6步链路:从PCI枚举到驱动probe

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux内核识别GPU的6步链路:从PCI枚举到驱动probe

在 Linux 服务器上安装 GPU 驱动或排查 GPU 不可见问题时,很多人都遇到过这种场景:lspci里明明能看到 NVIDIA 显卡,但进入系统后nvidia-smi却提示找不到设备;或者主机插了多张 GPU,重启后其中一张卡就像“凭空消失”一样,dmesg里还能看到 PCIe 资源分配失败。这些问题看似零散,其实都指向同一条链路:Linux 内核是如何一步步“认出”GPU 的。本文不绕弯子,直接从 PCI 枚举讲到驱动 probe,再把 6 个关键步骤对应的工具、sysfs 节点和排查方法完整梳理一遍。无论你是做 Linux 运维、AI 平台管理,还是刚开始接触内核驱动开发,都能从这套链路里找到排错方向。

1. 先建立直觉:6 步链路是什么

先思考一个基本问题:GPU 插在服务器的 PCIe 插槽上,对 CPU 和 Linux 内核来说,它首先是一块“PCIe 设备”。Linux 要让它真正可用,并不是简单地“加载驱动”就结束,而是要经过一条相对固定的链路。

对应lspcinvidia-smi这两个常见工具,可以这样理解:

  • lspci能看到设备,代表 Linux 已经完成了 PCI 枚举、配置空间识别、基本资源读取。
  • nvidia-smi能看到设备,代表驱动已经成功 probe,并且用户态工具通过内核模块可以访问 GPU。
  • 中间隔着的,正是驱动绑定、硬件初始化和用户态接口注册这些步骤。

把这套流程拆开,本文所说的 6 步分别是:

  1. PCI 总线枚举:扫描 PCIe 拓扑,找到设备所在的 Bus/Device/Function。
  2. 配置空间识别:读取 Vendor ID、Device ID、Class Code、BAR 等关键信息,确认设备身份。
  3. BAR 资源分配:为 GPU 的寄存器窗口和显存映射窗口分配 CPU 物理地址空间。
  4. 设备注册与驱动匹配:把pci_dev注册到内核设备模型,并让驱动通过id_table找到它。
  5. 驱动 probe:调用驱动的.probe回调,完成 DMA 设置、中断申请、显存映射等硬件初始化。
  6. 用户态接口注册:创建/dev/nvidia0/dev/dri/card0等设备节点,CUDA、PyTorch 等应用才能访问 GPU。

后面几章就按这 6 步逐层展开。每一部分都会先讲原理,再给出可执行的命令或代码,并指出最常见的问题点。

2. 第 1 步:PCI 总线枚举,先确认拓扑和 BDF

2.1 谁在做枚举?什么时候做?

PCI/PCIe 总线枚举是指系统扫描 PCI 总线拓扑的过程。x86 平台在开机时,固件(BIOS/UEFI)会先对 PCIe 总线做第一轮初始化,Linux 内核启动时通常也会重新扫描并对资源进行再分配。Linux 内核里对应的代码路径在drivers/pci/probe.c,核心动作就是:从一个已知的 PCI 域和总线号出发,逐段读取配置空间,找到桥设备,再继续往下扫描。

枚举的实际对象是设备的 BDF,也就是 Bus(总线号)、Device(设备号)、Function(功能号)。我们经常看到的0000:01:00.0就是标准格式:

  • 0000:PCI Domain,多数 x86 机器只有一个域 0。
  • 01:Bus 号。
  • 00:Device 号。
  • 0:Function 号。

2.2 PCIe 拓扑与桥的关系

PCIe 拓扑是树状的。CPU 内部或主板芯片组引出 Root Port,Root Port 下面可能接 Switch(交换机芯片),Switch 再分出多个端口接 Endpoint。GPU 就是典型的 PCIe Endpoint。

用下面命令可以一目了然地看到整棵拓扑:

lspci -tv

在一台插了 NVIDIA GPU 的服务器上,输出大致是这种结构(不同机器差异较大,仅作示意):

-+-[0000:00]-+-00.0 Intel Corporation Host Bridge | +-01.0-[01]----00.0 NVIDIA Corporation Device 2230 | +-01.1-[02]----00.0 NVIDIA Corporation Device 2230 | +-01.2-[03]----00.0 NVIDIA Corporation Device 2230 | +-02.0 Intel Corporation PCIe Root Port

其中-[01]表示总线号 01,----00.0表示在 Bus 01 上的设备 00 的功能 0。这个编号直接影响后文的 sysfs 路径和驱动绑定方式。

2.3 为什么枚举有时候会失败?

枚举能否成功,物理前提是 PCIe Link 已经 Train 成功。PCIe 设备上电后,链路两端要完成速率协商和链路训练,配置空间才能被访问。如果 GPU 供电不足、金手指没有插好、插槽物理损坏,或者 PCIe CEM 规范要求的边带信号异常,配置空间读出来都是0xFF,系统就会认为该位置不存在设备。

所以排查“Linux 完全看不到 GPU”时,第一步是确认物理链路本身是否正常,而不是急着装驱动。服务器层面常见的做法是看 BMC/IPMI 日志、重新插拔或更换插槽。

2.4 平台差异:x86 和 ARM64 的一点点区别

x86 平台访问 PCI 配置空间,早期使用 I/O 端口0xCF8/0xCFC的 Configuration Mechanism,PCIe 时代大量使用 MMCONFIG/ECAM 方式,把配置空间直接映射到内存地址。ARM64 和嵌入式平台(比如常见的 ZynqMP 等)通常没有传统 I/O 空间,更多依赖 Device Tree 或 ACPI 中的 PCIe 控制器节点来描述总线范围和地址映射。

对普通运维排查来说,不需要修改这些底层逻辑,但要建立概念:Linux 枚举 PCIe 设备,所依据的总线范围、MMIO 窗口,在 x86 上可能来自 ACPI 表和固件,在 ARM64 上可能来自设备树。看到“PCIe 设备不识别”类问题时,先确认平台层是否给控制器分配了足够的总线号和地址空间,往往能省很多时间。

3. 第 2 步:读取配置空间,识别设备身份

找到设备之后,下一步是读取 PCI 配置空间。每个 PCIe 功能都有一份 256 字节的标准配置空间,PCIe 又扩展到了 4KB,但前 256 字节的布局与 PCI 兼容。

3.1 配置空间里的关键字段

配置空间里的字段很多,对设备识别来说,最关键的是下面几个:

配置偏移宽度含义
0x0016 bitVendor ID,比如 NVIDIA 是 0x10DE
0x0216 bitDevice ID,比如具体 GPU 型号编号
0x088 bitRevision ID
0x098 bitProgramming Interface
0x0A8 bitSubclass
0x0B8 bitBase Class,0x03 表示显示控制器
0x10~0x2432 bitBAR0~BAR4,记录 MMIO/I/O 资源地址
0x3C8 bitInterrupt Line
0x3D8 bitInterrupt Pin

3.2 用 lspci 查看设备身份

lspci内部就是解析配置空间后把数字翻译成字符串。查看 NVIDIA 设备的常用命令如下:

# 只看 NVIDIA 设备的数值 ID lspci -nn -d 10de: # 查看内核驱动绑定情况 lspci -nnk -d 10de: # 查看某一个 BDF 的完整能力 lspci -vvv -s 01:00.0

第一行输出通常类似:

01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA102 [GeForce RTX 3080] [10de:2206] (rev a1)

方括号里有两个关键数字:

  • 10de是 Vendor ID,表示 NVIDIA。
  • 2206是 Device ID,表示具体型号。

如果插的是无显示输出的计算卡,Class Code 可能是0302,也就是 3D controller,而不是 VGA compatible controller。

3.3 用 setpci 直接验证配置空间

如果你想更底层地“看到”配置空间,可以用setpci读取原始数值。以读取 Bus 01 上的设备为例:

# 读取 Vendor ID,输出类似 10de setpci -s 01:00.0 0.w # 读取 Device ID,输出类似 2206 setpci -s 01:00.0 2.w # 读取 Class Code,输出类似 0300 setpci -s 01:00.0 0a.w

说明:setpci需要 root 权限,而且不同发行版不一定默认安装pciutils。这里只介绍只读操作,不要在排查时随意向配置空间写值,写错很可能导致设备状态异常。

3.4 为什么驱动识别靠 ID,而不是靠字符串?

内核驱动不会拿着字符串去匹配“RTX 3080”,而是维护一张pci_device_id表。表中包含 Vendor ID、Device ID、Subsystem Vendor ID、Subsystem Device ID、Class 等字段。驱动加载后,内核会拿设备的配置空间信息去遍历这张表,匹配成功才会进入后续 probe 流程。

同一个 GPU 核心,可能被多个显卡厂商做成不同整卡,所以 Subsystem ID 用来进一步区分板卡厂家和具体型号。系统里的modalias就是把设备身份拼成一段字符串,供模块自动加载使用。

4. 第 3 步:BAR 空间分配,资源不够会怎样

4.1 BAR 是什么?

BAR 是 Base Address Register,直译是“基地址寄存器”。它决定 CPU 可以从哪个物理地址访问设备的寄存器窗口和内存窗口。对 GPU 来说,BAR0 通常映射设备内部寄存器,后面可能还有 BAR1、BAR3 等映射显存部分区域。

如果 BAR 没有被分配有效地址,设备对驱动来说就是不可用的。

查看设备资源分配情况的最直接入口是 sysfs。假设设备 BDF 是0000:01:00.0

# 查看系统为该设备分配的 MMIO 窗口 cat /sys/bus/pci/devices/0000:01:00.0/resource

正常的 NVIDIA GPU 输出大致如下:

0x00000000fd000000 0x00000000fdffffff 0x0000000000140204 0x00000000c0000000 0x00000000cfffffff 0x000000000014220c

每行三列分别表示:起始地址、结束地址、资源标志。如果某一行全是0x0000000000000000,说明该 BAR 没有被分配,这是驱动 probe 失败的常见原因之一。

4.2 Bridge 窗口与多 GPU 资源不足

PCIe 拓扑是树状的,因此 CPU 访问下游设备时,需要经过中间的 PCIe Bridge。每一个 Bridge 都会把下游所有设备的 BAR 区间合并成一个或多个窗口,Windows 和 Linux 统称这些为桥窗口。

服务器插多张 GPU 时,常见问题就出现了:每张大显存 GPU 都要求几十 GB 的 MMIO 空间,如果主板 BIOS 没有开启 Above 4G Decoding,系统只能把资源压缩在 32 位地址空间内,很快就分配完了。这时dmesg会报类似错误:

pci 0000:03:00.0: can't allocate mem resource pcieport 0000:00:01.0: can't allocate memory window

这类“无法给 PCIe 桥或设备分配足够 BAR 空间”的问题,在多卡 AI 服务器上尤其常见。解决办法通常包括:

  • 进入 BIOS/UEFI,开启 Above 4G Decoding。
  • 按平台支持情况开启 Resizable BAR。
  • 更新主板/服务器 BIOS。
  • 尝试调整多 GPU 的插槽位置,让每张卡尽量分散在不同 Root Port 下。
  • 在 Linux 启动参数中加入pci=realloc,允许内核重新分配桥和设备资源。这个参数在部分场景有效,但生产环境要评估后再使用。

4.3 BAR 空间与 CPU 访问显存

现代 GPU 的本地显存(HBM/GDDR)通常不会全部映射到 CPU 地址空间。CPU 访问显存更多是通过 GPU 的 DMA 引擎和驱动管理的映射机制完成。BAR 里直接映射出来的只是设备需要 CPU 直接访问的那部分资源。

所以不要把“BAR 空间大小”和“显存容量”画等号。GPU 显存 80 GB,不代表 CPU 物理地址空间里要留出 80 GB 的 BAR 映射,而是指设备内部资源需要的窗口。

5. 第 4 步:设备注册与驱动匹配

5.1 Linux 设备模型里的三件套

Linux 设备模型中有三个核心概念:总线(Bus)、设备(Device)、驱动(Driver)。PCI 总线对应pci_bus_type,PCI 设备对应pci_dev,PCI 驱动对应pci_driver。它们三者的关系可以简化理解为:

  • 设备说“我在这里,我的身份信息是什么”。
  • 驱动说“我能支持这些设备,这是我的 id_table”。
  • 总线负责在设备和驱动之间牵线搭桥。

当内核枚举出一个新的 PCI 设备并完成资源分配后,它会生成一个pci_dev设备对象,挂到pci_bus_type上。与此同时,如果驱动模块已经加载,总线就会尝试让驱动与设备进行匹配。

5.2 pci_driver 与 id_table

一个最小的 PCI 驱动骨架通常长这样:

#include <linux/pci.h> #include <linux/module.h> static int demo_probe(struct pci_dev *pdev, const struct pci_device_id *id) { int ret; ret = pci_enable_device(pdev); if (ret) return ret; pci_set_master(pdev); dev_info(&pdev->dev, "demo GPU driver probed\n"); return 0; } static void demo_remove(struct pci_dev *pdev) { dev_info(&pdev->dev, "demo GPU driver removed\n"); } static const struct pci_device_id demo_pci_ids[] = { { PCI_DEVICE(0x10de, 0x2206) }, { 0, } }; MODULE_DEVICE_TABLE(pci, demo_pci_ids); static struct pci_driver demo_pci_driver = { .name = "demo_gpu_driver", .id_table = demo_pci_ids, .probe = demo_probe, .remove = demo_remove, }; module_pci_driver(demo_pci_driver); MODULE_LICENSE("GPL");

这里最重要的就是demo_pci_ids表。当设备 Vendor ID 是0x10de、Device ID 是0x2206时,总线匹配成功,内核就会调用demo_probe

5.3 查看设备当前由哪个驱动绑定

在真实系统里,用下面命令就能知道设备当前被谁绑定:

# 查看驱动符号链接 ls -l /sys/bus/pci/devices/0000:01:00.0/driver # 查看设备 ID 与模块自动加载信息 cat /sys/bus/pci/devices/0000:01:00.0/uevent cat /sys/bus/pci/devices/0000:01:00.0/modalias

driver文件是指向/sys/bus/pci/drivers/xxx的符号链接。如果链接不存在,说明还没有驱动绑定。如果链接指向nouveau而不是nvidia,说明 NVIDIA 官方驱动没抢到设备,这是常见的驱动切换冲突。

modalias的内容类似:

pci:v000010DEd00002206sv000010DEsd00002206bc03sc00i00

用户态工具modprobe可以根据这段字符串自动查找并加载对应的内核模块。这也是为什么设备出现时系统能自动拉起来一个驱动。

5.4 driver_override 与强制绑定

有时系统里存在多个驱动都支持同一设备,例如开源驱动和闭源驱动并存。此时可以通过driver_override指定某个驱动来绑定设备。

# 强制让设备优先绑定 nvidia 驱动 echo "nvidia" > /sys/bus/pci/devices/0000:01:00.0/driver_override # 重新触发驱动绑定 echo "0000:01:00.0" > /sys/bus/pci/drivers_probe
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 2:13:42

2020数模国赛A题炉温曲线代码拆解:从传热模型到参数反演与优化

简介&#xff1a;2020年全国大学生数学建模竞赛A题代码包&#xff0c;基于MATLAB实现&#xff0c;面向数模参赛者、科研人员及对数值计算与智能优化算法感兴趣的学习者。压缩包共22个文件&#xff0c;以19个.m脚本为主&#xff0c;涵盖有限差分法离散偏微分方程、最小二乘拟合、…

作者头像 李华
网站建设 2026/9/3 2:13:12

一份面向Java初学者的面试准备路线图

有人把Java面试准备当成背八股&#xff0c;从HashMap问到并发锁&#xff0c;从JVM调优问到Spring循环依赖&#xff0c;背得越熟&#xff0c;挂得越快。面试官真正想看的&#xff0c;不是你记住了多少答案&#xff0c;而是你面对未知问题时&#xff0c;能不能像一个会写代码的工…

作者头像 李华
网站建设 2026/9/3 2:12:45

基于STM32F407与AD9910的DDS信号源驱动实现与调试

简介&#xff1a;AD9910直接数字合成模块驱动&#xff0c;是一份面向嵌入式开发者和电子设计竞赛参赛者的信号发生器工程。工程基于意法半导体Cortex-M4内核微控制器&#xff0c;通过串行外设接口控制高性能直接数字合成芯片&#xff0c;可输出高达1.6GHz的正弦波信号&#xff…

作者头像 李华
网站建设 2026/9/3 2:12:10

Spring Boot与数据库实战:从框架集成到性能优化

你好&#xff0c;这个标题属于二战军事历史类文章&#xff0c;和我的定位不匹配。我平时专注于 CSDN 技术教程方向&#xff0c;比如 Spring Boot、Spring Security、Apollo、Python、Oracle、数据库实战、异常排查、项目落地这类内容&#xff0c;目标是输出结构完整、代码可复制…

作者头像 李华
网站建设 2026/9/3 2:11:36

KT6368A蓝牙芯片完整资料包解析与低延时调优实战

简介&#xff1a;这是一套面向嵌入式与物联网开发者的KT6368A蓝牙双模芯片完整资料包&#xff0c;围绕低功耗蓝牙5.1芯片的选型评估、电路设计、串口AT/透传开发以及生产调试等环节&#xff0c;提供从入门到量产所需的文档与工具。压缩包共38个文件、约47.34MB&#xff0c;以12…

作者头像 李华
网站建设 2026/9/3 2:11:32

普中HC6800-ES V2.0开发板资料包使用详解:从解压到项目移植

简介&#xff1a;普中HC6800-ES V2.0开发板资料.zip是一套基于51单片机的经典实验程序包&#xff0c;面向电子爱好者、初学者和嵌入式系统开发者&#xff0c;可用于课程学习、毕业设计和日常项目验证。压缩包共365个文件&#xff0c;大小仅5.73MB&#xff0c;以C源程序、Hex烧录…

作者头像 李华