QEMU 对象模型与设备初始化
一、QOM 类型复用与设备实例状态
QEMU 用 C 语言实现大量设备模型。每种设备既有独立的寄存器、队列和配置,又需要复用总线、复位、迁移与热插拔框架。QEMU Object Model(QOM)提供类型、继承、属性和对象生命周期;qdev 在其上增加设备与总线的组织规则。
以两块 virtio-net-pci 网卡为例,两者复用相同的 PCI 和 virtio 方法,却各有队列、MAC 地址和后端。这些类与实例都位于宿主 QEMU 进程中,客户机看到的是它们提供的 PCI 配置空间和设备接口。QOM 把共同方法放在类中,把每块网卡的状态放在实例中;PCI 包装对象组合独立的 virtio 网络对象,分别负责传输接口与网络功能。
创建一块网卡依次经过“查找类型 → 初始化实例 → 设置属性 → 接入总线并 realize”。下面先看网卡由哪些对象组成、配置如何生效,再展开类型表与初始化实现。realize 在此表示检查配置并建立设备工作状态,客户机驱动的初始化发生在随后。
源码版本:QEMU 10.2.0。
二、virtio-net-pci 的继承、组合与总线
virtio-net-pci 的类型链经过 virtio-net-pci-base、virtio-pci、pci-device、device,最终到 object。具体类型由 virtio_pci_types_register() 根据描述生成,包含通用、transitional 和 non-transitional 变体。网卡类型描述、类型生成
struct VirtIONetPCI {
VirtIOPCIProxy parent_obj; /* PCI 传输层:继承 */
VirtIONet vdev; /* 网络设备实例:组合 */
};
这里有两个独立的 QOM 对象。外层对象继承 PCI 传输层,嵌入的 vdev 另有自己的 Object、类指针和引用计数。virtio_instance_init_common() 把它初始化成 virtio-net-device,以 virtio-backend 为名称加入外层对象的组合树。嵌入结构的内存由外层一起分配,内部对象的初始化和生命周期仍由 QOM 管理。实例布局、内部对象初始化
Mermaid 源码
flowchart TB
accTitle: QOM 类型继承、对象组合与总线连接
accDescr: TypeInfo 注册为 TypeImpl,后者关联一份按需初始化的类对象。virtio-net-pci 实例继承 PCI 传输层并组合一个 virtio-net-device 子对象;外层接 PCI 总线,内部设备接 virtio 总线。组合树所有权与总线拓扑互相区分。
subgraph types[类型:每种类型共享类对象]
info["TypeInfo<br/>name · parent · sizes · callbacks"]
impl["TypeImpl<br/>parent_type · class"]
klass["virtio-net-pci 的类对象<br/>继承 virtio-net-pci-base → virtio-pci<br/>→ pci-device → device → object"]
info -->|注册到类型表| impl
impl -->|按需初始化| klass
end
subgraph objects[实例:每块网卡分别保存状态]
outer["外层 VirtIONetPCI<br/>parent_obj:VirtIOPCIProxy<br/>Object.class · ref · parent"]
inner["嵌入的 VirtIONet vdev<br/>独立 Object 与类指针<br/>类型 virtio-net-device"]
bus["外层创建的 virtio 总线<br/>VirtIOPCIProxy.bus"]
outer -->|child 属性 virtio-backend:强引用| inner
outer -->|创建并拥有子总线| bus
inner -. parent_bus .-> bus
end
container["机器对象树的容器<br/>例如 peripheral"]
pci["PCI 总线<br/>管理 PCI 连接与地址"]
outer -.->|"Object.class"| klass
container -->|child 所有权| outer
outer -. parent_bus .-> pci
classDef type fill:#eaf0f6,stroke:#8198ac,color:#263c4c
classDef instance fill:#e4f1ec,stroke:#438575,color:#173e34
classDef topology fill:#f5f4f1,stroke:#a6aaa7,color:#3c4542
class info,impl,klass type
class outer,inner instance
class bus,pci,container topology
QOM 的 Object.parent 和 qdev 的 DeviceState.parent_bus 可以不同。外层网卡可以放在机器的 peripheral 容器下,却接到某条 PCI 总线上;内部网络对象归外层所有,却接到外层创建的 virtio 总线上。前者组织对象的路径与所有权,后者决定总线操作、复位和设备连接关系。调试时,info qom-tree 与 info qtree 回答的是不同问题。
对 Libvirt 管理的测试虚拟机,可在宿主机执行以下只读查询,比较同一网卡的对象路径与总线位置。demo 替换为实例名称;第一条显示组合树,第二条显示设备/总线树,二者都不能直接证明客户机驱动已经工作:
virsh -c qemu:///system qemu-monitor-command demo --hmp 'info qom-tree'
virsh -c qemu:///system qemu-monitor-command demo --hmp 'info qtree'
相应命令由 QEMU 的HMP 查询接口提供;自行启动的 QEMU 可在其 HMP monitor 中直接输入 info qom-tree 与 info qtree。
三、设置属性:从 vectors=5 到实例字段
属性包含名称、类型、读写回调和相关数据。qdev 的字段属性进一步记录结构偏移与类型处理函数,设置属性时先检查是否允许修改,再解析输入,最后写入对应实例字段。因此命令行字符串、QMP 输入和 C 调用可以进入同一套属性处理逻辑。
给第一块网卡配置 virtio-net-pci,id=net0,vectors=5,vectors 这个 32 位无符号字段属性就会写入 net0 的 VirtIOPCIProxy.nvectors。第二块网卡 net1 的同名字段不随之变化。属性赋值保存数值;后续 realize 才据此建立传输层资源。其默认值是“未指定”,网卡 realize 会按队列对数量计算默认向量数,显式设置的 5 则保留下来。属性与默认值处理
外层对象还把内部网络设备的属性建立为别名,使用户能在同一个设备配置中设置网络功能。别名最终访问原对象的属性,不会创建第二份网络状态。bootindex 也显式指向内部设备的同名属性。别名建立
多数配置字段要求在 realize 前设置,避免运行中的资源规模与字段值脱节。qdev 的字段 setter 检查 realized_set_allowed,只有允许动态修改的类型才能越过该限制;自定义 QOM 属性则由各自的 setter 决定。realized 本身也是属性,它的写回调会执行生命周期操作,不能将所有属性都视作普通内存赋值。字段写入限制
四、realize 与两层设备实现的连接
qdev_new() 仅调用 QOM 创建对象,初始 realized=false。这一阶段建立局部状态、子对象与属性,不依赖用户已经填完全部配置。能够失败的资源连接、参数校验和设备启用集中在 realize 阶段,通过 Error ** 返回错误。对象已分配与设备已经接入虚拟机是两个时点。
Mermaid 源码
flowchart TB
accTitle: qdev 从对象创建到设备释放的生命周期
accDescr: 新对象先执行父类到子类的实例初始化,再设置属性、关联父总线并进入 realize。回调自身失败需局部清理,框架后续阶段失败则回滚已成功的实现。正常设备通过 unrealize 和 unparent 停用、脱离总线,最后引用归零才执行 finalize。
create["qdev_new → object_new<br/>引用计数初值 1"]
init["instance_init:父类 → 子类<br/>建立局部状态与子对象<br/>随后 instance_post_init"]
props["配置属性<br/>例如 vectors = 5<br/>realized = false"]
attach["qdev_realize<br/>关联 parent_bus<br/>设置 realized 属性为 true"]
before["框架前置检查<br/>必要的组合树挂接 · pre-plug"]
callback["DeviceClass.realize<br/>校验完整配置并建立资源"]
after["框架后续操作<br/>VMState 注册 · 子总线 · plug"]
ready["发布 realized = true<br/>设备实现完成"]
failLocal["回调失败<br/>实现层清理已取得的资源<br/>框架不自动调用 dc.unrealize"]
rollback["后续阶段失败<br/>撤销已完成的框架步骤<br/>调用 dc.unrealize"]
failure["返回错误<br/>未发布 realized = true<br/>调用者按所有权清理对象"]
stop["正常停用:unrealize<br/>停止设备并撤销运行资源"]
detach["unparent<br/>脱离组合树和父总线<br/>释放相应引用"]
final["最后一个引用释放<br/>删除实例属性<br/>instance_finalize:子类 → 父类"]
create --> init --> props --> attach --> before --> callback
callback -->|成功| after -->|成功| ready
callback -->|失败| failLocal --> failure
before -->|失败| failure
after -->|失败| rollback --> failure
ready --> stop --> detach --> final
failure -->|释放剩余引用| final
classDef normal fill:#e4f1ec,stroke:#438575,color:#173e34
classDef lifecycle fill:#eaf0f6,stroke:#8198ac,color:#263c4c
classDef error fill:#f6eee5,stroke:#b59373,color:#59402b
class create,init,props,ready normal
class attach,before,callback,after,stop,detach,final lifecycle
class failLocal,rollback,failure error
qdev_realize(dev, bus, errp) 先关联父总线,再写 realized=true。device_set_realized() 执行可迁移性检查、必要的对象树挂接、热插拔 pre-plug、设备类 realize,随后处理监听器、VMState 注册、子总线与热插拔 plug,最后发布成功状态。中途失败不会把 dev->realized 留为 true。入口、框架顺序
外层 virtio-net-pci 的 DeviceClass.realize
virtio_pci_dc_realize()
保存的父设备方法 → pci_qdev_realize()
PCIDeviceClass.realize → virtio_pci_realize()
建立 PCI 传输层与 virtio 总线
VirtioPCIClass.realize → virtio_net_pci_realize()
qdev_realize(内部 VirtIONet, virtio 总线)
virtio 设备框架 → virtio_net_device_realize()
这条调用链经过三种不同的类回调,最终又创建了一次内部设备的 realize 过程。VirtIONetPCI 的 class-init 设置的是 VirtioPCIClass.realize;PCI 公共逻辑与 virtio 网络功能由不同层负责。网络功能初始化队列及后端连接后,客户机仍需枚举设备、协商特性并建立队列,才会开始正常收发数据。传输层调用功能层、网络封装层、virtio 请求路径
五、TypeInfo、类方法与实例内存
TypeInfo 是注册时的类型描述。QOM 把它转成内部 TypeImpl,以类型名为键保存。类对象按需初始化,每个类型共享一份;设备实例各自分配状态,通过 Object.class 指向所属类。创建两块同型号网卡时,队列与寄存器各有一份,实现这些操作的类方法则可以共用。
| 结构 | 关键字段 | 用途 |
|---|---|---|
TypeInfo | name、parent、instance_size、class_size、初始化与析构回调 | 描述类型名、父类型、内存布局及生命周期入口 |
TypeImpl | parent_type、class、实例大小与回调 | 缓存解析后的父类型与类对象,供类型查找和实例创建使用 |
ObjectClass | type、interfaces、properties、unparent | 类级类型信息、接口和属性描述;设备派生类继续增加方法 |
Object | class、ref、parent、properties、free | 单个对象的类指针、引用计数、组合树父节点及释放方式 |
DeviceClass / DeviceState | 类:realize、unrealize、vmsd;实例:realized、parent_bus、child_bus | 设备行为与迁移描述由类提供,设备当前状态和总线连接由实例保存 |
定义见 Object 与 ObjectClass、TypeInfo、TypeImpl 和 设备类与实例。QOM 支持单继承和多个无实例状态的接口;接口用于声明一组能力,实际状态仍保存在实现它的对象中。
派生实例把父结构放在首成员,例如 DeviceState.parent_obj 是 Object。因此从派生实例向基类转换时,内存起点一致。DEVICE(obj) 等转换同时表达预期类型;它们不会新建另一个设备对象。
六、注册类型并继承初始化方法
type_register_static() 把描述加入类型表,此时不必分配每种类型的类对象。首次需要类时,type_initialize() 分配本类型的类结构,确保父类已经初始化,再把父类结构复制到子类前部,最后执行本类型的 class_init。函数指针由此继承;子类可以覆盖其中一部分。类型注册、类初始化
前面 net0 的 realize 调用链依赖这些预先建立的函数指针。virtio_pci_class_init() 用 device_class_set_parent_realize() 保存原来的设备 realize,再安装自己的包装函数;运行到 net0 的 realize 时,包装函数设置 PCIe 能力并显式调用保存的父方法,随后才到网卡功能层。继承提供默认方法,覆盖与显式调用决定实际执行顺序。virtio PCI 的父方法调用
实例初始化的规则不同:object_init_with_type() 自动按父类到子类的顺序调用各级 instance_init,之后再执行同方向的 instance_post_init。新实例的状态内存先清零,属性默认值在初始化过程中建立;qdev 的 post-init 阶段还会应用机器兼容属性和全局属性。实例回调顺序、对象初始化、兼容属性
七、失败回滚与对象释放
在 device_set_realized() 中,dc->realize() 自身返回错误时直接进入 fail 分支,不会自动调用 dc->unrealize()。这个回调可能只完成一半初始化,其内部及它调用的各层必须清理各自已取得的资源。若设备回调已经成功,随后 VMState 注册、子总线或 plug 阶段失败,框架才走对应的回滚路径,调用 unrealize 并撤销已完成的注册。失败位置、回滚分支
unrealize 撤销设备的工作状态,unparent 拆除组合树关系,finalize 处理引用归零后的最终清理。正常设备 unparent 会先按需 unrealize、移除子总线并脱离父总线;随后组合树释放它持有的引用。其他持有者仍在时,对象内存继续存在。设备 unparent
| 操作 | 引用与生命周期变化 |
|---|---|
object_new() | 返回一个调用者持有的初始引用 |
object_property_add_child() | 父对象新增一个对子对象的强引用;子对象只能有一个组合树父节点 |
object_initialize_child() | 初始化嵌入对象、添加 child,再释放临时初始引用;成功后由父节点持有 |
| link 属性 | 建立对象之间的关系;是否持有强引用取决于 OBJ_PROP_LINK_STRONG 等语义,不能仅按指针存在判断所有权 |
最后一次 object_unref() | 清理实例属性,按子类到父类执行 instance_finalize;独立分配的对象再调用 free 释放存储 |
引用计数负责生命周期,不会自动回收任意强引用环,也不会同步设备的普通状态访问。嵌入对象的存储跟随外层结构,不能保留一个指向其内部的引用却任由外层存储先释放。设备框架在拆除总线、清理 child 与 link 时必须共同维持这些约束。子对象引用交接、最终清理、child 释放
成功运行过的设备在 unrealize 时标记 pending_deleted_event,device_finalize() 再发出 DEVICE_DELETED。管理端发出删除命令、客户机接受拔除、设备停用和对象最终消失可能相隔一段时间;PCIe 的客户机协作过程见PCIe 设备热插拔。停用、删除事件
八、从 qdev 到统一对象模型
qdev 已经为设备配置提供了公共框架,但类型、属性和所有权问题同样存在于机器、内存后端等非设备对象。2012 年初合入的 Object 基类 引入通用对象基础;同年 属性从 qdev 下沉到 Object,让非设备对象也能使用属性与对象路径。设备管理继续由 qdev 负责,QOM 承担更底层的公共机制。
2013 年的 realized 属性改造 为设备实现引入明确的状态转换,最初通过兼容回调包装旧的 init 方法,再逐步迁移设备。把对象构造、用户配置与可失败的设备实现分开,便于查询属性、按配置组合设备,也让热插拔与错误处理有共同入口。
统一框架减少了重复的注册和管理代码,也增加了间接层:一个方法名可能对应多层类回调,一个对象路径未必就是它的总线拓扑,属性也可能是别名或带副作用的 setter。分析设备创建时,先确认具体类型和父类型,再区分实例初始化、属性写入与 realize,最后检查失败分支的资源归属,通常比单纯沿函数名跳转更容易找到完整路径。