guest 地址缺页、第二阶段翻译失败、设备 DMA 越界,发生在不同控制路径。先分清CPU 地址转换与设备访问授权,再看共享队列和迁移。
| 查阅目标 | 阅读位置 |
| 地址、memslot 与故障 | 前三节 |
| 队列所有权与后端 | virtio、vhost |
| 设备授权与迁移条件 | VFIO、直通迁移 |
两阶段地址转换
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 注入同一种缺页。
{"id":"guest-and-host-translation","title":"guest 允许写,为什么第二阶段仍能拒绝?","summary":"GVA→GPA 与 GPA→HPA 是不同映射,权限共同生效。EPT violation 不证明 guest 页表刚被修改,也不等于必须向 guest 注入同一种缺页。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"operation\"><button data-mode=\"read\" data-i18n=\"read\"></button><button data-mode=\"write\" data-i18n=\"write\"></button></div><div class=\"two\"><div class=\"card\"><header data-i18n=\"guest\"></header><label class=\"check\"><input type=\"checkbox\" id=\"gp\" checked><span data-i18n=\"gp\"></span></label><label class=\"check\"><input type=\"checkbox\" id=\"gw\" checked><span data-i18n=\"gw\"></span></label></div><div class=\"card\"><header data-i18n=\"host\"></header><label class=\"check\"><input type=\"checkbox\" id=\"ep\" checked><span data-i18n=\"ep\"></span></label><label class=\"check\"><input type=\"checkbox\" id=\"ew\"><span data-i18n=\"ew\"></span></label></div></div><div class=\"track-row\"><span data-i18n=\"translation\"></span><div id=\"translation\" class=\"track\"></div></div><div class=\"card\"><header data-i18n=\"result\"></header><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"guestFault\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"eptFault\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"ok\"></p></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.two{grid-template-columns:1fr}.check{overflow-wrap:anywhere}#translation{display:grid;grid-template-columns:1fr}#translation .cell{min-height:52px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let mode='write';function draw(){const write=mode==='write',g=$('#gp').checked&&(!write||$('#gw').checked),e=$('#ep').checked&&(!write||$('#ew').checked);cells('translation',[{text:'GVA 0x1234',cls:'on'},{text:g?'GPA 0x4234':'GPA —',cls:g?'on':'empty'},{text:g&&e?'HPA 0x9234':'HPA —',cls:g&&e?'gold':'empty'}]);set('explanation',t(!g?'guestFault':!e?'eptFault':'ok'));pressed(mode);}document.querySelectorAll('[data-mode]').forEach(e=>e.onclick=()=>{mode=e.dataset.mode;draw();});document.querySelectorAll('input').forEach(e=>e.onchange=draw);draw();","audio":false,"strings":{"ep":"EPT present","eptFault":"EPT violation:guest 已给出 GPA,但宿主第二阶段尚不允许。","ew":"EPT writable","gp":"guest PTE present","guest":"第一阶段:guest 控制","guestFault":"guest page fault:第一阶段不满足本次访问。","gw":"guest PTE writable","heading":"guest 允许写,为什么第二阶段仍能拒绝?","host":"第二阶段:host 控制","intro":"分别设置 guest PTE 与 EPT 的存在和写权限,再选择读或写。故障停在哪一层,后面的地址就不能作为已完成翻译使用。","ok":"两阶段条件都满足,保留同一页内偏移 0x234,得到 HPA。","operation":"访问类型","read":"读","reset":"重置","result":"失败层次或最终结果","scope":"固定 4 KiB 页,GVA=0x1234 → GPA=0x4234 → HPA=0x9234。guest 页表本身已被宿主后备并可访问,故只检查最终数据映射。省略 TLB、访问/脏位、execute 权限、页表层数和 MMIO;不计算固定遍历访存次数。","scopeLabel":"模型范围与约定","translation":"地址转换结果","write":"写"}}
页表遍历成本为什么不能固定成 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)。
| g | h | 翻译相关读取 | 包含最终数据读取 |
| 4 | 4 | 24 | 25 |
| 4 | 3 | 19 | 20 |
| 3 | 4 | 19 | 20 |
| 3 | 3 | 15 | 16 |
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 时应遵循相应规则,不能沿用这个反序例子。
下面只建模两块设备只读的请求缓冲区及其生命周期。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 协作。