When a guest exits to KVM, it does not necessarily cause KVM_RUN to return to user space. Within the execution loop, it is crucial to distinguish between hardware exits, kernel processing, and VMM processing. KVM provides the execution interface; QEMU is a common consumer, not the only prescribed one.
Hardware examples focus on x86 VMX/SVM, while other architectures use different execution levels and mechanisms.
Target of Inquiry
Reading Location
Responsibilities and Exit Hierarchy
First two sections
Interface Detection and Host Scheduling
File descriptors, vCPU
Execution and Notification Boundaries
Run loop, Interrupt injection
The Kernel Handles Execution Boundaries; The VMM Organizes the Entire Machine
KVM utilizes Linux infrastructure such as scheduling and memory management, and implements hardware virtualization control through architecture-specific implementations. User-space VMMs typically handle machine configuration, firmware, device models, and migration coordination; some device data paths can enter the kernel or operate as independent backends.
The kvm module on x86 also contains corresponding architecture-specific code. kvm_intel/kvm_amd then interface with vendor-specific mechanisms; one cannot simplify the entire kvm.ko into a purely architecture-agnostic core. The directories virt/kvm and arch/x86/kvm can help distinguish source code responsibilities.
Two Types of "Exits" Need to Be Separated
VMX root/non-root and guest internal CPL0/CPL3 are different dimensions: the guest kernel can still execute at its own privilege level. Ordinary instructions are usually executed directly by the CPU; hardware control specifies which events or operations require an exit.
Hardware VM exits first return to KVM. If KVM can handle it, such as certain page mappings or interrupt status updates, it can directly re-enter the guest; only in cases requiring user-space processing does KVM_RUN return. Multiple hardware entries and exits can occur during a single KVM_RUN.
Therefore, counting ioctl calls cannot replace counting hardware VM exits. Exits are not all triggered by "sensitive instructions"; external events and nested paging issues can also trigger them.
Independently select one guest operation, CPUID, MMIO, or host signal, and count hardware exits and KVM_RUN returns separately. The two boundaries can be crossed individually; do not mix counts using a single "exit count."
Preparing the visual
Does a hardware exit cause KVM_RUN to return?
Hardware exits enter KVM first; if the kernel can handle it, it can directly re-enter the guest. KVM_RUN only dispatches exit_reason upon successful return; error returns handle errno first.
{"id":"kvm-exit-boundaries","title":"Does a hardware exit cause KVM_RUN to return?","summary":"Hardware exits enter KVM first; if the kernel can handle it, it can directly re-enter the guest. KVM_RUN only dispatches exit_reason upon successful return; error returns handle errno first.","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":"Ordinary Arithmetic","cpuid":"CPUID","error":"ret=-1; errno=EINTR. Handle the error first; do not treat the residual exit_reason from the previous shared region as a new device request.","handled":"MMIO has kernel handler","hardware":"Number of Hardware VM Exits in This Example","heading":"Does a hardware exit cause KVM_RUN to return?","intro":"Select an execution scenario and compare the actual boundaries crossed between guest, KVM, and VMM. MMIO can be toggled to be handled by the kernel.","mmio":"Device MMIO","reset":"Reset","result":"Return Information Readable by VMM","returns":"Number of User-space KVM_RUN Returns in This Example","route":"Boundary Path for This Scenario","running":"KVM_RUN has not returned yet; continuing guest execution.","scenario":"Select Execution Scenario","scope":"Each option is an independent execution snapshot; operation counts are not cumulative. x86 ordinary arithmetic is not intercepted; CPUID is configured to be intercepted and handled by the kernel; MMIO handler existence is optional. The EINTR scenario observes signals before entering the guest, so there is no hardware exit in this example. Not all KVM configurations follow fixed paths.","scopeLabel":"Model Scope and Conventions","signal":"Signal Before Entry","userspace":"ret=0; exit_reason=KVM_EXIT_MMIO. After VMM processing, KVM_RUN must be entered again to complete the corresponding operation."}}
File Descriptor Hierarchy
Handle
Acquisition Method
Primary Purpose
KVM system fd
open /dev/kvm
Query API, capabilities, create VM
VM fd
KVM_CREATE_VM
Register memory, create vCPUs and devices
vCPU fd
KVM_CREATE_VCPU
Configure CPU state, KVM_RUN
Device fd
Corresponding device creation interface
Configure specific kernel virtual devices
Different ioctls must be used with the correct category of fd. Interface capabilities are queried via KVM_CHECK_EXTENSION; do not rely solely on the kernel version number; some extensions also require explicit enabling. VM lifecycle is tied to its references; closing a duplicate fd does not necessarily destroy the entire VM immediately. KVM API
Below only probes interfaces without creating a VM:
Save as kvm_probe.c and compile with cc -D_GNU_SOURCE -Wall -Wextra kvm_probe.c -o kvm_probe. The absence of /dev/kvm or lack of permissions is a different issue from interface capability support; retain specific errors during diagnosis.
vCPU is an Execution Context Scheduled by the Host
QEMU's common KVM mode assigns one host thread per vCPU. The number of vCPUs is not the number of physical cores; threads may be preempted, migrated to different CPUs, or waiting for I/O/interrupts; CPU pinning and NUMA placement affect results.
KVM uses scheduling notifications and other mechanisms to maintain architecture execution state during host switches. Steal time, virtual clocks, and TSC offset/scaling have specific interfaces; do not describe every thread switch as "recalculating all time offsets."
HLT indicates the guest has no work to do and is waiting for a wake-up; whether it blocks in the kernel or returns to user space depends on configuration; it is not equivalent to idle injection used to limit host power consumption.
The Run Loop Must Handle Errors and Completion Protocols
KVM_RUN Return Value
Handle What First
-1
Handle errors such as signal interrupts according to errno
0
Then dispatch based on the current exit_reason
On error returns, do not blindly read the shared exit_reason which may still belong to the previous exit.
After returning 0, the user space handles exit reasons such as KVM_EXIT_IO, KVM_EXIT_MMIO, etc. Some I/O operations require re-entering KVM_RUN to complete on the kernel side; when preparing to save state or migrate, you must consider these incomplete operations.
Which Completion Boundaries Must Be Crossed for One Input Instruction
Assume the guest executes "read one byte from a port, then add 1 to the result." The device model is in user space. After the first KVM_RUN returns due to KVM_EXIT_IO, the VMM finds the virtual device for that port and fills the value 7 into the shared run area. However, "buffer filled with 7" and "guest input instruction has submitted the result" are still two distinct states.
Stage
What VMM Has Completed
What Cannot Be Asserted Yet
Received I/O Exit
Knows direction, port, width, count
Guest has obtained the device return value
Filled Input Buffer
Device model provided value 7
Registers and instruction progress have formed a complete, migratable state
Call KVM_RUN Again
Kernel completes pending operations
Subsequent guest instructions have definitely executed
Allow Continued Execution
Guest can use the result to complete addition
The state of this single vCPU is sufficient to migrate the entire machine
The KVM API explicitly requires such operations to re-enter the kernel for completion. When you need to stop after completion but before the next guest instruction, you can use immediate_exit or pending unmasked signals depending on capability support; completing pending operations occurs before subsequent signal checks. immediate_exit support should be queried via KVM_CAP_IMMEDIATE_EXIT. Migration also requires coordinating other vCPUs, devices, and memory; here we only handle one easily overlooked local boundary. KVM Run Area and I/O Completion Conventions
Below is the state machine for three abstract instructions IN → ADD → HALT. pc is the instruction index, not an x86 byte address; it is used to demonstrate completion order and does not simulate true instruction decoding, hardware exits, or KVM ioctls.
importerrnoclassGuestModel:def__init__(self):self.pc=0self.acc=0self.waiting_input=Falseself.input_byte=Noneself.exit_reason=Nonedefrun(self,immediate_exit=False):# Complete the input operation from the previous exit first, then handle the early return for this round.
ifself.waiting_input:ifself.input_byteisNoneornot0<=self.input_byte<=255:raiseValueError("Must provide an input byte first")self.acc=self.input_byteself.pc+=1self.waiting_input=Falseself.input_byte=Noneifimmediate_exit:return-1, errno.EINTR# exit_reason does not represent a new exit this time
whileTrue:ifself.pc==0:self.waiting_input=Trueself.exit_reason="IO_IN"return0, 0ifself.pc==1:self.acc=(self.acc+1)&255self.pc+=1else:self.exit_reason="HALT"return0, 0forvaluein(0,7,255):guest=GuestModel()assertguest.run()==(0,0)andguest.exit_reason=="IO_IN"guest.input_byte=valueassert(guest.pc,guest.acc,guest.waiting_input)==(0,0,True)assertguest.run(immediate_exit=True)==(-1,errno.EINTR)assert(guest.pc,guest.acc,guest.waiting_input)==(1,value,False)assertguest.exit_reason=="IO_IN"# Old field remains; do not process it again as a new I/O
assertguest.run()==(0,0)andguest.exit_reason=="HALT"assert(guest.pc,guest.acc)==(2,(value+1)&255)
The trace for value 7 stops at pc=1, acc=7 after completing the input; running further yields 8. If registers are saved after writing to the buffer but before re-entering the kernel, the hidden state of the pending operation is not fully expressed by ordinary visible states, and the restore side may be unable to continue the same operation.
The second call in the model also illustrates a detail: EINTR does not mean that all previous work did not happen. This round may have already completed the previous pending operation; however, it does not provide a new successful exit, so you cannot call the device model again based on the leftover IO_IN field. A normal run loop should first check the ioctl return value before deciding which exit fields to read.
I/O Data Interpreted by Offset and Count
The data_offset of KVM_EXIT_IO is relative to the start of the entire struct kvm_run mapping; the data is a compact array of count elements, each size bytes. For example, size=2, count=3 means 6 bytes, not just processing the first 16-bit value. For OUT, the VMM reads this data; for IN, the VMM fills the device return value back into the same region.
The offset is not a guest virtual address, nor a guest physical address; using it directly to query guest page tables will access the wrong address space. MMIO exits use a different set of fields, including physical address, length, and data, and cannot be mixed with the layout of port I/O. During processing of the shared region, ensure that the execution of the same vCPU and device processing are serialized according to the predetermined thread protocol, avoiding rewriting data from the previous round while re-entering KVM_RUN.
The Shared Run Area is Not a Complete Register Mirror
KVM_GET_VCPU_MMAP_SIZE gives the vCPU shared mapping size; do not assume it is one page. struct kvm_run is used for execution control and exit data, not as a general-purpose complete register mirror; registers are usually handled via corresponding ioctls, or under explicitly supported and enabled synchronization mechanisms.
Interrupt Injection Depends on Whether the Guest Can Receive Them
Device completion, virtual timers, and vCPU IPIs can all generate virtual interrupts. Setting the pending state does not guarantee immediate delivery before the next guest instruction; it must also comply with guest interrupt masking, priority, and hardware injection conditions. kick is used to prompt the vCPU to process new work and does not mean unconditionally running its handler immediately.
Kernel irqchip, irqfd, and hardware interrupt virtualization can reduce some user-space round trips, but the path depends on architecture capabilities. Address translation is covered in Memory and Device Virtualization; thread and migration collaboration is covered in QEMU and KVM.