virtio-mem 的内存添加与回收
一、virtio-mem 的内存块与容量调整
虚拟机运行期间,负载需要的内存会改变。virtio-mem 在预先划定的客户机物理地址范围内,按小块提供和收回内存。管理程序设置目标容量,客户机驱动决定哪些块可以加入内存管理、哪些块能够安全回收,再通过 virtqueue 与 QEMU 完成交接。
本文沿 x86 Linux 客户机的扩容和缩容过程,分析容量字段、设备协议与页面回收。重点采用 Sub Block Mode(SBM);较大设备块的 Big Block Mode(BBM)另作对照。
以可调设备当前提供 4 GiB 为例:目标改为 6 GiB,驱动需要先请求新块,再把它们加入分配器;目标改为 2 GiB,则先迁走待回收区域中的页面,再请求 QEMU 撤回内存。两条路径都由客户机确认可安全交接后推进,目标字段因此可以先改变,实际容量随后才逐步靠近它。
源码版本:QEMU 10.2.0、Linux 6.18。
二、目标容量、实际容量与可用范围
一个 virtio-mem 设备关联一个 memory backend,并管理从 addr 开始的一段 GPA。后端大小决定容量上限;实际向客户机提供多少内存,由块状态决定。初始内存与这个设备的容量分别计数。
| 字段 | 含义 | 更新方 |
|---|---|---|
region_size | 后端覆盖的最大范围,QMP 中为 max-size | 创建设备时确定 |
usable_region_size | 客户机当前可选择 PLUG / UNPLUG 地址的范围 | QEMU 随目标增长而扩展 |
requested_size | 管理程序要求设备提供的目标容量 | 运行时设置 requested-size |
plugged_size | 已成功 PLUG 的总量,QEMU 对象属性为 size | PLUG / UNPLUG 成功后更新 |
| 客户机可分配范围 | 已纳入 Linux 分配器、未被驱动隔离的页 | 客户机添加、上线、隔离与移除操作 |
例如后端容量 8 GiB,目标 4 GiB,实际提供 4 GiB。将目标改为 2 GiB 后,size 可以暂时仍为 4 GiB;客户机逐步回收后才会下降。反向扩容时,PLUG 已获确认的内存,也可能尚未上线。客户机的空闲内存和宿主 QEMU 的 RSS 又是不同指标。
下面的配置片段为客户机保留 4 GiB 初始内存,另建一个上限 8 GiB 的可调区域,目标先设为 4 GiB。两个部分都可用后,客户机获得的内存总量约为 8 GiB,而设备的 size 只计其中可调的 4 GiB:
-m 4G,maxmem=12G \
-object memory-backend-ram,id=vmem0,size=8G,reserve=off \
-device virtio-mem-pci,id=vm0,memdev=vmem0,block-size=2M,requested-size=4G
后端地址空间可以预留到上限,但无需预先填满所有页面。若使用需要预分配的后端,virtio-mem 设备的 prealloc 控制每次 PLUG 的内存准备;直接预分配整个后端会提前承担上限容量的开销。容量调整只作用于这个设备,初始内存不会随 requested-size 一起缩减。
可用范围给驱动留下选择空间。QEMU 10.2 的 x86 实现增加 256 MiB 余量,并受后端上限和块对齐约束。运行时调低目标不会立即缩小这个范围;UNPLUG_ALL 等允许重置范围的路径才会重新计算。它不能解释为已插入容量或当前物理占用。范围调整实现
本例按 2 MiB 设备块交接容量。若 Linux 的内存热插拔管理块为 128 MiB,SBM 可以把其中的 64 个子块分别提供或收回。因此,调整 2 MiB 不必同时撤回整个 128 MiB 区域。先沿这两个粒度看扩缩容,后面再对照块位图和请求格式。
三、扩容:PLUG 后再加入客户机分配器
管理程序通过 QOM 属性提出请求:
{"execute":"qom-set","arguments":{
"path":"/machine/peripheral/vm0",
"property":"requested-size","value":6442450944
}}
这里将名为 vm0 的设备目标设为 6 GiB。QEMU 检查块对齐和容量上限,更新属性,发送配置变化通知。QMP 成功表示属性设置成功;完成扩容需要继续观察 query-memory-devices 的 size 和客户机上线情况。
- 配置变化客户机设置
config_changed并调度工作队列,重读目标和可用范围。 - 请求 PLUG驱动选择未提供的子块。QEMU 检查地址、原状态及容量限制,准备宿主内存并通知相关内存监听者。
- 收到 ACK设备块位图与实际提供量更新;客户机记录成功提供的范围。
- 加入客户机分配器新 Linux 块先添加,再由上线策略上线;已在线块内的新子块通过 fake-online 释放给分配器。
Linux 的 virtio_mem_sbm_plug_and_add_mb() 先 PLUG,再添加管理对象,以便上线时识别哪些子块可用。若添加失败,会尝试 UNPLUG 回滚。长期没有上线的块也受驱动阈值限制,避免只积累管理开销而没有可用内存。添加与局部上线路径
四、缩容:隔离页面后再 UNPLUG
降低目标后,客户机首先要使待回收页退出分配和使用。对于已在线 Linux 块中的子块,virtio_mem_fake_offline() 使用 alloc_contig_range() 隔离指定范围,迁出可迁移页,并由驱动持有这些页。它还通过 adjust_managed_page_count() 减少分配器管理的页数;同一 Linux memory block 的其他子块仍可在线使用。
以一个包含 64 个子块的 128 MiB Linux memory block 为例,回收第 63 个 2 MiB 子块,只需使该子块退出使用。成功后,驱动的子块位图从全 1 变为 63 个 1 和 1 个 0,Linux 块转入部分提供状态,QEMU 的设备容量减少 2 MiB。其余 126 MiB 可以继续服务客户机。最后一个子块也回收后,才有条件清理整个 Linux 块。
隔离成功后才发送 UNPLUG。QEMU 执行 ram_block_discard_range(),通知 RamDiscardManager 监听者,清除块位图,再减少 size 并返回 ACK。启用动态 memslot 时,完全不再含已提供块的 memslot 可以停用。discard 的底层行为取决于匿名内存、文件或大页后端,设备容量下降不等于进程 RSS 按同一数值立即下降。
Mermaid 源码
sequenceDiagram
accTitle: virtio-mem 在线子块的回收交接
accDescr: 管理程序调低目标后,Linux 客户机先隔离子块,再向 QEMU 请求回收;隔离失败时跳过该子块,设备 BUSY 时归还已隔离页,成功时减少设备容量。
participant M as 管理程序
participant Q as QEMU virtio-mem
participant V as 客户机 virtio-mem 驱动
participant L as Linux 内存管理
M->>Q: 降低 requested-size
Q-->>M: 属性更新成功
Q-->>V: 配置变化通知
V->>Q: 读取 requested / plugged / usable
Q-->>V: 当前容量与地址范围
V->>L: alloc_contig_range 隔离在线子块
alt 页暂时不能迁出
L-->>V: -EBUSY,未完成隔离
Note over V,L: 跳过该子块,尝试其他范围并安排重试
else 隔离成功
L-->>V: 页由驱动持有,暂不再分配
V->>Q: UNPLUG(addr, nb_blocks)
alt QEMU 暂不能完成
Q-->>V: BUSY
V->>L: fake-online,归还已隔离页
else 回收成功
Note over Q: discard、通知监听者、清位图、减少 size
Q-->>V: ACK
Note over V,L: 全部子块回收后,再尝试移除 Linux 内存块
end
end
成功回收一个子块不要求先 offline 整个 Linux memory block。全部子块都已回收后,驱动再尝试 offline 并移除这个管理对象,释放对应管理资源;这一步失败时,设备块已经回收的事实仍然成立,清理会重试。BBM 则先隔离、offline 并移除覆盖的 Linux 内存块,再请求 UNPLUG,其状态机与 SBM 不同。SBM 在线子块回收、BBM 回收路径
五、逐步观察扩缩容与失败重试
目标容量、客户机上线与回收
一个容量为 8 GiB 的设备,初始提供 4 GiB。修改目标后逐步处理;点击已上线的区域,可设置或解除客户机回收阻塞。
- 管理端目标
- 4 GiB
- 设备实际提供
- 4 GiB
- 客户机管理容量
- 4 GiB
- 可用地址范围
- 4.25 GiB
客户机管理容量包含已使用与空闲页;它不等同于 Linux memory block 的 online 状态。已交客户机:由分配器管理 · 等待上线:设备已提供 · 已隔离:暂离分配器 · 回收受阻:各子块仍有不可迁移页 · 未提供:客户机不可使用
- 目标从 4 GiB 调到 6 GiB,设备通知客户机;实际提供量暂时不变。
- 客户机请求 PLUG。QEMU 返回 ACK 后,实际提供量增加;客户机完成添加与上线后,才能用于普通分配。
- 目标调低时,客户机先迁出可迁移页并隔离范围,再发送 UNPLUG;QEMU 完成 discard 后确认。
- 不可迁移页会阻止客户机隔离。QEMU 返回 BUSY 时,驱动归还已隔离页并稍后重试。
启用 JavaScript 后可修改目标、设置回收阻塞,并逐步观察容量差异。
每格汇总 512 MiB,相当于 256 个 2 MiB 设备块;它既不是一个真实设备块,也不是一个 Linux memory block。每步汇总一组块的处理。阻塞选项表示该格每个回收子块都有暂不可迁移页;实际驱动会尝试回收其中能够迁出的子块。这里合并展示添加与上线、discard 与应答;隔离期间目标更新时,合并展示取消请求、归还页和重读配置。不计算宿主 RSS、耗时或页面迁移目标。可用地址范围按 x86 的 256 MiB 余量更新;普通缩容不使它缩小。
六、设备块、Linux 内存块与请求格式
设备的 block_size 定义协议的最小操作单位,可以是 2 MiB。Linux 的 memory block 是内存热插拔管理对象,大小应读取 /sys/devices/system/memory/block_size_bytes,不能与设备块混用。
驱动根据设备块与内核 pageblock 等粒度计算 subblock。若它小于 Linux memory block,就采用 SBM:一个 memory block 包含多个 subblock,可处于部分提供状态。例如 Linux 块为 128 MiB、subblock 为 2 MiB 时,一个管理对象包含 64 个可独立处理的子块。更大的设备块采用 BBM,操作范围覆盖一个或多个 Linux memory block。模式与粒度选择
| 结构 | 关键内容 |
|---|---|
QEMU VirtIOMEM | 宿主用户态设备对象;保存 size、requested_size、block_size 与按设备块索引的 bitmap |
Linux struct virtio_mem | 客户机内核驱动对象;保存容量副本、workqueue、virtqueue 与重试定时器;SBM 用 mb_states 记录 Linux 块状态、sb_states 位图记录子块状态 |
virtio_mem_req | 客户机内存中的请求缓冲区,QEMU 读取;含 type、地址 addr、设备块数量 nb_blocks,完整请求为 24 字节 |
virtio_mem_resp | 客户机内存中的 10 字节响应缓冲区,QEMU 写入、驱动读取;type 为 ACK、NACK、BUSY 或 ERROR,STATE 查询还返回块状态 |
驱动向 virtqueue 提交一个设备可读的请求缓冲区和一个设备可写的响应缓冲区。PLUG / UNPLUG 携带 GPA 与块数,不携带内存页内容。addr = A + 4 MiB, nb_blocks = 2 在设备块为 2 MiB 时表示处理从该地址开始的 4 MiB。请求提交与响应处理
七、区分客户机隔离失败与设备 BUSY
不可迁移的内核分配、暂时固定的页或迁移目标页不足,都可能使隔离失败。驱动会跳过暂忙子块,尝试其他范围;部分子块成功时,实际容量可以停在初始值与目标之间。ZONE_MOVABLE 能减少不可迁移分配带来的干扰,但仍需要处理临时引用和内存压力。
设备侧 BUSY 是另一个原因。QEMU 在迁移期间限制块状态变化;预分配失败、discard 失败或监听者无法建立映射,也可能使请求暂不能完成。在线 SBM 子块的 UNPLUG 失败后,驱动会 fake-online,将已经隔离的页归还分配器,再安排重试。QEMU 状态改变、工作队列与重试
NACK 表示 PLUG 超过当前目标,例如目标在处理期间变化;驱动重读配置。ERROR 表示地址、对齐或块状态等请求条件错误,不能与 BUSY 混同。管理程序应同时记录目标、实际提供量及变化时间,再检查客户机内存块状态和驱动日志,避免把一次 QMP 返回当作扩缩容完成。
判断停在哪一层,可以依次看三个现象:目标已变而设备容量未变,检查驱动是否运行、是否持续收到 BUSY;设备容量已增加而客户机可用量未增加,检查内存上线策略;设备容量只降了一部分,则查看余下子块的页面引用和可迁移性。重复设置相同目标能重新触发配置通知,但不能强制释放仍被客户机使用的页面。
宿主侧通过 QMP {"execute":"query-memory-devices"} 找到 id = vm0,对照以字节计的 requested-size、size 和 block-size。客户机内可读取 /sys/devices/system/memory/block_size_bytes,并用 lsmem 查看 Linux 内存块的上线状态。SBM 允许一个 online 块只提供部分子块,所以 lsmem 的块状态还需结合驱动状态与设备容量判断,不能把所有 online 块的标称大小直接相加作为 virtio-mem 已提供量。QMP 容量字段
八、粒度、资源开销与设计演进
DIMM 热插拔以设备为单位管理容量,客户机 balloon 在已有内存中交还页面,virtio-mem 则在设备预留范围内改变已提供的块。三者对客户机支持、容量边界和管理对象的要求不同,适用条件也不同。
2020 年的 QEMU 初始实现已经采用目标容量、块位图与客户机协作。这让较小粒度的持续调整不必反复添加大量 DIMM,但引入了协议驱动和回收状态机,无法绕过客户机页面是否可迁移的限制。
2021 年引入的 RamDiscardManager 接口把块变化通知给相关使用者,为直通设备等映射同步提供机制。失败需要回滚,容量可调同时也增加了 DMA 映射协调要求。
2023 年加入的动态 memslot解决另一项成本:后端上限很大、实际只使用少量内存时,单个巨大 memslot 仍可能带来内核元数据开销。按需映射多个槽可以降低这种浪费,但需要权衡槽位上限、粒度和外部后端支持。设备块、Linux 内存块和 KVM memslot 是三种不同边界,设计和排查时应分别观察。