KVM 内存与设备虚拟化

本页目录

guest 地址缺页、第二阶段翻译失败、设备 DMA 越界,发生在不同控制路径。先分清CPU 地址转换与设备访问授权⁠,再看共享队列和迁移。

查阅目标阅读位置
地址、memslot 与故障前三节
队列所有权与后端virtio、vhost
设备授权与迁移条件VFIO、直通迁移

两阶段地址转换

客户机地址与宿主地址:两阶段转换和用户空间映射 GVA guest 虚拟地址 GPA guest 物理地址 HPA host 物理地址 guest 页表 EPT / NPT HVA VMM 的用户虚拟地址 memslot 对应关系 HVA 用于宿主软件找到 backing;硬件执行 guest 访存时不会按此图先查询 QEMU 页表。

guest OS 管理 GVA 到 GPA 的页表;x86 EPT/NPT 再把 GPA 映射到 HPA,并施加宿主控制的权限。两阶段权限共同决定访问是否允许。VMM 的 HVA 是宿主软件管理 backing 的视角,不是 guest 可直接使用的地址。

经典用户内存接口用 memslot 注册一段 GPA 与 VMM 用户空间内存的对应关系。注册不等于一次性为全部范围锁定物理页;宿主缺页、回收与映射失效仍需 KVM 协调。私有 guest memory 等新机制又有不同接口,不能把所有 VM 内存都假定为普通可读写 HVA。内存接口

memslot 的偏移计算不会给出物理页号

设普通用户内存 slot 覆盖 GPA [0x10000000, 0x10003000),对应的 HVA 起点是 0x70000000。GPA 0x10001234 落在 slot 内,偏移为 0x1234,所以对应 HVA 为 0x70001234。这个加法只给出 VMM 地址空间中的位置,无法推出 HPA;宿主页表及 backing 状态决定它当前由哪一物理页承载。

地址起点在 slot 内,也不保证整次访问都在里面。例如从 0x10002ffc 读取 8 字节,后 4 字节越过 slot 的排他上界。以下模型要求一整段非空访问位于单个 slot,不负责拆分跨 slot 请求。

GPA_BASE, HVA_BASE, SIZE = 0x10000000, 0x70000000, 0x3000

def slot_hva(gpa, length):
    if not 0 <= gpa < 2**64 or not 0 < length <= 2**64:
        raise ValueError("非法地址或长度")
    offset = gpa - GPA_BASE
    if not 0 <= offset < SIZE or length > SIZE - offset:
        raise ValueError("整段访问必须位于本 slot")
    return HVA_BASE + offset

assert slot_hva(0x10001234, 8) == 0x70001234
assert slot_hva(0x10002ffc, 4) == 0x70002ffc
for gpa, length in [(0x10002ffc, 8), (0x10003000, 1),
                    (GPA_BASE - 1, 1), (GPA_BASE, 0), (2**64 - 1, 8)]:
    try:
        slot_hva(gpa, length)
    except ValueError:
        pass
    else:
        raise AssertionError("应拒绝越界或非法访问")

在固定宽度语言中,用“长度是否超过剩余空间”做检查,还可避免先计算 gpa + length 时整数回绕。对实际设备后端,找不到 RAM slot 后应按机器地址图判断是否属于某个 MMIO 设备或无效访问,不能默认任何空洞都是一个可读写设备。

EPT violation 不等于 guest 修改了页表

guest 更新自己的 GVA 页表,通常不要求重新建立所有 GPA 到 HPA 映射。EPT violation 反映的是第二阶段转换不存在或权限不满足,可能对应按需建立映射、写保护跟踪,或访问被当作 MMIO 的区域等。

guest 自身页表产生的 page fault 与第二阶段问题也不同:前者可能需要交给 guest 内核处理。诊断时必须辨别故障发生在哪一层,而不是把所有 fault 都归为 guest 改页表。

硬件嵌套分页减少了软件影子页表的维护负担,但 KVM MMU 仍有复杂的映射和失效管理,嵌套虚拟化也会带来额外组合。不能把 shadow MMU 仅理解为彻底不再存在的旧 CPU 遗产。KVM MMU

