Linux Pcie(1)————pci子系统枚举硬件+edu-pci驱动安装
一:qemu调试界面
不启动系统,进入qemu调试界面,查看相关的qemu模拟出来的硬件设备情况
①info qtree:介绍qemu模拟的设备
info qtree的设计目的就是只显示外设总线和设备(并不会看到CPU等设备的)
main-system-bus (系统总线)
├─ hpet (高精度事件定时器)
├─ ioapic (I/O高级可编程中断控制器)
├─ q35-pcihost (Q35 PCIe主桥)
│ └─ pcie.0 (PCIe根总线)
│ ├─ pcie-root-port (rp1, PCIe根端口)
│ │ └─ rp1 (PCIe下游总线)
│ │ └─ edu (实验教学设备)
│ ├─ ICH9-SMB (SMBus控制器)
│ ├─ ich9-ahci (SATA控制器)
│ ├─ ICH9-LPC (LPC桥接器)
│ └─ mch (内存控制器集线器)
├─ fw_cfg_io (QEMU固件配置接口)
└─ kvmvapic (KVM虚拟化APIC加速)
系列info命令
| 命令 | 功能 |
|---|---|
info status |
查看虚拟机运行状态 |
info version |
查看 QEMU 版本 |
info block |
查看块设备 (硬盘、光驱) 信息 |
info network |
查看网络设备信息 |
info pci |
查看所有 PCI 设备的详细信息 |
info irq |
查看中断统计信息 |
二:上电开机之后的流程
1:qemu虚拟机启动的时候 就是模拟出一个带有这个q35的 硬件平台
以下种种都是通过qemu软件的情况模拟出这个 对应的硬件设备情况
创建的内容有
①host bridge
- 创建
ICH9 LPC Controller(Intel 82801IR)作为Host Bridge(PCI 主桥) - Host Bridge 是 CPU 与 PCI 总线之间的桥梁,负责地址空间转换(CPU 物理地址 ↔ PCI 总线地址)和中断路由
②root bus
- Host Bridge 初始化时调用
pci_root_bus_new()创建PCIe Root Bus(编号为 0,对应pcie.0) - 初始化
struct PCIBus数据结构,维护该总线上的所有设备列表 - 注册 PCI 配置空间访问接口:q35 架构使用MMIO 方式访问 PCI 配置空间(地址范围
0xcf8-0xcff兼容 IO 端口方式,同时支持0xe0000000起始的 MMIO 扩展配置空间)
③root port
- 解析
-device pcie-root-port参数,调用pcie_root_port_realize()函数 - 在 Root Bus(bus 0)上分配设备地址
00:1c.0(设备号 28,功能号 0) - 初始化 Root Port 的配置空间:
- Vendor ID:
0x8086(Intel) - Device ID:
0x2940(ICH9 PCIe Root Port 1) - Class Code:
0x060400(PCI-PCI Bridge) - 配置 Secondary Bus Number 寄存器(初始为 0,内核枚举时会分配)
- 配置 PCIe Capability 结构,标识这是一个 PCIe Root Port
- Vendor ID:
- Root Port 本质是一个 PCI-PCI 桥,负责连接上游 Root Bus 和下游 Secondary Bus,当然既然都叫root port了 他的上游仅仅指代bus0,下游则是链接的其他总线
┌─────────────────────────────────────────────────────────┐
│ CPU 芯片内部 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ CPU 核心 0 │ │ CPU 核心 1 │ │ ... │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ PCIe Root Complex │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ │ Root Port│ │ Root Port│ │ Root Port│ │ Root Port│ │
│ │ │ 00:1c.0 │ │ 00:1c.1 │ │ 00:1c.2 │ │ 00:1c.3 │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ └───────┼─────────────┼─────────────┼─────────────┼───────┘ │
└──────────┼─────────────┼─────────────┼─────────────┼─────────┘
│ │ │ │
┌──────────▼─────────┐ │ │ │
│ PCIe 总线 1 (Bus 1)│ │ │ │
│ │ │ │ │
│ ┌─────────────┐ │ │ │ │
│ │ EDU 设备 │ │ │ │ │
│ │ 01:00.0 │ │ │ │ │
│ └─────────────┘ │ │ │ │
└────────────────────┘ │ │ │
│ │ │
┌────────────────────────▼─────────┐ │ │
│ PCIe 总线 2 (Bus 2) │ │ │
│ │ │ │
│ ┌─────────────┐ ┌─────────────┐│ │ │
│ │ 网卡 │ │ 声卡 ││ │ │
│ │ 02:00.0 │ │ 02:00.1 ││ │ │
│ └─────────────┘ └─────────────┘│ │ │
└──────────────────────────────────┘ │ │
│ │
┌──────────────────────────────────────▼─────────┐ │
│ PCIe 总线 3 (Bus 3) │ │
│ │ │
│ ┌─────────────┐ │ │
│ │ PCIe 桥 │ │ │
│ │ 03:00.0 │ │ │
│ └──────┬──────┘ │ │
│ │ │ │
│ ┌──────▼──────┐ ┌─────────────┐ ┌─────────────┐│
│ │ PCIe 总线 4 │ │ NVMe SSD │ │ 显卡 ││
│ │ (Bus 4) │ │ 04:00.0 │ │ 03:01.0 ││
│ └─────────────┘ └─────────────┘ └─────────────┘│
└────────────────────────────────────────────────────┘
④edu endpoint
- 解析
-device edu参数,调用edu_realize()函数 - 将 EDU 设备挂载到 Root Port
rp0的下游总线(后续内核会分配为 bus 1) - 初始化 EDU 设备的配置空间:
- Vendor ID:
0x1234(QEMU 示例设备) - Device ID:
0x11e8(EDU 设备 ID) - Class Code:
0xff0000(未分类设备) - BAR0: 配置为 4KB 大小的 MMIO 空间(用于设备寄存器访问)
- Vendor ID:
- 注册 EDU 设备的 MMIO/IO 访问回调函数,当内核读写这些地址时,QEMU 会执行对应的模拟逻辑,pci设备 专门用来教学使用的
⑤root complex:
承担着对TLP包的封包 解包,数据路由等等功能,也就是不管是PCI设备到外界的访问,还是外界对PCI设备的访问都必须经过这个root complex处理一下子.
它是CPU 世界和 PCIe 世界之间的唯一桥梁:
- 所有 CPU 对 PCIe 设备的访问(如 MMIO 寄存器读写)都要经过 Root Complex
- 所有 PCIe 设备对系统内存的访问(如 DMA 传输)都要经过 Root Complex
- 它是整个 PCIe 总线树的根节点,负责所有 PCIe 数据包的路由和转发
是作为一个独立芯片的,所有其他的PCI设备都要挂载到他上边(与这个host bridge是一个意思只是叫法上不一样)
- 过去:Root Complex 是主板上独立的北桥芯片(North Bridge)
- 现在:Root Complex 已经完全集成在 CPU 内部,成为 CPU 的一部分
| 设备类型 | 作用 | 在拓扑中的位置 | 例子 | BDF 特点 |
|---|---|---|---|---|
| Root Complex | PCIe 树的根节点,连接 CPU 和 PCIe 系统 | 最顶层 | CPU 内部集成 | 通常是 00:00.0 |
| Root Port | Root Complex 向外延伸的端口,每个端口连接一条独立的 PCIe 总线 | Root Complex 的直接子节点 | CPU 上的 PCIe 插槽 | 都在 Bus 0 上,如 00:1c.0 |
| PCIe Bridge | PCIe 总线扩展器,用于连接更多的 PCIe 设备 | 树的中间节点 | PCIe 交换机、主板上的 PCIe 桥 | 自己有一个 BDF,下游总线号大于上游 |
| Endpoint | 最终的功能设备,是 PCIe 事务的发起者和接收者 | 树的叶子节点 | 网卡、显卡、SSD、EDU 设备 | 没有下游总线 |
pci bridge跟pci switch其实也差不太多
2:Linux guest开机
2.1整体流程

