virt-report虚拟化社区动态

QEMU PCIe 设备热插拔与异步移除

本文由 OpenAI Codex (GPT-6-Astra) 辅助编写。

一、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 控制属性、原生控制权协商

端口、网卡与后端的关联
Root Port 的插槽寄存器负责通知客户机;virtio 网卡是下游端点;netdev 是宿主后端对象。三者有不同的标识和生命周期。
Root Port 的插槽寄存器负责通知客户机;virtio 网卡是下游端点;netdev 是宿主后端对象。三者有不同的标识和生命周期。 打开原图
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 表示注意按钮事件。客户机读取这些状态并处理通知;完整寄存器和回调表放在添加、移除流程之后。

  1. 查类型和总线qdev_device_add_from_qdict() 查找设备类和 rp1 总线,检查类型、可用位置与迁移状态。
  2. 创建并设置属性qdev_new() 建立对象,设置 net1 标识及 netdev 等属性,再调用 qdev_realize()。
  3. 检查与初始化热插拔处理器执行 pre_plug;设备实现注册 BAR、队列和后端关联;框架完成后续初始化。
  4. 发布插槽变化端口 plug 回调置 PDS,并记录 PDC / ABP 等事件。满足使能条件后向客户机通知。
  5. 客户机配置设备客户机扫描下游总线、分配资源、绑定驱动并初始化 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 的匹配字段。异步删除接口约定

PCIe 原生热拔出的交接
图示已上电、正在使用的单功能端点。QMP 返回只表示请求已受理;客户机停用设备并关闭插槽后,QEMU 才执行拆除,完成后再报告 DEVICE_DELETED。
图示已上电、正在使用的单功能端点。QMP 返回只表示请求已受理;客户机停用设备并关闭插槽后,QEMU 才执行拆除,完成后再报告 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。

五、插槽状态、事件与热插拔回调

对象或寄存器关键字段作用
DeviceStateid、parent_bus、realized、hotplugged标识设备、连接总线,区分初始化完成和启动后添加
删除交接pending_deleted_event、pending_deleted_expires_ms记录待完成的删除通知及重复请求检查窗口
HotplugHandlerClasspre_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 定义。

九、参考资料