分别改变 guest PTE 与 EPT 权限,让一次访存停在第一或第二阶段。图中保留相同页内偏移,但失败之后的地址不会作为已完成的翻译结果显示。

正在呈现知识画面
guest 允许写,为什么第二阶段仍能拒绝?

GVA→GPA 与 GPA→HPA 是不同映射,权限共同生效。EPT violation 不证明 guest 页表刚被修改,也不等于必须向 guest 注入同一种缺页。

页表遍历成本为什么不能固定成 24 步

四级 guest 页表叠加四级嵌套页表,在理想化的无缓存、无大页条件下,可以推导出多次访存:获取每个 guest 页表项本身也需要第二阶段转换。实际硬件有 TLB、页表遍历缓存、大页和不同层数,并不是每次 load 都重走完整路径。

大页可能降低翻译压力,但 guest 与 host 两层的大页、对齐和连续性都需要考虑。脏页跟踪、共享或失效还可能拆分映射;不能单看 guest 里 HugePages 就断言第二阶段也用了大页。

展开一次完全未命中的四级遍历

假设 guest 页表走 g 级,访问每个 guest 页表项所在的 GPA 都要完整走 h 级第二阶段页表;所有相关缓存均未命中,没有大页提前结束,也忽略访问位/脏位更新等额外事务。

每取得一个 guest 页表项,需要 h 次第二阶段页表项读取,再读取 guest 页表项本身,合计 h+1 次。这样的项有 g 个。得到最终 GPA 后,还需 h 次第二阶段页表项读取,最后 1 次才读取目标数据:

  • 翻译相关读取:g × (h+1) + h。
  • 加上最终数据读取:(g+1) × (h+1)。
gh翻译相关读取包含最终数据读取
442425
431920
341920
331516
def uncached_walk(g, h):
    assert g >= 1 and h >= 1
    translation = sum(h + 1 for _ in range(g)) + h
    return translation, translation + 1

assert uncached_walk(4, 4) == (24, 25)
assert uncached_walk(4, 3) == uncached_walk(3, 4) == (19, 20)
assert uncached_walk(3, 3) == (15, 16)

这解释了“24”通常在数什么,也解释了为什么它不能被写成每次访存的固定成本:TLB 命中时不会发生上述完整遍历;页表项所在页和最终数据页也未必具有相同的第二阶段叶子层级。表格把它们统一设为 h,只是在比较假设变化,并不表示给 guest 配一页大页就必然把全部 h 都减 1。

virtio 定义设备协议,不限定后端在何处

virtio driver 与设备通过协商的能力和 virtqueue 交换缓冲描述符。PCI、MMIO 等是传输方式,virtio-net、virtio-blk 等是设备类型;后端可以是 QEMU、内核 vhost、用户空间服务或兼容硬件。Virtio

split virtqueue 把描述符表、available 与 used 区域分开;packed virtqueue 使用另一种发布与回收形式。不能用 split 的字段流程直接描述所有 packed 实现。

split virtqueue 阶段所有权与可见性要求
驱动准备先填描述符和数据
发布 available按要求排序后交给设备
设备处理并发布 used让完成状态按协议可见
驱动回收确认完成后才复用缓冲

屏障与发布顺序维持这个协议,不只是性能优化。

kick 和中断是通知,不是一条请求严格对应一次:可以抑制、合并,驱动也可轮询。缓冲在交给设备后,不能在设备仍使用时被随意修改或释放;共享内存不意味着没有所有权协议,也不意味着零拷贝。Virtio 规范

两个请求先后发布,却可以反序回收

以未协商 VIRTIO_F_IN_ORDER 的 split queue 为例,驱动先发布描述符链头 0,再发布链头 1,设备可以先完成 1、后完成 0。驱动必须根据 used 中的链头 ID 找回请求,不能把“第一个完成项”硬配给“第一个提交请求”。协商了 IN_ORDER 时应遵循相应规则,不能沿用这个反序例子。

完成顺序可以改变,链头 ID 仍指向自己的请求 发布顺序 0:A → 1:B 设备完成顺序 1:B → 0:A 驱动按 ID 回收 先回收 B,再回收 A 0 在 A 完成并回收之前,不能拿去承载新请求 C split queue,未协商 IN_ORDER;图中 0、1 是描述符链头 ID,不是 used ring 位置。