2.1.1加载运行BIOS->执行汇编->执行真正的c语言初始化
- x86 架构:CPU 上电后从
0xfffffff0地址开始执行 BIOS/UEFI 代码- BIOS/UEFI 加载内核镜像到内存,然后跳转到内核的入口点:
arch/x86/kernel/head_64.S中的_start- 经过一系列汇编级别的初始化(设置页表、启用保护模式、初始化栈等),最终跳转到 C 语言入口:
start_kernel()
2.1.2C语言开始执行kernel👉start_kernel():内核初始化的总控函数
start_kernel()定义在init/main.c,它是内核 C 语言代码的入口,负责执行所有核心子系统的初始化。Linux 内核没有把所有初始化函数都写在start_kernel()里(那样会导致这个函数变得无比庞大),而是设计了一套initcall 机制(具体的初始化是在do_initcalls() ):
- 定义了一系列宏:不同宏的优先级不一样,由此实现不同子系统的不同优先级初始化
- 每个内核组件通过这些宏把自己的初始化函数注册到一个特定的级别
- 链接器会把所有同一级别的初始化函数收集到一个连续的内存区域,形成一个函数指针数组
- 内核启动时会按优先级从高到低依次执行每个级别的所有初始化函数
关于initcall机制的宏定义的等级
| 数字优先级 | 段名 | 别名宏 | 设计目的 | ||
|---|---|---|---|---|---|
| 0 | .initcall0.init |
early_initcall |
最早期架构初始化,不能依赖任何其他子系统 | ||
| 1 | .initcall1.init |
pure_initcall |
纯软件初始化,只操作内存,不访问硬件 | ||
| 2 | .initcall2.init |
core_initcall |
核心内核子系统初始化 | ||
| 3 | .initcall3.init |
postcore_initcall |
核心子系统之后,总线系统之前 | ||
| 4 | .initcall4.init |
arch_initcall |
架构相关的硬件初始化 | ||
| 5 | .initcall5.init |
subsys_initcall |
通用总线和子系统初始化 | ||
| 6 | .initcall6.init |
fs_initcall |
文件系统初始化 | ||
| 7 | .initcall7.init |
device_initcall |
设备驱动初始化 | ||
| 8 | .initcall8.init |
late_initcall |
最后阶段初始化 |
在内核源文件的相关定义:(有点像是注册表机制)

