guest 退出到 KVM,不一定让 KVM_RUN 返回用户空间。沿执行循环要分清硬件退出、内核处理和 VMM 处理。KVM 提供执行接口,QEMU 是常见使用者而非唯一规定。
硬件例子以 x86 VMX/SVM 为主,其他架构使用不同执行级和机制。
| 查阅目标 | 阅读位置 |
| 职责与退出层次 | 前两节 |
| 接口探测与宿主调度 | 文件描述符、vCPU |
| 执行与通知边界 | 运行循环、中断注入 |
内核负责执行边界,VMM 组织整台机器
KVM 使用 Linux 的调度、内存管理等基础设施,并通过架构实现控制硬件虚拟化。用户空间 VMM 通常负责机器配置、固件、设备模型和迁移协调;部分设备数据路径可以进入内核或独立后端。
x86 上的 kvm 模块也包含相应架构代码,kvm_intel/kvm_amd 再对接厂商机制,不能把整个 kvm.ko 简写成纯架构无关核心。目录 virt/kvm 与 arch/x86/kvm 可以帮助区分源码责任。
两种“退出”需要分开
VMX 的 root/non-root 与 guest 内部 CPL0/CPL3 是不同维度:guest 内核仍可在自己的特权级执行。普通指令通常直接由 CPU 执行,硬件控制指定哪些事件或操作需要退出。
硬件 VM exit 首先回到 KVM。若 KVM 能处理,例如某些页映射或中断状态更新,可以直接再次进入 guest;只有需要用户空间处理等情况,KVM_RUN 才返回。一个 KVM_RUN 期间可能发生多次硬件进入与退出。
因此统计 ioctl 次数不能代替统计硬件 VM exit。exit 也不全由“敏感指令”触发,外部事件和嵌套分页问题同样可能触发。
独立选择一次 guest 运算、CPUID、MMIO 或宿主信号,分别统计硬件退出与 KVM_RUN 返回。两条边界可以只跨一条,不能用同一个“exit 次数”混合计数。
正在呈现知识画面
一次硬件退出,会不会让 KVM_RUN 返回?硬件退出先进入 KVM;内核能处理时可直接重入 guest。KVM_RUN 成功返回才分派 exit_reason,错误返回先处理 errno。
{"id":"kvm-exit-boundaries","title":"一次硬件退出,会不会让 KVM_RUN 返回?","summary":"硬件退出先进入 KVM;内核能处理时可直接重入 guest。KVM_RUN 成功返回才分派 exit_reason,错误返回先处理 errno。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"scenario\"><button data-mode=\"arithmetic\" data-i18n=\"arithmetic\"></button><button data-mode=\"cpuid\" data-i18n=\"cpuid\"></button><button data-mode=\"mmio\" data-i18n=\"mmio\"></button><button data-mode=\"signal\" data-i18n=\"signal\"></button></div><label class=\"check\"><input type=\"checkbox\" id=\"handled\"><span data-i18n=\"handled\"></span></label><div class=\"track-row\"><span data-i18n=\"route\"></span><div id=\"route\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"hardware\"></span><strong id=\"hardware\"></strong></div><div><span data-i18n=\"returns\"></span><strong id=\"returns\"></strong></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=\"running\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"userspace\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"error\"></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#route{display:grid;grid-template-columns:1fr}#route .cell{min-height:65px}","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='arithmetic';function draw(){const inKernel=$('#handled').checked,hw=mode==='cpuid'||mode==='mmio',returned=mode==='signal'||(mode==='mmio'&&!inKernel);cells('route',[{text:mode==='signal'?'guest —':'guest → '+(hw?'KVM':'guest'),cls:mode==='signal'?'empty':'on'},{text:hw?'KVM → '+(returned?'VMM':'guest'):mode==='signal'?'KVM → VMM':'KVM —',cls:hw||returned?'gold':'empty'},{text:returned?'VMM ●':'VMM —',cls:returned?'on':'empty'}]);set('hardware',Number(hw));set('returns',Number(returned));$('#handled').disabled=mode!=='mmio';set('explanation',t(mode==='signal'?'error':returned?'userspace':'running'));pressed(mode);}document.querySelectorAll('[data-mode]').forEach(e=>e.onclick=()=>{mode=e.dataset.mode;draw();});$('#handled').onchange=draw;draw();","audio":false,"strings":{"arithmetic":"普通算术","cpuid":"CPUID","error":"ret=-1; errno=EINTR。先处理错误,不把共享区上次残留的 exit_reason 当成新设备请求。","handled":"MMIO 有内核 handler","hardware":"本例硬件 VM exit 次数","heading":"一次硬件退出,会不会让 KVM_RUN 返回?","intro":"选择一次执行情景,比较 guest、KVM 和 VMM 之间实际跨过的边界。MMIO 可以切换是否由内核处理。","mmio":"设备 MMIO","reset":"重置","result":"VMM 可读取的返回信息","returns":"本例 KVM_RUN 用户态返回次数","route":"本情景的边界路径","running":"KVM_RUN 尚未返回;继续 guest 执行。","scenario":"选择执行情景","scope":"每个选项是一段独立执行快照,不累计操作次数。x86 普通算术不截获,CPUID 配置为截获并由内核处理;MMIO handler 是否存在可选。EINTR 情景在进入 guest 前观察信号,因此没有本例硬件退出。不是所有 KVM 配置的固定路径。","scopeLabel":"模型范围与约定","signal":"进入前的信号","userspace":"ret=0; exit_reason=KVM_EXIT_MMIO。VMM 处理后还需再次进入 KVM_RUN 完成相应操作。"}}
文件描述符分层
| 句柄 | 获得方式 | 主要用途 |
| KVM system fd | open /dev/kvm | 查询 API、能力,创建 VM |
| VM fd | KVM_CREATE_VM | 注册内存、创建 vCPU 与设备 |
| vCPU fd | KVM_CREATE_VCPU | 配置 CPU 状态,KVM_RUN |
| 设备 fd | 相应设备创建接口 | 配置具体的内核虚拟设备 |
不同 ioctl 必须用于正确类别的 fd。接口能力通过 KVM_CHECK_EXTENSION 查询,不能只看内核版本号;部分扩展还需要显式启用。VM 生命周期与其引用关联,关闭一个重复 fd 不必然立刻销毁整台 VM。KVM API
下面只探测接口,不创建 VM:
#include <fcntl.h>
#include <linux/kvm.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <unistd.h>
int main(void) {
int fd = open("/dev/kvm", O_RDWR | O_CLOEXEC);
if (fd == -1) { perror("open /dev/kvm"); return 1; }
int version = ioctl(fd, KVM_GET_API_VERSION, 0);
if (version < 0) { perror("KVM_GET_API_VERSION"); close(fd); return 1; }
if (version != KVM_API_VERSION) {
fprintf(stderr, "unsupported KVM API: %d\n", version);
close(fd); return 1;
}
int memory = ioctl(fd, KVM_CHECK_EXTENSION, KVM_CAP_USER_MEMORY);
if (memory < 0) { perror("KVM_CHECK_EXTENSION"); close(fd); return 1; }
int bytes = ioctl(fd, KVM_GET_VCPU_MMAP_SIZE, 0);
if (bytes < 0) { perror("KVM_GET_VCPU_MMAP_SIZE"); close(fd); return 1; }
printf("API=%d user_memory=%d vcpu_mmap=%d\n", version, memory, bytes);
close(fd);
return 0;
}
保存为 kvm_probe.c,用 cc -D_GNU_SOURCE -Wall -Wextra kvm_probe.c -o kvm_probe 编译。/dev/kvm 不存在或无权限与接口能力不支持是不同问题,诊断时应保留具体错误。
vCPU 是被宿主调度的执行上下文
QEMU 的常见 KVM 模式为每个 vCPU 配一个宿主线程。vCPU 数量不是物理核数量,线程可能被抢占、迁移到不同 CPU,或等待 I/O/中断;CPU pinning 和 NUMA 放置会影响结果。
KVM 使用调度通知等机制在宿主切换时维护架构执行状态。steal time、虚拟时钟、TSC offset/scaling 各有具体接口,不能把每次线程切换描述成“重新计算全部时间偏移”。
HLT 表示 guest 暂时没有工作等待唤醒,是否阻塞在内核或返回用户空间取决于配置;它不等于用来限制宿主功耗的 idle injection。
运行循环要处理错误与完成协议
| KVM_RUN 返回值 | 首先处理什么 |
| -1 | 按 errno 处理信号中断等错误 |
| 0 | 再按本次 exit_reason 分派 |
错误返回时,不能盲读可能还属于上次退出的共享 exit_reason。
返回 0 后,用户空间按 KVM_EXIT_IO、KVM_EXIT_MMIO 等退出原因处理。部分 I/O 操作需要再次进入 KVM_RUN 才在内核侧完成;准备保存状态或迁移时,必须考虑这些尚未完成的操作。
一个输入指令,需要跨过哪些完成边界
假设 guest 执行“从端口读取一个字节,再把结果加 1”。设备模型在用户空间,第一次 KVM_RUN 因 KVM_EXIT_IO 返回后,VMM 找到该端口的虚拟设备,把值 7 填入共享运行区。但“缓冲区已填入 7”与“guest 输入指令已经提交结果”仍是两个状态。
| 阶段 | VMM 已完成什么 | 还不能据此断言什么 |
| 收到 I/O 退出 | 知道方向、端口、宽度、数量 | guest 已经取得设备返回值 |
| 填好输入缓冲区 | 设备模型给出了值 7 | 寄存器和指令进度已形成可迁移的完整状态 |
| 再次调用 KVM_RUN | 内核完成挂起的操作 | 后续 guest 指令一定已经执行 |
| 允许继续执行 | guest 可使用结果完成加法 | 仅凭这个 vCPU 的状态就能迁移整台机器 |
KVM API 明确要求这类操作再次进入内核完成。需要停在完成之后、下一条 guest 指令之前时,可按能力支持使用 immediate_exit 或待处理的未屏蔽信号;完成挂起操作发生在后续信号检查之前。immediate_exit 应通过 KVM_CAP_IMMEDIATE_EXIT 查询支持。迁移还需要协调其他 vCPU、设备与内存,这里只处理一项容易遗漏的局部边界。KVM 运行区与 I/O 完成约定
下面是三个抽象指令 IN → ADD → HALT 的状态机。pc 是指令序号,不是 x86 字节地址;它用于展示完成顺序,不模拟真正的指令解码、硬件退出或 KVM ioctl。
import errno
class GuestModel:
def __init__(self):
self.pc = 0
self.acc = 0
self.waiting_input = False
self.input_byte = None
self.exit_reason = None
def run(self, immediate_exit=False):
if self.waiting_input:
if self.input_byte is None or not 0 <= self.input_byte <= 255:
raise ValueError("必须先提供一个输入字节")
self.acc = self.input_byte
self.pc += 1
self.waiting_input = False
self.input_byte = None
if immediate_exit:
return -1, errno.EINTR while True:
if self.pc == 0:
self.waiting_input = True
self.exit_reason = "IO_IN"
return 0, 0
if self.pc == 1:
self.acc = (self.acc + 1) & 255
self.pc += 1
else:
self.exit_reason = "HALT"
return 0, 0
for value in (0, 7, 255):
guest = GuestModel()
assert guest.run() == (0, 0) and guest.exit_reason == "IO_IN"
guest.input_byte = value
assert (guest.pc, guest.acc, guest.waiting_input) == (0, 0, True)
assert guest.run(immediate_exit=True) == (-1, errno.EINTR)
assert (guest.pc, guest.acc, guest.waiting_input) == (1, value, False)
assert guest.exit_reason == "IO_IN" assert guest.run() == (0, 0) and guest.exit_reason == "HALT"
assert (guest.pc, guest.acc) == (2, (value + 1) & 255)
值 7 的轨迹在完成输入后停于 pc=1, acc=7,继续运行才得到 8。若在缓冲区写完、尚未再次进入内核时就保存寄存器,挂起操作的隐藏状态没有被普通可见状态完整表达,恢复端可能无法继续同一个操作。
模型中的第二次调用还说明一个细节:EINTR 不表示此前的一切工作都没有发生。这一轮可能已经完成了前次挂起操作;但它也没有提供一个新的成功退出,不能再根据遗留的 IO_IN 字段调用设备模型一次。普通运行循环应先检查 ioctl 返回值,再决定读取哪些退出字段。
I/O 数据按偏移和数量解释
KVM_EXIT_IO 的 data_offset 相对于整个 struct kvm_run 映射起点,数据是 count 个、每个 size 字节的紧凑数组。例如 size=2、count=3 意味着 6 个字节,不是只处理第一个 16 位值。OUT 时 VMM 读取这段数据,IN 时 VMM 把设备返回值填回同一段区域。
偏移不是 guest 虚拟地址,也不是 guest 物理地址;直接把它拿去查询 guest 页表会走错地址空间。MMIO 退出使用另一组字段,包括物理地址、长度和 data,不能与端口 I/O 的布局混用。处理共享区域期间也应保证同一 vCPU 的运行与设备处理按预定线程协议串行化,避免一边重入 KVM_RUN、一边改写上一轮的数据。
共享运行区不是完整寄存器镜像
KVM_GET_VCPU_MMAP_SIZE 给出 vCPU 共享映射大小,不能自己假定一页。struct kvm_run 用于执行控制和退出数据,不是通用的完整寄存器镜像;寄存器通常通过相应 ioctl,或在明确支持并启用的同步机制下处理。
中断注入取决于 guest 是否可接收
设备完成、虚拟定时器和 vCPU 间 IPI 都可产生虚拟中断。设置待处理状态不保证下一条 guest 指令前立即交付,还需符合 guest 中断屏蔽、优先级与硬件注入条件。kick 用于促使 vCPU 处理新工作,不等于无条件立即运行其处理程序。
内核 irqchip、irqfd 与硬件中断虚拟化可减少部分用户空间往返,但路径与架构能力有关。地址转换见 内存与设备虚拟化,线程与迁移协作见 QEMU 与 KVM。