Status: Draft (2026-10-17), Source + Read-only Measurement. Based on KubeVirt
v1.9.0(commit79d34c17). Measurements used only read commands (virsh dumpxml,qemu-img info -U,/proc·/sysread) inside the virt-launcher pods of two running VMs.
Reading KubeVirt Source Code 5: virt-launcher
Table of Contents
- 1. Intuition: Translator, Driver, Sentinel
- 2. Diagram: A Single SyncVMI
- 3. Code Paths
- 4. Measurement: Reading Inside the Pod
- 5. Limitations
- References
1. Intuition: Translator, Driver, Sentinel
A single virt-launcher pod contains three roles.
- Translator: Converts a VMI (the Kubernetes language) into a libvirt domain XML (the virtualization language). This is handled by the converter.
- Driver: Launches the libvirt daemon inside the pod and instructs that daemon to define and start the domain.
- Sentinel: Ensures that when QEMU terminates, the pod also terminates, and forwards signals to the VM when Kubernetes attempts to stop the pod.
Once virt-handler in Part 4 calls via gRPC to "start", everything that follows happens inside this pod.
2. Diagram: A Single SyncVMI
3. Code Paths
3.1 Setting Up the Process Tree: cmd/
PID 1 of the pod is virt-launcher-monitor . RunAndMonitor()
(virt-launcher-monitor.go:98) spawns
/usr/bin/virt-launcher as a child, and reaps zombies using SIGCHLD whenever it receives Wait4(-1, …) . Since QEMU is launched like a daemon by libvirt, it becomes a child of PID 1 when its parent exits. If PID 1 does not reap zombies, the process table fills up.
virt-launcher 's main()(virt-launcher.go:347) prepares in this order:
- Creates directories (
initializeDirs), and launches the libvirt daemonvirtqemud(:432). It also launches the log daemonvirtlogd. v1.9.0 uses modular daemons dedicated only to the QEMU driver rather than the legacy monolithiclibvirtd.docs/components.mdis stilllibvirtdAs it says, trust the code. - It connects to libvirt and creates a domain manager
NewLibvirtDomainManager(...). - It opens the cmd server with a temporary socket name(
startCmdServer, :482). - starts monitoring domain events and sets up signal handlers.
markReady()(:80) — renaming the temporary socket to the real name (launcher-sock) serves as the readiness signal. virt-handler starts communicating with it as soon as that socket appears.waitForDomainUUID()(:525) for the domain to be defined. If the domain does not exist even after the time specified in Part 3 (--qemu-timeout240 seconds + randomization), it terminates itself. This is a safeguard to prevent pods with stuck startups from lingering forever.
3.2 SyncVMI: What a Single Request Does
The cmd server's SyncVirtualMachine()(cmd-server/server.go:250) unpacks the VMI received as JSON and passes it to LibvirtDomainManager.SyncVMI()
(manager.go:1380). This function is the core of virt-launcher.
domainModifyLockIt acquires a . Even if the handler sends the same VMI multiple times (in practice, "Synced vmi" four times within a second), they are processed one at a time.generateConverterContext()(:1150) — Gathers external facts needed for translation. Examples include disk image formats, whether they are block devices, PCI addresses for GPU/SR-IOV devices (Part 7), and EFI firmware locations. This structure prevents the converter from directly searching the filesystem, allowing the converter to be tested as a pure function.Convert_v1_VirtualMachineInstance_To_api_Domain()(3.3 below) — VMI →api.Domainstruct.lookupOrCreateVirDomain()(:1619) — Searches by name, and if not found, prepares disks withpreStartHook()and defines the XML. If successful, "Domain defined." (:1651).preStartHook()(:892) — preparing cloud-init data, creating container disk and ephemeral volume write layers(3.4), creating empty disks (emptyDisk), and creating ConfigMap and Secret disks.- If the domain is shut down and the VMI is not yet Running,
startDomain()(:1596) — creates the cloud-init ISO anddom.CreateWithFlags()starts QEMU with . "Domain started." - If the domain is already running,
syncDisks()andnetwork.Sync()are used to reconcile only the differences for hotplugged disks and interfaces.
3.3 converter: A builder that chains configurators
converter/converter.go of Convert_v1_VirtualMachineInstance_To_api_Domain()
(:967) avoids spreading device code everywhere by creating a list of configurators(:993) which the builder runs in sequence (:1062). There are 28 items in the list (metadata, networks, TPM, vsock, clock, RNG, input devices, memory balloon, graphics, sound, host devices, watchdog, console, SMBIOS, OS, virtiofs, controllers, CPU, IO threads, memory, etc.), and depending on the hypervisor, either a KVM or MSHV configurator is added, bringing the total to 29. Each configurator has only a single Configure(vmi, domain) method, so a PR fixing a single device only touches one configurator file and its test .
converter/compute/ The file names below directly correspond to device names (watchdog.go, balloon.go, host_device.go …).
Disks are handled outside the configurators; after the builder, vmi.Spec.Domain.Devices.Disks runs and calls the conversion functions for each volume type. You only need to look at three things.
| Volume | Function | XML Structure |
|---|---|---|
| PVC/DataVolume (File System) | Convert_v1_FilesystemVolumeSource_To_api_Disk(:784) |
raw file …/vmi-disks/<볼륨>/disk.img |
| container disk | Convert_v1_ContainerDiskSource_To_api_Disk(:898) |
qcow2 write layer + <backingStore> image on |
| ephemeral (temporary write on PVC) | Convert_v1_EphemeralVolumeSource_To_api_Disk(:922) |
qcow2 write layer + <backingStore> PVC on |
The domain name is determined by the metadata configurator <네임스페이스>_<이름> as
(metadata/configurator.go:34).
3.4 Write Layer: pkg/ephemeral-disk/ephemeral-disk.go
A container disk is a file inside an image, so it is read-only. For the guest to write to it, there must be a qcow2 write layer on top of it.
containerdisk.CreateEphemeralImages()(container-disk.go:328) calls
for each container disk volume CreateBackedImageForVolume()
(ephemeral-disk.go:96), and the actual command is this
(:143).
cmd := exec.Command("qemu-img",
"create",
"-f",
"qcow2",
"-b",
backingFile,
"-F",
backingFormat,
imagePath,
)
Source: kubevirt/kubevirt v1.9.0 pkg/ephemeral-disk/ephemeral-disk.go lines 145–154 (Apache-2.0). The location of the write layer is GetFilePath()
(:91determined by <ephemeral-disk-dir>/<볼륨>/disk.qcow2
and that folder is the pod's emptyDir. When the pod is deleted, the write layer disappears as well. Since the file is not recreated if it already exists, it is safe even if the launcher receives SyncVMI multiple times within the same pod. The "thin write layer on top of the golden disk" mentioned in Part 4 of "Until the Student VM Boots Up" is precisely this function's volume.Ephemeral branch(CreateEphemeralImages, :127).
3.5 Network: Masquerade Binding
The default binding for VMs using pod networking is masquerade. The work is divided into two parts.
- virt-handler (outside the pod) creates bridge
k6t-eth0and tap in the pod's network namespace. The desired shape ismasqueradeBindingSpec()(network/setup/netpod/netpod.go:415). The bridge is assigned a fixed MAC02:00:00:00:00:00(link/mac.go:27) and a gateway address for a small private band dedicated to the VM (api.DefaultVMCIDR), and the owner of tap is set to QEMU's UID. Then, a rule is added to nftables stating "redirect outbound traffic from the guest address to the pod address" (MasqPod.Setup, masquerade.go:99). - virt-launcher (inside the pod) runs a small DHCP server that assigns an address from that band to the guest (
network/setup/launcher'sensureDHCP), and writes<interface type='ethernet'>and<target dev='tap0' managed='no'/>in the domain XML (domainspec/generators.go:106).managed='no').
4. Measurement: Reading Inside the Pod
Read from the virt-launcher pods (compute container) of two VMs (2026-10-17). VM A is 2 vCPU, 2Gi, PVC root; VM B is 8 vCPU, 8Gi, container disk root.
Process tree and memory (VM A). /proc/*/status 's PPid and VmRSS.
| PID | Parent | Process | RSS | Share of Part 3 estimation formula |
|---|---|---|---|---|
| 1 | — | virt-launcher-monitor | 8.7MiB | 25Mi |
| 13 | 1 | virt-launcher | 68.5MiB | 100Mi |
| 26 | 13 | virtqemud | 31.6MiB | 40Mi |
| 27 | 13 | virtlogd | 14.8MiB | 25Mi |
| 97 | 1 | qemu-kvm | 626MiB | Guest 2Gi + 30Mi, etc. |
The fact that QEMU's parent is PID 1 corresponds to "monitor reaps QEMU launched as a daemon by libvirt" in Section 3.1. The sum of RSS for the four processes excluding QEMU is 123.6MiB, which falls within the fixed value of the estimation formula (190Mi, excluding QEMU's 30Mi). Due to the nature of RSS, which calculates shared libraries per process, the actual headroom is even larger. The guest was allocated 2Gi, but QEMU's RSS was 626MiB. Memory not yet touched by the guest is not captured by the host.
libvirt connection. launcher log's Connecting to libvirt daemon: qemu+unix:///session?socket=/var/run/libvirt/virtqemud-sock
(libvirt.go:680) shows that it connected to virtqemud in non-root session mode.
Domain XML (VM A, truncated). virsh dumpxml default_fedora-vm of the beginning and some devices.
<domain type='kvm' id='1'>
<name>default_fedora-vm</name>
<maxMemory unit='KiB'>8388608</maxMemory>
<memory unit='KiB'>2097152</memory>
<vcpu placement='static'>2</vcpu>
<os><type arch='x86_64' machine='pc-q35-rhel9.8.0'>hvm</type> …</os>
<cpu mode='custom' match='exact' check='full'> … 기능 70여 줄 줄임 … </cpu>
<devices>
<emulator>/usr/libexec/qemu-kvm</emulator>
<disk type='file' device='disk' model='virtio-non-transitional'>
<driver name='qemu' type='raw' cache='none' error_policy='stop' discard='unmap'/>
<source file='/var/run/kubevirt-private/vmi-disks/root/disk.img' index='2'/>
<target dev='vda' bus='virtio'/>
<alias name='ua-root'/>
</disk>
<interface type='ethernet'>
<target dev='tap0' managed='no'/>
<model type='virtio-non-transitional'/>
<alias name='ua-default'/>
</interface>
<serial type='unix'><source mode='bind' path='/var/run/kubevirt-private/<VMI UID>/virt-serial0'/> …</serial>
</devices>
</domain>
Comparing with the code, it is as follows. The name is <네임스페이스>_<이름>, maxMemory is 4 times that of the guest (default ratio for memory hotplug upper limit
DefaultMaxHotplugRatio = 4), the disk is the first line of the table in Section 3.3, a user-defined name is prefixed with ua- (user alias), and the serial console and VNC are Unix sockets under kubevirt-private/<VMI UID>/ instead of TCP. virt-handler connects the console to this socket. Additionally,
pcie-root-port controllers were present. Some are used by disks and NICs, and the remaining are empty slots pre-allocated by allocateHotplugPorts() for hotplugging.
Write layer (VM B). disk XML and qemu-img info -U .
| File | Format | Virtual Size | Actual Size |
|---|---|---|---|
/var/run/kubevirt/container-disks/disk_0.img(Base, Read-only) |
qcow2 | 3.5GiB | 596MiB |
/var/run/kubevirt-ephemeral-disks/disk-data/root/disk.qcow2(Write layer) |
qcow2, backing file = above file | 3.5GiB | 998MiB (used for about 2.5 days) |
/var/run/libvirt/empty-disks/scratch.qcow2(emptyDisk) |
qcow2 | — | 1.6G |
The path of the write layer is GetFilePath() of <볼륨 이름>/disk.qcow2, and the backing format is -F qcow2 . The base 596MiB is the same size as the measurements in Part 3 of "Until the Student VM Starts". In the folder of the same pod, container-disks there was also disk_0.sock . This is a marker used by virt-handler in Part 4 to find and attach the container holding the image.
Network (VM A). inside the pod /sys/class/net there were eth0, k6t-eth0(MAC 02:00:00:00:00:00), tap0(attached to bridge k6t-eth0 ), lo
four interfaces. The guest MAC in the domain XML was the same as the MAC of pod eth0 .
Time. In the launcher logs, "Marked as ready" to "Domain defined." was 1.20 seconds, and "Domain started." was 0.19 seconds (table in Part 4). The preceding 1.2 seconds are mostly handler-side preparation (network and devices) time, while definition and startup themselves are short.
5. Limitations
- Domains with GPUs and host devices attached were not run this time, so their XML could not be read (Part 7, unmeasured).
- ).
- The addresses received by the guest via DHCP were not checked because it would require inspecting inside the guest. The address in the VMI status reported by the guest agent was the pod address. Memory figures are RSS at a single point in time. Changes over time and the relationship with page cache areunmeasured
- .
host-model.
Titi's Word. Thanks to the converter being changed to a list of configurers, if you want to fix a single device, you just need to open a single file with that device's name in
converter/compute/. It is a great size for a first contribution!
References
All accessed on 2026-10-17.
manager.go: https://github.com/kubevirt/kubevirt/blob/v1.9.0/pkg/virt-launcher/virtwrap/manager.goconverter.go: https://github.com/kubevirt/kubevirt/blob/v1.9.0/pkg/virt-launcher/virtwrap/converter/converter.goephemeral-disk.go: https://github.com/kubevirt/kubevirt/blob/v1.9.0/pkg/ephemeral-disk/ephemeral-disk.go- libvirt Domain XML format: https://libvirt.org/formatdomain.html
- libvirt modular daemon (virtqemud): https://libvirt.org/daemons.html
- Sister series "Until the Student VM Starts" Part 3: Disks are PVCs, Part 4: Overlay from a PVC Perspective