3:ACPI/PCI 子系统初始化 + 发现 PCI root bus&bus 0
ACPI子系统和PCI子系统的初始化是被按照同一个标准拆分成了好几个小段,两个子系统之间的小段彼此之间可能存在相互依赖关系.
3.1名词解释:
①:ACPI(Advanced Configuration and Power Interface,高级配置与电源接口)是操作系统和硬件固件之间的一个标准化接口,它的核心作用是:让操作系统以统一的方式发现、配置和管理系统中的所有硬件设备,而不需要依赖硬件特定的驱动程序.
3.2整体流程:
阶段 A:命令行早期解析,这会设置影响 PCI 行为的全局参数。
阶段 B:driver core 基础设施起来
driver_init() 创建通用 driver model 的基础设施:
/sys/devices
/sys/bus
/sys/class
没有这一步
PCI 后面无法 bus_register(&pci_bus_type),
也无法把 struct pci_dev 加进 device model。
阶段 C:PCI bus type 注册
这一步会告诉 driver core:
PCI bus 叫 "pci"
PCI 设备和 PCI 驱动怎么匹配:pci_bus_match()
匹配后怎么 probe:pci_device_probe()
remove 时怎么处理:pci_device_remove()
uevent/modalias 怎么生成:pci_uevent()
所以后面, edu_pci_driver 注册进来时,driver core 并不直接懂 PCI VID/DID,它会通过 pci_bus_type.match 间接调用 PCI 的匹配逻辑。
即:driver core 在 pci_bus_type 上匹配 VID/DID
阶段 D:x86 PCI 架构层初始化
这里干的是 x86 相关的 PCI 基础:
PCI config space 访问方式
MMCONFIG/ECAM
raw_pci_ops/raw_pci_ext_ops
MSI domain
DMI quirk
注意:它一般还不是“把所有 endpoint 都枚举出来”的地方。它更像是准备 x86 平台访问 PCI 配置空间的能力。
阶段 E:ACPI/PCI 辅助初始化
位置:
- kernel-src/linux/drivers/pci/pci-acpi.c:1499
- kernel-src/linux/drivers/pci/pci-acpi.c:1519
static int __init acpi_pci_init(void)
{
..
acpi_pci_slot_init();
acpiphp_init();
return 0;
}
arch_initcall(acpi_pci_init);
还有 x86 的:
- kernel-src/linux/arch/x86/pci/acpi.c:606
int __init pci_acpi_init(void)
{
if (acpi_noirq)
return -ENODEV;
pr_info("Using ACPI for IRQ routing\n");
acpi_irq_penalty_init();
pcibios_enable_irq = acpi_pci_irq_enable;
pcibios_disable_irq = acpi_pci_irq_disable;
x86_init.pci.init_irq = x86_init_noop;
...
return 0;
}
这里是 ACPI 和 PCI 的桥接,比如 IRQ routing、PCI slot、hotplug 等。
阶段 F:ACPI 子系统扫描,发现 PCI root bridge
ACPI 主初始化在:
- kernel-src/linux/drivers/acpi/bus.c:1420
- kernel-src/linux/drivers/acpi/bus.c:1461
acpi_init() 里会做 ACPI namespace 扫描。
acpi_pci_root_init() 是在 ACPI scan 初始化过程中调用的:
当 ACPI namespace 里发现 PCI root bridge,例如 QEMU/q35 提供的 ACPI PCI root,handler 会调用:static int acpi_pci_root_add(struct acpi_device *device, ...)
QEMU 提供 ACPI 表
↓
Linux ACPI 子系统扫描 namespace
↓
发现 PCI root bridge
↓
acpi_pci_root_add()
↓
pci_acpi_scan_root()
↓
扫描 root bus / root port / endpoint
↓
创建 struct pci_dev
↓
pci_bus_add_devices()
↓
/sys/bus/pci/devices/0000:01:00.0 出现
阶段 G:x86 PCI subsys 初始化
位置:
- kernel-src/linux/arch/x86/pci/legacy.c:58
- kernel-src/linux/arch/x86/pci/legacy.c:77
在 ACPI 场景下,x86_init.pci.init() 通常会走 ACPI PCI init 路径;如果失败,可能 fallback 到 legacy PCI scanning。
这个阶段会做 PCI BIOS/x86 侧资源 survey、排序、IRQ 等处理。
3.3具体到函数的实现
3.3.1. pure_initcall 级别(优先级 1):pci_realloc_setup_params
- 定义位置:
drivers/pci/setup-bus.c - 核心作用:把内核命令行中的
pci=realloc参数从临时的启动内存复制到永久的内核内存中 - 为什么在这个级别?
pure_initcall是最早能使用内核内存分配器的级别- 之前的
pci_setup(early_param)只能把参数存在临时的启动内存中,启动内存会在后面被释放 - 必须在任何 PCI 资源分配操作之前把参数保存下来
3.3.2 postcore_initcall 级别(优先级 3):pci_driver_init
- 定义位置:
drivers/pci/pci-driver.c - 核心作用:注册
pci_bus_type总线类型,这是整个 PCI 子系统的基础 - 代码实现:
static int __init pci_driver_init(void) { int ret; ret = bus_register(&pci_bus_type); if (ret) return ret; #ifdef CONFIG_PCIEPORTBUS ret = bus_register(&pcie_port_bus_type); if (ret) return ret; #endif dma_debug_add_bus(&pci_bus_type); return 0; } postcore_initcall(pci_driver_init); - 为什么在这个级别?
- 所有后续的 PCI 操作都依赖
pci_bus_type的存在 - 必须在任何 PCI 设备被创建和注册之前完成总线类型的注册
- 所有后续的 PCI 操作都依赖
3.3.3 arch_initcall 级别(优先级 4):pci_arch_init + acpi_pci_init
这是x86 架构特有的 PCI 初始化,不同架构(ARM、RISC-V)的实现完全不同。
(1) pci_arch_init(x86 PCI 架构初始化)
- 定义位置:
arch/x86/pci/init.c - 核心作用:
- 配置 PCI 配置空间的访问方式(IO 端口
0xcf8/0xcfc或 MMIO 方式MCONFIG) - 初始化 x86 的 PCI 域(PCI domain)
- 初始化 MSI/MSI-X 中断机制
- 配置 PCI 配置空间的访问方式(IO 端口
- 为什么在这个级别?
- 必须在 ACPI 初始化之前完成硬件访问方式的配置
- ACPI 解析 PCI 设备时需要能够访问 PCI 配置空间
(2) acpi_pci_init(ACPI PCI 辅助初始化)
- 定义位置:
drivers/acpi/pci_root.c - 核心作用:注册 ACPI PCI 根总线的驱动程序
- 为什么在这个级别?
- 必须在 ACPI 子系统初始化之前注册好 ACPI PCI 驱动
- 这样当 ACPI 扫描命名空间发现 PCI 根总线对象时,就能找到对应的驱动来处理它
3.3.4 subsys_initcall 级别(优先级 5):acpi_init + pci_subsys_init
这是最关键的一个级别,PCI 子系统的核心框架和 ACPI 子系统都在这个级别初始化完成。
(1) acpi_init(ACPI 子系统初始化)
- 定义位置:
drivers/acpi/bus.c - 核心作用:
- 完整解析 ACPI 命名空间
- 发现
_SB.PCI0根总线对象 - 调用之前注册的
acpi_pci_root驱动来处理这个对象
- 为什么在这个级别?
- 必须在 PCI 子系统核心初始化之后执行,因为它需要调用 PCI 子系统的接口来创建 PCI 总线
- 必须在所有设备驱动初始化之前执行,因为所有设备都需要通过 ACPI 来发现
(2) pci_subsys_init(PCI 子系统核心初始化)
- 定义位置: arch/x86/pci/legacy.c
- 核心作用:
- 初始化 PCI 资源管理系统
- 初始化 PCI 中断路由系统
- 注册 PCI 热插拔通知链
- 打印 PCI 子系统的启动信息:
PCI: Probing PCI hardware
- 为什么在这个级别?
- 必须在
pci_driver_init之后执行(总线类型已经注册) - 必须在
acpi_init之前执行(ACPI 需要 PCI 子系统的接口)
- 必须在
3.3.5. ACPI PCI 根总线处理(subsys_initcall 级别之后)
这是PCI 设备枚举的真正开始,它发生在acpi_init函数的内部:
- ACPI 发现 PCI 根桥:
acpi_init扫描命名空间,发现_SB.PCI0对象 - 调用 ACPI PCI 根驱动:调用
acpi_pci_root_add()函数 - 提取 ACPI 信息:从
_BBN、_CRS、_PRT方法中提取 PCI 根总线的信息 - 触发 PCI 扫描:调用
pci_acpi_scan_root()函数 - 递归扫描总线:
- 创建
struct pci_bus代表 bus 0 - 扫描 bus 0,发现
00:1c.0PCIe Root Port - 分配 secondary bus 号 1,创建 bus 1
- 扫描 bus 1,发现
01:00.0EDU 设备 - 创建
struct pci_dev,读取配置空间,分配资源
- 创建
- 注册到驱动核心:调用
pci_bus_add_devices(),把所有发现的 PCI 设备注册到pci_bus_type
3.3.6 device_initcall 级别(优先级 7): EDU 驱动
- 你的驱动通过
module_init(edu_init)注册 module_init是device_initcall的别名- 为什么在这个级别?
- 必须在所有 PCI 子系统初始化完成之后执行
- 必须在所有 PCI 设备都被枚举和注册之后执行
- 这样当你的驱动加载时,
0000:01:00.0设备已经存在于 sysfs 中,驱动和设备可以成功匹配
linux内核先初始化 总线(由于pci子系统在编译的时候添加的有关键字,这也就是把自己注册到了,总线里边,)从而确保初始化的时候,内核会把这个注册到总线上的所有子系统一并初始化了.
4:内核开始遍历这个Root Port下挂载的东西
4.1 整体流程
QEMU 创建 q35 host bridge/root bus/root port/edu endpoint
↓
guest Linux 启动
↓
ACPI/PCI 初始化发现 PCI root bus
↓
PCI core 扫描 bus 0
↓
发现 00:1c.0 是 pcie-root-port
↓
继续扫描 secondary bus
↓
发现 01:00.0 是 edu endpoint
↓
读取配置空间 vendor/device/class/BAR
↓
创建 struct pci_dev
↓
注册到 pci_bus_type
↓
出现在 /sys/bus/pci/devices/0000:01:00.0
4.2内核自动完成的通用工作(不需要任何驱动)
扫描总线发现设备:
- 递归扫描所有 PCI 总线,读取每个可能位置的 Vendor ID
- 发现有效设备后,创建
struct pci_dev结构体读取标准配置空间:
- 读取 Vendor ID、Device ID、Class Code、Revision ID
- 读取 BAR 寄存器,计算设备需要的地址空间大小
- 读取中断引脚信息
分配系统资源:
- 从 PCI 根总线的资源池中,为每个 BAR 分配实际的物理地址
- 将分配的地址写回设备的 BAR 寄存器
- 分配中断号,配置中断路由
- 启用设备的 MMIO/IO 访问(设置 Command 寄存器)
注册到设备模型:
- 将
struct pci_dev添加到 PCI 总线的设备列表- 在
/sys/bus/pci/devices/下创建对应的符号链接- 创建所有标准的 sysfs 属性文件(
vendor、device、resource、config等)
4.3在sysfs中呈现的结果(安装驱动之前 子系统枚举的结果)

| BDF 地址 | 设备名称 | 对应我们之前讲的 |
|---|---|---|
0000:00:00.0 |
Intel Corporation 82G33/G31/P35/P31 Express DRAM Controller | q35 Host Bridge / PCI 根复合体 |
0000:00:1c.0 |
Intel Corporation 82801I (ICH9 Family) PCI Express Port 1 | PCIe Root Port 1(我们的桥设备) |
0000:00:1f.0 |
Intel Corporation 82801IB (ICH9) LPC Interface Controller | ICH9 南桥 LPC 总线控制器 |
0000:00:1f.2 |
Intel Corporation 82801IR/IO/IH (ICH9R/DO/DH) 6 port SATA Controller [AHCI mode] | SATA 磁盘控制器 |
0000:00:1f.3 |
Intel Corporation 82801I (ICH9 Family) SMBus Controller | SMBus 系统管理总线控制器 |
0000:01:00.0 |
Red Hat, Inc. QEMU Virtual Machine Education Device | ✅ 我们的 EDU 端点设备 |
该图也表明了pcie设备的拓扑关系
最后一行
lrwxrwxrwx 1 0 0 0 May 14 08:18 0000:01:00.0 -> ../../../devices/pci0000:00/0000:00:1c.0/0000:0
pci0000:00代表 PCI 根总线(bus 0)0000:00:1c.0是挂在根总线上的 PCIe Root Port0000:01:00.0是挂在0000:00:1c.0下游端口的 EDU 设备- 它是 Root Port 的子设备,所以它的路径里多了一级
0000:00:1c.0

以上结果是手动安装驱动之前
也就是系统自动完成设备枚举产生的东西
这些都是 Linux 内核PCI 子系统自动完成的通用工作
5:insmod edu_pci.ko之后的变化
5.1整体流程
一个新 driver 注册进 pci_bus_type
↓
driver core 遍历 pci_bus_type 上所有已有 PCI device
↓
对每个 device 尝试匹配
5.2具体函数实现
5.2.1用户键入按照驱动命令之后===>向PCI子系统注册驱动
由于事先已将在驱动标明,probe,remove等函数的具体内容
static struct pci_driver edu_pci_driver = {
.name = "edu_pci",
.id_table = edu_pci_ids,
.probe = edu_pci_probe,
.remove = edu_pci_remove,
};
module_pci_driver(edu_pci_driver);
以上等价于
static int __init edu_pci_init(void)
{
return pci_register_driver(&edu_pci_driver);//PCI core 把这个 driver 挂到 pci_bus_type 上
}
static void __exit edu_pci_exit(void)
{
pci_unregister_driver(&edu_pci_driver);
}
module_init(edu_pci_init);
module_exit(edu_pci_exit);
此步骤只是调用pci_register_driver(&edu_driver),向 PCI 子系统注册自己,声明自己能处理的设备 ID:{ PCI_DEVICE(0x1234, 0x11e8) }
5.2.2PCI子系统响应用户的注册=======>驱动与设备匹配
- PCI 子系统遍历所有已注册的驱动和已发现的设备
- 发现
0000:01:00.0的 Vendor/Device ID 和你的驱动匹配- 调用驱动的
probe函数
insmod edu_pci.ko===========》用户shell里边键入的安装指令
↓
module_pci_driver()=========》向pci子系统注册(用户定义的probe remove函数地址传递给子系统)
↓
pci_register_driver()
↓
__pci_register_driver() 把 PCI 专用的pci_driver包成通用的设备驱动,挂到pci_bus_type
↓
driver_register()======》其实是把这个驱动注册到pci总线上了
↓
bus_add_driver()
↓
driver_attach()
↓
bus_for_each_dev(pci_bus_type)
↓
pci_bus_match()
↓
pci_match_device()
↓
pci_match_id()============================》通过ko文件设备ID去匹配总线上已有的pci设备ID
↓
driver_probe_device()=====================》开始下达执行probe函数的指令
↓
really_probe()=========================》probe函数开始执行的命令 逐级下发
↓
pci_device_probe()
↓
__pci_device_probe()
↓
pci_call_probe()
↓
local_pci_probe()
↓
pci_drv->probe(pci_dev, id)
↓
edu_pci_probe(pdev, id)=====================》真正开始执行用户定义的probe函数
5.2.3 probe函数的能力
/**
* edu_pci_probe - PCIe设备探测函数(内核自动调用)
* @pdev: 内核为EDU设备创建的PCI设备对象指针(包含设备所有信息)
* @id: 匹配成功的设备ID条目(指向驱动中pci_device_id数组的对应元素)
*
* 当内核发现一个设备与驱动的pci_device_id表匹配时,自动调用此函数
* 返回值: 0表示成功,负数表示错误码(如-ENOMEM、-EIO等)
*/
static int edu_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
// 设备私有数据指针:保存本设备特有的所有状态和资源
struct edu_pci_dev *edu;
// BAR资源的物理起始地址(resource_size_t是内核专门用于资源地址的类型)
resource_size_t start;
// BAR资源的大小(字节)
resource_size_t len;
// 用于读取EDU设备版本寄存器的临时变量
u32 id_value;
// 通用返回值变量,用于保存所有函数调用的错误码
int ret;
// 打印设备基本信息:厂商ID和设备ID
// 永远使用dev_*系列函数打印日志,不要用printk
// dev_info会自动在日志前加上设备的BDF地址(如0000:01:00.0),方便调试
dev_info(&pdev->dev, "probe vendor=%04x device=%04x\n", pdev->vendor, pdev->device);
//-------------------------------------------------------------------------
// 阶段1:启用PCI设备(必须是probe函数中第一个调用的PCI API)
//-------------------------------------------------------------------------
// pcim_enable_device是pci_enable_device的托管版本(devres框架)
//(m= manage是管理的意思 Linux中存在新旧两套接口 旧接口要自己管理生命周期 新接口由内核自动管理 卸载的时候 内核直接帮我把映射以及资源释放)
// 它会自动完成以下工作:
// 1. 将设备从低功耗状态唤醒(D3 -> D0)
// 2. 分配并启用设备的I/O和内存资源
// 3. 启用设备的总线主控权(为DMA做准备)
// 4. 设置设备的命令寄存器(启用内存空间、I/O空间、总线主控)
// 托管优势:当驱动卸载或probe中途失败时,内核会自动调用pci_disable_device
ret = pcim_enable_device(pdev);
if (ret) {
dev_err(&pdev->dev, "pcim_enable_device failed: %d\n", ret);
return ret;
}
//-------------------------------------------------------------------------
// 阶段2:请求并映射BAR资源到内核虚拟地址空间
//-------------------------------------------------------------------------
// pcim_iomap_regions是pci_iomap的托管版本,一次性映射指定的多个BAR
// 参数2: BIT(EDU_BAR)表示只映射第0个BAR(EDU只有BAR0)
// 如果要映射多个BAR,可以用BIT(0)|BIT(1)
// 参数3: 资源名称,会出现在/proc/iomem文件中
// 它会自动完成以下工作:
// 1. 调用pci_request_regions()请求独占BAR资源
// 2. 调用ioremap()将BAR的物理地址映射到内核虚拟地址空间
// 托管优势:自动调用pci_release_regions()和iounmap()
ret = pcim_iomap_regions(pdev, BIT(EDU_BAR), "edu_pci");
if (ret) {
dev_err(&pdev->dev, "pcim_iomap_regions BAR%d failed: %d\n", EDU_BAR, ret);
return ret;
}
//-------------------------------------------------------------------------
// 阶段3:分配并初始化设备私有数据结构
//-------------------------------------------------------------------------
// devm_kzalloc是kzalloc的托管版本
// 参数1: 设备对象指针(用于关联资源)
// 参数2: 要分配的内存大小(sizeof(*edu)是最佳实践,避免类型错误)
// 参数3: 内存分配标志,GFP_KERNEL表示可以睡眠等待内存
// 它会自动完成以下工作:
// 1. 分配一块内核内存
// 2. 将内存内容全部初始化为0(这就是kzalloc和kmalloc的区别)
// 托管优势:自动调用kfree()释放内存
edu = devm_kzalloc(&pdev->dev, sizeof(*edu), GFP_KERNEL);
if (!edu)
return -ENOMEM;
// 保存pci_dev指针到私有数据结构,方便后续使用
edu->pdev = pdev;
// 从pcim_iomap_table中获取BAR0的内核虚拟地址
// pcim_iomap_table返回一个数组,包含所有已映射BAR的虚拟地址
// 数组索引就是BAR的编号(0-5)
edu->bar0 = pcim_iomap_table(pdev)[EDU_BAR];
if (!edu->bar0)
return -ENOMEM;
//-------------------------------------------------------------------------
// 阶段4:关联私有数据与PCI设备对象
//-------------------------------------------------------------------------
// 这是PCI驱动中最重要的函数之一
// 将我们的私有数据结构edu和内核的pci_dev对象关联起来
// 以后在任何地方(中断处理函数、remove函数、ioctl等)
// 只要有pdev指针,就可以用pci_get_drvdata(pdev)取回edu指针
pci_set_drvdata(pdev, edu);
// 显式启用设备的总线主模式
// 虽然pcim_enable_device已经做了这件事,但显式调用是好习惯
// 总线主模式允许设备主动发起PCIe事务(比如DMA传输)
pci_set_master(pdev);
//-------------------------------------------------------------------------
// 阶段5:打印BAR资源信息(调试用)
//-------------------------------------------------------------------------
// 获取BAR0的物理起始地址
// 永远使用pci_resource_*系列API访问资源,不要直接访问pdev->resource数组
// 因为API是稳定的,而结构体内部字段可能会随内核版本变化
start = pci_resource_start(pdev, EDU_BAR);
// 获取BAR0的大小(字节)
len = pci_resource_len(pdev, EDU_BAR);
// 打印BAR信息:物理地址、大小、映射后的内核虚拟地址
// %pa是内核专门用于打印resource_size_t类型的格式符
// %p打印内核虚拟地址
dev_info(&pdev->dev, "BAR%d start=%pa len=%pa mapped=%p\n", EDU_BAR, &start, &len, edu->bar0);
//-------------------------------------------------------------------------
// 阶段6:测试MMIO读写(验证设备通信正常)
//-------------------------------------------------------------------------
// 读取EDU设备偏移0x00的版本寄存器
// 绝对不能直接解引用__iomem指针(比如*edu->bar0)
// 必须使用内核提供的MMIO读写函数:ioread8/16/32/64和iowrite8/16/32/64
// 这些函数会自动处理:
// 1. 内存屏障(确保读写顺序正确)
// 2. 字节序转换(PCI是小端,内核是主机字节序)
// 3. 平台相关的I/O操作
id_value = ioread32(edu->bar0);
// EDU设备的版本寄存器固定返回0x00000001
dev_info(&pdev->dev, "MMIO[0x00]=0x%08x (EDU version)\n", id_value);
// 测试偏移0x04的加法寄存器
// EDU设备的0x04和0x08是加法寄存器:
// 写入两个数到0x04和0x08,读取0x04会返回它们的和
if (len >= EDU_MMIO_SIZE) {
// 读取寄存器当前值
u32 before = ioread32(edu->bar0 + 0x04);
// 写入测试值0xa5a5a5a5(交替的0和1,容易发现硬件错误)
iowrite32(0xa5a5a5a5, edu->bar0 + 0x04);
// 读回并打印,验证写入成功
dev_info(&pdev->dev, "MMIO[0x04] before=0x%08x after=0x%08x\n",
before, ioread32(edu->bar0 + 0x04));
}
// 所有初始化成功完成
return 0;
}
5.3 内核日志输出(安装驱动后)
[ 2.643928] tsc: Refined TSC clocksource calibration: 3686.332 MHz
[ 2.644364] clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x6a45cc130bc, max_idle_ns: 881591016015 ns
[ 2.644481] clocksource: Switched to clocksource tsc
[15155.974033] edu_pci: loading out-of-tree module taints kernel.
[15155.974281] edu_pci: module verification failed: signature and/or required key missing - tainting kernel
[15155.982169] edu_pci 0000:01:00.0: probe vendor=1234 device=11e8
[15155.985762] edu_pci 0000:01:00.0: BAR0 start=0x00000000fe600000 len=0x0000000000100000 mapped=(____ptrval____)
[15155.986106] edu_pci 0000:01:00.0: MMIO[0x00]=0x010000ed
[15155.986155] edu_pci 0000:01:00.0: MMIO[0x04] before=0x00000000 after=0x5a5a5a5a
5.4在sysfs中呈现的结果(安装驱动之后 子系统驱动的结果)

edu_pci是我们手动安装的结果,已经和硬件绑定过了,通过日志也可以反应出来
pcieport是系统自带的驱动,因为这个驱动是和设备PCIe Root Port 1绑定
剩下的 10 多个驱动(如8250_mid、agpgart-intel、virtio-pci等)都是内核编译时默认包含的,但在你的虚拟机中没有对应的设备,所以处于 "待命" 状态
5.5 edu_pci驱动的细节

5.5.1 该驱动与pci总线之间的拓扑关系
lrwxrwxrwx 1 0 0 0 May 14 13:06 0000:01:00.0 -> ../../../../devices/pci0000:00/0000:00:1c.0/0000:01:00.0
说明:edu_pci 驱动已经成功注册到 PCI 总线,驱动的 probe 函数已经执行完成,如果驱动没有绑定设备,这个符号链接就不会存在。
5.5.2 bind 和 unbind
--w------- 1 0 0 4096 May 14 13:06 bind
--w------- 1 0 0 4096 May 14 13:06 unbind
这是实际开发中最常用的两个接口,允许手动绑定和解绑驱动与设备,不需要重新加载内核模块。
# 手动解绑 edu_pci 驱动和 0000:01:00.0 设备
echo 0000:01:00.0 > unbind
# 手动重新绑定 edu_pci 驱动和 0000:01:00.0 设备
echo 0000:01:00.0 > bind
- 调试驱动时,不需要反复
rmmod和insmod- 可以强制让一个设备使用特定的驱动
- 可以解决驱动冲突问题
5.5.3module 符号链接
lrwxrwxrwx 1 0 0 0 May 14 13:06 module -> ../../../../module/edu_pci
这个链接指向/sys/module/edu_pci/目录,包含了这个内核模块的所有信息:
refcnt:模块的引用计数(有多少个设备在使用这个驱动)holders:依赖这个模块的其他模块parameters:模块的参数sections:模块的代码段和数据段信息
# 查看 edu_pci 模块的引用计数
cat /sys/bus/pci/drivers/edu_pci/module/refcnt
# 输出应该是 1,因为有一个设备在使用这个驱动
5.5.4new_id 和 remove_id
--w------- 1 0 0 4096 May 14 13:06 new_id
--w------- 1 0 0 4096 May 14 13:06 remove_id
这两个接口允许你动态添加和删除驱动支持的设备 ID,不需要重新编译驱动。
# 让 edu_pci 驱动支持 Vendor ID 0x1234,Device ID 0x11e9 的设备
echo "1234 11e9" > new_id
# 移除刚才添加的设备 ID
echo "1234 11e9" > remove_id
- 调试新设备时,可以快速测试驱动是否能支持新的设备 ID
- 不需要修改驱动代码和重新编译
- 可以临时让一个通用驱动支持特定的设备
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)