下面只建模两块设备只读的请求缓冲区及其生命周期。device 状态包含已经发布、设备尚未开始或仍在处理的时间段;used 表示完成已发布、驱动尚未回收。队列索引回绕、描述符链、屏障和通知机制均由完整实现另行处理。

from collections import deque

class Buffers:
    def __init__(self):
        self.state = {0: "driver", 1: "driver"}
        self.data = {}
        self.used = deque()

    def write(self, ident, payload):
        if self.state[ident] != "driver":
            raise RuntimeError("缓冲尚未回到驱动,不能改写")
        self.data[ident] = bytes(payload)

    def publish(self, ident):
        assert self.state[ident] == "driver" and ident in self.data
        self.state[ident] = "device"

    def complete(self, ident):
        assert self.state[ident] == "device"
        self.state[ident] = "used"
        self.used.append(ident)

    def reclaim(self):
        ident = self.used.popleft()
        assert self.state[ident] == "used"
        self.state[ident] = "driver"
        return ident, self.data[ident]

queue = Buffers()
queue.write(0, b"request A"); queue.publish(0)
queue.write(1, b"request B"); queue.publish(1)
try:
    queue.write(0, b"overwritten")
except RuntimeError:
    pass
else:
    raise AssertionError("不能提前复用")
queue.complete(1)
assert queue.reclaim() == (1, b"request B")
assert queue.state[0] == "device"
queue.complete(0)
assert queue.reclaim() == (0, b"request A")
queue.write(0, b"request C")
queue.publish(0)
assert queue.state == {0: "device", 1: "driver"}

“没有收到中断”不能用来断言还没完成,“收到一个中断”也不能用来断言只完成一个请求。回收依据是按协议可见的 used 信息;中断和 kick 只促使另一端去检查队列。内存顺序的要求则保证接收端看到索引推进时,描述符、数据或完成信息已经可见,上面的顺序 Python 调用没有验证真实弱内存排序。Virtio split queue 与 IN_ORDER

vhost 与 vhost-user 的边界

内核 vhost-net 等后端把部分队列数据处理放到内核,减少相应请求进入 QEMU 设备模型的往返。QEMU 仍负责配置和控制,不是进程完全退出虚拟机运行。

vhost-user 把后端放到另一个用户空间进程,通过协议传递队列、内存与通知信息。名字含 vhost 不代表所有后端都在内核。vhost-vsock 处理的是 AF_VSOCK 语义,不能写成“AF_UNIX 跨 VM 边界”。

后端选择会改变复制、调度、内存共享及故障隔离路径。性能需要对实际队列数、包或块大小、CPU 放置和负载测量,不能承诺统一减少 50% 延迟。

VFIO:分配设备访问权与 DMA 隔离

VFIO 暴露受控设备访问能力,可用于直通,也用于用户空间驱动。CPU 的 EPT/NPT 不会替设备自动限制 DMA;IOMMU 映射、设备隔离拓扑与中断重映射等需要各自配置。VFIO

传统接口中的 IOMMU group 表达可隔离的设备集合,往往不等于一块 PCI function。解绑设备前必须理解同组设备、复位范围和宿主依赖;理论教程无需用一行 unbind 假装完成整个生命周期配置。

新接口可能使用 IOMMUFD 等方式管理设备地址空间,但不能因此忽略底层硬件隔离限制。DMA 只能访问被授权范围,是正确配置后的目标,不是“绑定 vfio-pci”一项操作自动完成的所有保证。

直通迁移不是绝对不可能

迁移前需要设备提供哪些恢复材料

迁移需要保存和恢复设备状态、处理进行中的 DMA,并正确记录设备对内存的修改。没有这些能力的设备不能透明迁移;具备 VFIO migration 支持并满足 QEMU 与目标兼容条件的设备则可能支持相应迁移流程。VFIO 迁移

因此应写成能力与兼容性问题,而不是“所有直通设备无法在线迁移”。整台 VM 的内存、CPU 和设备一致性边界见 QEMU 与 KVM 协作。