QEMU PCIe 设备热插拔与异步移除
一、PCIe 设备热插拔的处理范围
PCIe 设备热插拔允许在虚拟机运行期间添加或移除网卡、存储控制器等设备。QEMU 通过端口的插槽状态与事件通知客户机,客户机操作系统再完成设备枚举、驱动绑定或停用。管理程序负责发起变更,并在设备移除后回收独立的宿主后端。
这是虚拟机热插拔中的设备操作路径。CPU 热插拔、内存容量调整与 PCIe 设备增删都涉及资源准备、客户机状态更新和资源回收,但使用的通知协议与资源约束不同。本文以 q35 上预先创建的 PCIe Root Port 和一块现代 virtio-net-pci 网卡为例,分析 PCIe 原生热插拔;ACPI PCI 热插拔则使用另一套通知和应答路径。
例子中的端口是 rp1,网卡是 net1,宿主网络后端是 n1。添加流程依次为预留插槽、创建后端与网卡、发布插槽事件、客户机枚举并绑定驱动。删除流程先通知客户机停用设备,再拆除网卡,最后清理独立后端。下文沿这两条流程说明拓扑、寄存器、源码回调和完成事件。
源码版本:QEMU 10.2.0。
二、插槽拓扑与资源预留
PCIe 原生热插拔控制器位于 Root Port 或 Downstream Port。网卡接在端口的下游总线上,端口自己的配置空间保存插槽状态。启动时直接放在 pcie.0 上的集成端点没有这套下游插槽机制;device_del 的支持范围取决于设备类型、端口能力与机器拓扑。PCIe 拓扑与热插拔规划
本例启动配置中的相关部分为:
-machine pc-q35-10.2,accel=kvm
-global ICH9-LPC.acpi-pci-hotplug-with-bridge-support=off
-device pcie-root-port,id=rp1,chassis=1,slot=1
第一行选择机器型号;第二行关闭本例的 ACPI PCI 热插拔控制,让客户机协商使用原生路径;第三行预留一个端口。当前 q35 也支持 ACPI 控制 PCI 热插拔,是否走本文路径需要同时看机器兼容属性、固件的 _OSC 协商和客户机驱动。ACPI 控制属性、原生控制权协商
Mermaid 源码
flowchart TB
accTitle: PCIe 热插拔的端口、端点与网络后端
accDescr: Q35 根总线连接预先创建的 rp1 端口,下游总线连接热插入的 net1 网卡。端口插槽寄存器向客户机热插拔驱动报告变化,网卡经独立的 n1 网络后端处理数据。
ROOT["q35 · pcie.0 根总线"] --> RP["rp1 · PCIe Root Port<br/>启动时预留"]
RP --> BUS["rp1 下游总线<br/>总线号与桥窗口"]
BUS --> NIC["net1 · virtio-net-pci<br/>运行时添加的端点"]
RP -. 插槽寄存器与事件中断 .-> HP["客户机热插拔驱动<br/>枚举 / 配置 / 停用"]
HP -. 配置与驱动绑定 .-> NIC
NIC --> NET["n1 · netdev 后端<br/>独立创建与清理"]
空端口只解决拓扑位置。客户机还需要总线号、BAR 对应的 MMIO 窗口和中断资源。固件通常在启动时安排桥窗口,后续客户机能否重分配资源也受系统能力限制。QEMU 的 Root Port 提供 bus-reserve、mem-reserve、pref64-reserve 等资源预留属性;它们向固件描述需求,不代表插入后必然能得到资源。资源预留属性
三、添加 net1:创建、通知与客户机枚举
管理程序先创建网络后端,再把设备连接到它。以下是完成 QMP 握手后的请求示例,id 位于命令外层时用于匹配响应,位于 arguments 内时表示设备或后端标识。
{"execute":"netdev_add","arguments":{"type":"user","id":"n1"},"id":"backend-add"}
{"return":{},"id":"backend-add"}
{"execute":"device_add","arguments":{"driver":"virtio-net-pci","id":"net1",
"bus":"rp1","netdev":"n1","disable-legacy":true},"id":"nic-add"}
{"return":{},"id":"nic-add"}
user 后端需要相应构建支持,示例选它以集中观察设备生命周期。实际发包的队列与后端处理见网卡模拟与 virtio-net 发包路径。
下面流程中的 PDS 表示插槽当前有设备,PDC 表示存在状态发生过变化,ABP 表示注意按钮事件。客户机读取这些状态并处理通知;完整寄存器和回调表放在添加、移除流程之后。
- 查类型和总线qdev_device_add_from_qdict() 查找设备类和 rp1 总线,检查类型、可用位置与迁移状态。
- 创建并设置属性qdev_new() 建立对象,设置 net1 标识及 netdev 等属性,再调用 qdev_realize()。
- 检查与初始化热插拔处理器执行 pre_plug;设备实现注册 BAR、队列和后端关联;框架完成后续初始化。
- 发布插槽变化端口 plug 回调置 PDS,并记录 PDC / ABP 等事件。满足使能条件后向客户机通知。
- 客户机配置设备客户机扫描下游总线、分配资源、绑定驱动并初始化 virtio 队列。
单功能端点的插入会让插槽报告存在;多功能设备还有 function 0 的可见性和添加顺序要求,不能直接把本例推广成每添加一个 function 都发同样通知。插入回调
device_add 成功表示 QEMU 已完成本次设备创建路径,客户机驱动仍可能尚未绑定,甚至因 BAR 资源不足而失败。反过来,设备创建报错时,前一步单独创建的 n1 后端也不会作为这条命令的事务自动回滚,需要管理程序按实际关联情况处理。QOM 的创建与 realize 关系见对象模型与设备初始化。
四、删除 net1:客户机停用与完成事件
对正在使用的网卡,管理程序首先发送移除请求。QEMU 找到 net1 后调用 qdev_unplug(),若处理器实现了 unplug_request,就交给它发起与客户机的交接。此时模型和后端关联可能仍然存在。
{"execute":"device_del","arguments":{"id":"net1"},"id":"nic-del"}
{"return":{},"id":"nic-del"}
稍后收到事件(这里省略 timestamp):
{"event":"DEVICE_DELETED","data":{"device":"net1","path":"/machine/peripheral/net1"}}
确认设备已删除后,清理独立后端:
{"execute":"netdev_del","arguments":{"id":"n1"},"id":"backend-del"}
上面展示常见的先响应、后事件顺序。管理程序需要持续接收事件并按设备 ID 或 QOM 路径关联,不能假定它与命令响应的相对位置。删除请求的响应 ID 也不会变成 DEVICE_DELETED 的匹配字段。异步删除接口约定
Mermaid 源码
sequenceDiagram
accTitle: 运行中 PCIe 端点的异步热拔出
accDescr: 管理程序请求删除,QEMU 发出插槽注意按钮事件。客户机停用设备后关闭插槽,QEMU 执行 unrealize、unparent 与最终引用回收,发送设备删除完成事件。命令响应与最终完成分离。
participant M as 管理程序
participant Q as QEMU QMP / qdev
participant P as Root Port 插槽
participant G as 客户机热插拔与设备驱动
M->>Q: device_del net1
Q->>P: unplug_request,设置 pending
P-->>G: 注意按钮事件 / 插槽中断
Q-->>M: 请求受理,return {}
Note over M,G: 图示一种常见顺序;响应与完成事件先后不固定
G->>G: 停止设备使用,释放客户机资源
G->>P: 写 Slot Control:PCC / PIC 均 off
P->>Q: unplug → qdev_unrealize
Q->>Q: object_unparent,释放引用
Note over Q,P: 插槽不再有设备;最终回收时发事件
Q-->>M: DEVICE_DELETED,device = net1
M->>Q: netdev_del n1,清理独立后端
原生插槽回调先检查热插拔能力和锁定状态,再设置删除标记,并模拟注意按钮事件。客户机热插拔驱动处理请求,协调设备驱动停用、释放资源,并改变插槽控制寄存器。QEMU 在检测到有设备的插槽从未关闭状态切换到 PCC = off 且 PIC = off 时,调用 pcie_cap_slot_do_unplug()。已经关闭的插槽和部分尚未向客户机暴露的设备存在直接拆除分支。请求回调、关闭插槽后的拆除
实际拆除依次调用处理器的 unplug、qdev_unrealize() 和 object_unparent()。unrealize 停止设备实现并撤销其资源;unparent 解除对象树及总线关系;对象引用释放到零后才进入 finalize。device_finalize() 在最后阶段检查删除标记并发送 DEVICE_DELETED,之后释放对象内存。拆除端点、最终事件与父子关系清理
端口模型按插槽处理下游设备,多功能设备可能一起被移除。本文使用单功能网卡;生产管理层需要按实际插槽与功能关系决定操作范围,不能只看一个 QMP ID。
五、插槽状态、事件与热插拔回调
| 对象或寄存器 | 关键字段 | 作用 |
|---|---|---|
| DeviceState | id、parent_bus、realized、hotplugged | 标识设备、连接总线,区分初始化完成和启动后添加 |
| 删除交接 | pending_deleted_event、pending_deleted_expires_ms | 记录待完成的删除通知及重复请求检查窗口 |
| HotplugHandlerClass | pre_plug、plug、unplug_request、unplug | 分别完成接入检查、插入通知、移除请求和实际拆除 |
| Slot Capabilities · 32 位 | HPC、PCP 等 | 声明热插拔、插槽电源控制等能力 |
| Slot Status · 16 位 | PDS、PDC、ABP、CC | 设备存在、存在状态变化、注意按钮事件、控制命令完成 |
| Slot Control · 16 位 | HPIE、PDCE、ABPE、PCC、PIC | 事件使能、电源控制和电源指示灯状态 |
PDS 表示设备是否存在,PDC 表示存在状态发生过变化,不能混用。事件状态位按写 1 清除等寄存器语义处理;QEMU 只有在事件和对应使能条件满足时才发出通知。这里的中断来自端口热插拔控制器,与网卡收发完成中断是两条路径。事件与中断条件、插槽寄存器初始化
六、异常状态与资源回收条件
可以先对照 QEMU 与客户机的设备视图。宿主侧通过已协商的 QMP 连接执行 {"execute":"query-pci"},沿 pci_bridge.devices 找到 qdev_id = net1,核对总线位置和 BAR;客户机内执行 lspci -nnk 查看设备及绑定驱动,执行 ip -br link 查看网络接口。QEMU 的 net1 标识不会直接成为客户机的网卡名称,二者应通过 PCI 位置、设备类型等信息关联。query-pci 的返回字段
| 看到的现象 | 含义与检查位置 |
|---|---|
| device_add 立即报错 | 检查设备类、总线、ID 冲突、属性和资源初始化;未进入成功发布路径 |
| 添加成功,客户机没有可用网卡 | 分别检查插槽通知、客户机枚举、BAR 分配和驱动;QMP 成功不覆盖客户机这几步 |
| device_del 成功,迟迟无完成事件 | 请求仍可能等待客户机处理;检查插槽状态、驱动占用与客户机是否运行,保留资源和待处理状态 |
| 重复删除提示正在移除 | QEMU 的 pending 标志阻止重复请求;它不证明上一条已经完成 |
| 监控连接断开 | 重连后重新核对 QOM / PCI 设备状态;不能仅凭本地等待超时就复用设备 ID 或删除仍在使用的后端 |
本路径中的 pending_deleted_expires_ms 被设为虚拟时钟当前值加 5 秒,配合 qmp_device_del 的重复请求检查。这个数值不表示五秒后自动强制删除,也不保证客户机在五秒内完成;虚拟时钟还可能在暂停时停止推进。设置检查窗口、重复请求判定
QMP 定义了 DEVICE_UNPLUG_GUEST_ERROR 事件,但只有能检测到相应客户机错误的路径才会发送。未收到错误事件也不能证明操作正常完成。对于直通设备,宿主驱动、IOMMU 映射和设备复位又增加了独立约束,相关地址隔离见VFIO DMA 路径。
七、源码:插槽通知与设备最终拆除
通用设备层通过热插拔处理器找到端口实现。Root Port 继承 PCIESlot 的处理接口,插槽类初始化时登记下面四个回调;端口实例初始化时,再用 qbus_set_hotplug_handler() 把下游总线与自己关联:
/* pcie_slot_class_init() 中的回调登记 */
hc->pre_plug = pcie_cap_slot_pre_plug_cb;
hc->plug = pcie_cap_slot_plug_cb;
hc->unplug = pcie_cap_slot_unplug_cb;
hc->unplug_request = pcie_cap_slot_unplug_request_cb;
因此,删除 net1 时,qdev_unplug() 从设备所在总线取得处理器 rp1,通过 unplug_request 进入插槽通知逻辑。随后 QMP 调用结束,等待客户机处理。客户机写入插槽控制寄存器时,另一次配置访问进入 rp_write_config(),检查电源状态变化,再触发实际拆除。两段处理由插槽寄存器和设备的待删除状态连接,期间网卡对象仍然存在。回调登记、总线与处理器关联
| 入口 | 检查重点 |
|---|---|
| qdev_device_add_from_qdict() | 查类、查总线、创建对象、设置属性与 realize,错误路径的引用释放 |
| device_set_realized() | pre_plug、设备初始化与 plug 的顺序,后续阶段失败时的回滚 |
| qdev_unplug() / qmp_device_del() | 异步请求与同步拆除的分支,以及迁移和重复请求限制 |
| pcie_cap_slot_unplug_request_cb() | 插槽能力、锁定、电源状态及注意按钮通知 |
| rp_write_config() | 保存旧插槽状态、应用配置写入,再检查电源状态切换 |
| device_finalize() | 最后阶段发送 DEVICE_DELETED,而非在请求受理时发送 |
八、控制方式与管理接口的演进
PCI / PCIe 热插拔的控制方式与所用机器和桥类型有关。ACPI 通过固件描述、通知及 eject 方法组织交接;PCIe 原生路径把插槽能力、事件和电源控制放在端口标准寄存器中,由客户机的 PCIe 热插拔驱动处理。两种路径长期并存,不能简单按“新旧 QEMU”划分。
2017 年 QEMU 引入通用 PCIe Root Port,将端口能力与特定芯片模型解耦;随后增加资源预留能力,帮助固件为后续设备分配空间。统一端口模型简化了拓扑组织,但总线号、桥窗口和客户机支持仍需规划。
管理接口也逐步把“发起”和“完成”分开:QMP 的 device_del 从 0.14 提供,DEVICE_DELETED 从 1.5 提供,DEVICE_UNPLUG_GUEST_ERROR 从 6.2 提供。完成事件让管理程序能够安排 ID 复用及后端清理;代价是需要维护异步状态、处理连接中断,并区分可检测错误和无响应。版本标注见 QAPI 定义。