Adding IOThreads does not guarantee that all device operations run in parallel, nor does running pre-copy migration for a few more rounds guarantee a reduction in dirty pages. QEMU and KVM collaboration requires simultaneous management of execution context, device state, and migration consistency. The complete backend may also reside in other processes.
Objective
Reading Location
Machine and Execution Organization
Threads, creation, execution loops
Notification and Data Division
eventfd
Convergence, Switching, and Fault Localization
Migration-related sections
Threads Are Not a Fixed One-to-One Mapping for Devices
When a vCPU thread enters KVM_RUN, the main event loop handles management and corresponding device events; devices configured with IOThreads can use independent event loops. Not every virtio device automatically gets its own thread; thread pools, backends, and device implementations also participate in I/O. IOThreads
Multithreading allows for parallelism but introduces state synchronization and execution context constraints. Mechanisms such as QEMU's global lock, device locks, and AioContext are responsible for specific states; one cannot infer that all device operations are parallel or lock-free simply by "adding IOThreads."
From Creation to an Executable Machine
Stage
User Space Responsibilities
KVM Provides
Interface Detection
Query API and required capabilities
system fd, extended queries
Machine Creation
Select machine layout and backing
VM object and memory registration interfaces
CPU Initialization
Set architecture-required registers, features, and boot state
vCPU objects, architecture ioctls
Device Setup
Configure device models and backends
Optional kernel devices, irqfd/ioeventfd, etc.
Run and Exit Handling
Handle operations requiring user space
Guest execution and exits that can be handled in the kernel
Simply calling CREATE_VM, registering RAM, and CREATE_VCPU does not mean the operating system can start: configurations for CPU mode, boot code or firmware, interrupt controllers, and devices are still missing. The simplified flow in tutorials must be distinguished from a "complete launcher."
After creating backing via mmap, physical pages may be allocated on demand; MAP_NORESERVE does not guarantee that memory will always be sufficient later. The memslot expresses the guest address mapping, but the VMM must still maintain the backing lifecycle; changes or unmap operations need coordination with KVM. KVM API
Two Levels of Return from the Execution Loop
Hardware exits can be handled by KVM and re-enter the guest directly; the VMM's exit handling only enters when KVM_RUN returns. When an error is returned, handle errno first; upon successful return, read run->exit_reason; do not interpret every ioctl return as a normal device request.
For MMIO/PIO reads/writes that need to be completed, the VMM fills in the data and re-enters KVM to complete the corresponding guest operation. If only registers are saved before migration while ignoring unfinished exits, state boundaries may be lost. QEMU already has architecture and device collaboration logic; it is inappropriate to use a few lines of pseudo-C functions lacking error handling as its real source code.
eventfd Reduces Notification Detours, Does Not Carry All Data
ioeventfd can convert matching guest I/O writes into eventfd notifications; irqfd can associate corresponding eventfd signals with guest interrupt injection. Request data remains in buffers described by the virtqueue or device protocol; eventfd is not the content of every complete request.
The kernel vhost backend can reduce the round-trip of QEMU user space processing data requests based on this, but initialization, feature negotiation, device state, and migration control still require the VMM. User-space backends have another set of shared memory and inter-process protocols.
Pre-copy Migration and Convergence
While the source VM is running, memory continues to be modified, and the migrator needs to track pages that remain dirty after this round. Write-protection triggering tracking is one way; hardware dirty bits, dirty bitmap/ring, etc., may also participate; not all mechanisms can be described as "setting all RAM to read-only at the beginning."
If the rate of continuously dirtying pages approaches or exceeds the rate the transfer can handle, repeated pre-copy may not converge. The amount of dirty pages in the new round is not guaranteed to be less than the previous round; bandwidth, compression, workload throttling, and switching strategies jointly affect the result. Migration Framework
Teaching Condition
Rate
Effective copy capability
1 GiB/s
Workload continuously re-dirties large ranges of pages
~1.5 GiB/s
At this point, you cannot wait for "copying a few more rounds to naturally clear to zero." Real convergence is also affected by repeated writes to the same page, page granularity, and windows; you cannot treat the two rates directly as a precise prediction formula.
From 8 GiB to Final Downtime: Calculate Round by Round
Assume the memory to be copied is M=8 GiB, the effective send rate is B=2 GiB/s, and the new different dirty pages generated each round and needing to be sent in the next round grow at D=0.5 GiB/s. Assume fixed rates, no compression, and temporarily ignore page granularity rounding and additional transmission overhead. This is a round-aggregated model; D represents the data volume of different dirty pages, not the original bytes of application execution stores.
When the send amount for this round is R, the time is R/B; the new workload generated during this time is min(M, D×R/B). When not hitting the M limit, the shrinkage factor is D/B=1/4.
Running Round
Send This Round (GiB)
Duration This Round (s)
Dirty Pages to Send Next Round (GiB)
1
8
4
2
2
2
1
0.5
3
0.5
0.25
0.125
4
0.125
0.0625
0.03125
If the fixed CPU/device switching overhead is 40 ms, stopping the source immediately after the third round requires 0.125/2 s + 40 ms = 102.5 ms. Running a fourth round leaves 0.03125 GiB, i.e., 32 MiB, with an estimated downtime of 15.625 + 40 = 55.625 ms, which finally falls within the 60 ms budget for this example.
When D≥B, this model regenerates all 8 GiB every round, and continuing to increase rounds does not shorten the final downtime. The actual workload's hot page set may be small: the same page written ten thousand times might still only need to be sent once in a certain statistical window. Therefore, judgments should be made based on different dirty page amounts, windows, and actual bandwidth; do not directly substitute memory write bandwidth counts into D. QEMU's dirty-limit mechanisms help convergence by limiting dirty workloads, but the cost includes impacting the source workload. QEMU Dirty limit
Clearing Dirty Bits After Copying May Erase Newly Occurring Writes
Consider a page with a current value of 0, and the dirty bit is already set. After the migrator copies 0 to the target, the guest changes the source page to 1, and the dirty bit remains 1. If the migrator unconditionally clears the dirty bit at this moment to indicate "this page copy is complete," it erases the evidence of the new write; the next round will neither see the dirty page nor supplement the value 1.
A correct protocol needs to establish clear tracking boundaries, separating the old dirty state processed in this round from subsequent new modifications. Below, use atomic exchange of dirty sets to express this boundary: take out the current round's set and start tracking the next round, then copy; modifications occurring during the copy enter the new set. It does not implement real KVM dirty bits, write-protection restoration, memory barriers, or DMA tracking.
classMemory:def__init__(self):self.pages={0:0}self.dirty={0}defwrite(self,page,value):self.pages[page]=valueself.dirty.add(page)deftake_epoch(self):old, self.dirty=self.dirty, set()returnold# In the model, this operation cannot be interrupted by writes
# Incorrect order: clearing after copy swallows new modifications.
bad=Memory()target={0:bad.pages[0]}bad.write(0,1)bad.dirty.clear()assertnotbad.dirtyandtarget!=bad.pages# Correct boundary: new modifications after this round's copy are left for the next round.
source=Memory()target={}forpageinsource.take_epoch():target[page]=source.pages[page]source.write(0,1)assertsource.dirty=={0}andtarget[0]==0forpageinsource.take_epoch():target[page]=source.pages[page]asserttarget==source.pagesandnotsource.dirtyforvalueinrange(2,102):source.write(0,value)assertlen(source.dirty)==1# One hundred writes, still only one different dirty page
Real systems have multiple layers of tracking including KVM, QEMU RAM accounting, and migration sending; you cannot directly replace take_epoch() in the model with any clear ioctl. Kernel re-protection of pages, hardware dirty bit collection, and sending order need to jointly ensure that "modifications after reading are not lost." Page content may also change during copying; finally, after stopping the source, re-synchronizing the remaining dirty state forms the final consistent switch boundary.
Change effective bandwidth B and dirtying rate D, then choose when to finish pre-copy. Watch how the remaining set and stop-and-copy time behave as D/B approaches one.
Mn+1=min(M,BDMn),Tstop≈BMn+1+Tfixed
M is total memory and M_n the set copied in round n. The model assumes constant bandwidth and an effective distinct-page dirtying rate, capped by total memory; repeated writes to one page do not add unlimited dirty bytes.
Preparing the visual
Do more copy rounds always reduce the remaining dirty data?
Round i takes Mᵢ/B time, during which new dirty data is approximately D×Mᵢ/B. It shrinks when D/B<1; near or above 1, more precopy does not ensure convergence.
{"id":"migration-dirty-rate","title":"Do more copy rounds always reduce the remaining dirty data?","summary":"Round i takes Mᵢ/B time, during which new dirty data is approximately D×Mᵢ/B. It shrinks when D/B<1; near or above 1, more precopy does not ensure convergence.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"bandwidth\" data-i18n=\"bandwidth\"></label><output for=\"bandwidth\" id=\"bandwidth-value\"></output><input id=\"bandwidth\" type=\"range\" min=\"100\" max=\"1000\" value=\"500\" step=\"10\"></div><div class=\"control\"><label for=\"dirty\" data-i18n=\"dirty\"></label><output for=\"dirty\" id=\"dirty-value\"></output><input id=\"dirty\" type=\"range\" min=\"0\" max=\"1200\" value=\"250\" step=\"10\"></div><p class=\"caption\" data-i18n=\"rounds\"></p><div class=\"rounds\"><button data-round=\"0\"><span>1</span><div><i id=\"round-0\"></i></div><strong id=\"size-0\"></strong></button><button data-round=\"1\"><span>2</span><div><i id=\"round-1\"></i></div><strong id=\"size-1\"></strong></button><button data-round=\"2\"><span>3</span><div><i id=\"round-2\"></i></div><strong id=\"size-2\"></strong></button><button data-round=\"3\"><span>4</span><div><i id=\"round-3\"></i></div><strong id=\"size-3\"></strong></button><button data-round=\"4\"><span>5</span><div><i id=\"round-4\"></i></div><strong id=\"size-4\"></strong></button><button data-round=\"5\"><span>6</span><div><i id=\"round-5\"></i></div><strong id=\"size-5\"></strong></button><button data-round=\"6\"><span>7</span><div><i id=\"round-6\"></i></div><strong id=\"size-6\"></strong></button><button data-round=\"7\"><span>8</span><div><i id=\"round-7\"></i></div><strong id=\"size-7\"></strong></button></div><div class=\"metrics\"><div><span data-i18n=\"remaining\"></span><strong id=\"remaining\"></strong></div><div><span data-i18n=\"downtime\"></span><strong id=\"downtime\"></strong></div></div><div class=\"formula\" id=\"formula\"></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"shrinking\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"stalled\"></p></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.rounds button{display:grid;grid-template-columns:20px minmax(0,1fr) 62px;align-items:center;gap:8px;width:100%;padding:10px 8px;margin:5px 0;font-size:12px}.rounds button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.rounds button>div{height:18px;background:var(--surface)}.rounds i{display:block;height:100%;background:var(--accent);transition:width .2s}.rounds strong{font:11px var(--viz-font-ui);text-align:right}.formula{min-height:6em;white-space:pre-line}","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 chosen=2;function draw(){const b=+$('#bandwidth').value,d=+$('#dirty').value,ratio=d/b,sizes=[512];for(let i=0;i<8;i++)sizes.push(Math.min(512,ratio*sizes[i]));set('bandwidth-value',b+' MB/s');set('dirty-value',d+' MB/s');for(let i=0;i<8;i++){$('#round-'+i).style.width=sizes[i]/512*100+'%';set('size-'+i,viz.number(sizes[i],1)+' MB');document.querySelector('[data-round=\"'+i+'\"]').setAttribute('aria-current',String(i===chosen));}set('remaining',viz.number(sizes[chosen+1],2));set('downtime',viz.number(sizes[chosen+1]/b*1000+20,1));set('formula','D/B = '+viz.number(ratio,2)+'\\nM'+(chosen+2)+' = min(512, '+d+' × '+viz.number(sizes[chosen],2)+' / '+b+')\\nstop = '+viz.number(sizes[chosen+1],2)+' / '+b+' s + 20 ms');set('explanation',t(ratio<1?'shrinking':'stalled'));}$('#bandwidth').oninput=$('#dirty').oninput=draw;document.querySelectorAll('[data-round]').forEach(e=>e.onclick=()=>{chosen=+e.dataset.round;draw();});draw();","audio":false,"strings":{"bandwidth":"Effective copying B / MB/s","dirty":"Dirtying D / MB/s","downtime":"Estimated downtime / ms","heading":"Do more copy rounds always reduce the remaining dirty data?","intro":"Adjust effective bandwidth B and dirtying rate D; compare eight rounds. Select a round to inspect final-copy time if the VM stops after it.","ratio":"D/B","remaining":"Remaining after selected round / MB","reset":"Reset","rounds":"Select a round: bar is data pending at its start","scope":"M=512 MB decimal; bandwidth already includes copying overhead. Uniform unique-dirty-byte approximation: Mᵢ₊₁=min(M,D×Mᵢ/B). Selection stops the VM after that round; downtime=remaining/B+fixed 20 ms device finalization. Compression, repeated dirty pages, tracking overhead, and other migration stages are omitted; this is not a real-VM downtime prediction.","scopeLabel":"Model scope and assumptions","shrinking":"Each round reduces the remainder, but convergence slows as the ratio approaches 1.","stalled":"Dirtying keeps up with copying, so bars do not shrink. Infinitely many rounds cannot yield a tiny final copy in this model."}}
Consistency at the Last Moment Involves More Than Just RAM
State at Switching
Why Coordinate Together
vCPU registers, virtual interrupts, and timers
Restore correct execution position and pending events
Therefore, migration consistency cannot be judged solely by "RAM has been copied."
Post-copy can fetch missing pages on demand after the target resumes, changing the trade-off between downtime and page fault waiting, and making source/target fault recovery after switching more complex. It is not equivalent to "running the last round of pre-copy faster" and should not be compared using a single downtime number.
Before and after successful migration, it must be clear which instance has external write rights to avoid split-brain operation. Whether passthrough devices can be migrated depends on device state and DMA tracking capabilities; see Memory and Device Virtualization.
Find the Layer First When Observing Problems
When CPU usage is high, distinguish between guest effective execution, frequent exits, and host scheduling waits; when I/O latency occurs, distinguish between guest queuing, notifications, QEMU/backend execution, and actual storage network; when migration stalls, look at dirty page rates, effective bandwidth, and device blocking.
/dev/kvm availability detection can be found in KVM Architecture. Further source code can be entered from QEMU accel/kvm/kvm-all.c, Linux virt/kvm, and corresponding arch directories; version-related paths should be based on the actual checkout.