4. Linux设备模型
PCIe热插拔在用户空间看起来只是“设备突然出现或消失”,但在内核内部,它依赖一整套设备模型把硬件、驱动、总线、类接口和uevent事件串联起来。理解kobject、kset、device、driver、bus、class之间的关系,是排查热插拔事件丢失、驱动未绑定、sysfs节点缺失等问题的关键。
一、核心原理:设备模型的分层与引用
Linux设备模型的核心是kobject。它不是一个具体设备,而是所有设备模型对象的“基类”,提供引用计数、sysfs目录项、uevent上报等基础能力。kobject通常嵌入在更大的结构体中,例如device、driver、bus_type、class等。kset则是kobject的集合,用于组织层级和统一管理同类对象。
在PCIe热插拔场景中,几个关键对象的关系如下:
- bus:表示总线类型,例如pci_bus_type。它定义match、probe、remove、uevent等回调,是device与driver匹配的规则中心。
- device:表示一个具体设备实例,例如PCIe下游端口下的网卡、NVMe盘。它注册到bus上,并在sysfs中生成设备目录。
- driver:表示设备驱动,例如nvme、igb、pcieport。它同样注册到bus上,等待与device匹配。
- class:表示按功能划分的设备类,例如net、block、pci_bus。它提供统一的用户空间接口,如/sys/class/net/eth0。
- kset:用于把同类kobject组织成集合,例如/sys/bus/pci/devices、/sys/class/net。
当PCIe热插拔控制器检测到插槽状态变化时,内核PCI核心会创建或销毁pci_dev,并调用device_add或device_del。device_add会触发bus的匹配流程,尝试把device与driver绑定;绑定成功后,driver的probe被调用,设备正式可用。同时,device_add还会通过kobject_uevent发送uevent,通知用户空间。
二、方法流程:从热插拔事件到用户空间
下面以PCIe网卡热插入为例,梳理内核与用户空间的事件流:
- PCIe热插拔控制器检测到插槽有卡插入,产生中断或轮询事件。
- PCI核心读取配置空间,分配总线号、内存和I/O资源,创建pci_dev。
- 调用device_add(),把设备注册到设备模型,并在/sys/devices/pci...下建立目录。
- bus的match函数比较device与已注册driver的id_table或of_match_table。
- 匹配成功后调用driver的probe,初始化硬件并注册netdev、blockdev等类设备。
- kobject_uevent()发送KOBJ_ADD事件,携带ACTION、DEVPATH、SUBSYSTEM、PCI_SLOT_NAME等环境变量。
- 用户空间udev/systemd-udevd接收uevent,根据规则创建/dev节点、重命名接口、加载固件或执行脚本。
热拔出时流程相反:先调用driver的remove,再device_del,最后发送KOBJ_REMOVE事件。用户空间收到后删除设备节点并清理状态。
# 观察PCIe设备热插拔uevent的常用命令
udevadm monitor --kernel --property --subsystem-match=pci
udevadm monitor --udev --property
# 查看设备模型中的关键节点
ls -l /sys/bus/pci/devices/
ls -l /sys/bus/pci/drivers/nvme/
ls -l /sys/class/net/
cat /sys/bus/pci/devices/0000:01:00.0/uevent
三、案例场景:NVMe盘热插入后的对象变化
假设一台服务器通过PCIe热插拔槽位插入一块NVMe SSD。内核首先在/sys/bus/pci/devices/下新增0000:3b:00.0目录,其中包含vendor、device、class、driver等属性。bus匹配到nvme驱动后,/sys/bus/pci/drivers/nvme/下出现该设备的符号链接。随后nvme驱动创建block类设备,/sys/class/block/nvme0n1出现,并触发KOBJ_ADD uevent。
用户空间udev收到事件后,依据SUBSYSTEM=block和DEVTYPE=disk创建/dev/nvme0n1。如果此时驱动未加载,设备会停留在“已枚举但未绑定”状态,sysfs中driver链接为空,uevent仍会发送,但用户空间可能只看到PCI设备而没有块设备。这正是热插拔排障中常见的“设备可见但不可用”现象。
| 对象 | 典型sysfs路径 | 热插拔中的作用 |
|---|---|---|
| kobject | /sys/devices/.../uevent | 提供uevent上报和引用计数基础 |
| kset | /sys/bus/pci/devices/ | 组织同类设备集合,便于遍历和管理 |
| device | /sys/devices/pci0000:00/.../0000:3b:00.0 | 表示具体PCIe设备实例 |
| driver | /sys/bus/pci/drivers/nvme/ | 与device匹配并执行probe/remove |
| bus | /sys/bus/pci/ | 定义匹配规则和uevent行为 |
| class | /sys/class/block/nvme0n1 | 提供功能视角的用户空间接口 |
四、误区与复盘:热插拔事件为何“看起来没发生”
第一个常见误区是认为“设备出现在lspci里就等于热插拔成功”。实际上,lspci只反映PCI配置空间可见,不代表driver已绑定,也不代表class设备已创建。若driver未加载或match失败,uevent仍可能发送,但用户空间不会得到可用的/dev节点。
第二个误区是忽略uevent的时序。device_add先注册sysfs,再发送uevent;用户空间处理uevent时,sysfs通常已经就绪。但如果udev规则中访问了尚未创建的class节点,就可能失败。因此规则中应优先使用DEVPATH和SUBSYSTEM,而不是假设某个class路径一定存在。
第三个误区是混淆“热插拔事件”与“驱动probe”。uevent是通知机制,probe是绑定动作。两者可能成功一个失败另一个。排障时应依次检查:PCI设备是否枚举、driver是否匹配、probe是否返回成功、uevent是否发出、用户空间是否收到并处理。
1. 用lspci -tv确认设备是否枚举;
2. 用ls -l /sys/bus/pci/devices/.../driver确认绑定;
3. 用dmesg | grep -i pcie查看热插拔和probe日志;
4. 用udevadm monitor确认uevent是否到达用户空间;
5. 检查udev规则是否匹配SUBSYSTEM、ACTION、DEVPATH。
理解kobject、kset、device、driver、bus、class之间的层次与事件关系,就能把“热插拔”从黑盒变成可观测、可追踪、可复盘的工程链路。后续无论是调试PCIe插槽事件丢失,还是编写udev规则,都可以沿着这条链路逐层定位。