状态:草稿(2026-10-17),源码 + 只读实测。 基准为 KubeVirt
v1.9.0(提交79d34c17)。实测仅在两台运行中的 VM 的 virt-launcher Pod 内使用了读取命令(virsh dumpxml,qemu-img info -U,/proc·/sys读取)。
KubeVirt 源码阅读 5:virt-launcher
目录
1. 直觉:翻译器、司机、守夜人
每个 virt-launcher Pod 中包含三个角色。
- 翻译器:将 VMI(Kubernetes 的语言)转换为 libvirt 域 XML(虚拟化的语言)。这项工作由 converter 完成。
- 司机:在 Pod 内启动 libvirt 守护进程,并指挥该守护进程定义并启动域。
- 守夜人:确保 QEMU 结束时 Pod 也随之结束,并在 Kubernetes 尝试停止 Pod 时将信号传递给 VM 端。
第 4 篇中的 virt-handler 通过 gRPC 呼叫“启动”之后,后续的所有工作都在此 Pod 内发生。
2. 图解:一次 SyncVMI
3. 代码路径
3.3 建立进程树的一端: cmd/
Pod 的 PID 1 是 virt-launcher-monitor 。 RunAndMonitor()
(virt-launcher-monitor.go:98)
/usr/bin/virt-launcher 作为子进程启动,并且每当收到 SIGCHLD 时,就会通过 Wait4(-1, …) 回收僵尸进程。由于 QEMU 是由 libvirt 像守护进程一样启动的,因此当父进程结束时,它会成为 PID 1 的子进程。如果 PID 1 不回收僵尸进程,进程表就会被填满。
virt-launcher 的 main()(virt-launcher.go:347)
按此顺序进行准备。
- 创建文件夹(
initializeDirs),并启动 libvirt 守护进程virtqemud(:432)。同时启动日志守护进程virtlogd。 v1.9.0 使用的不是以往的单体式libvirtd,而是只负责 QEMU 驱动的模块化守护进程。docs/components.md尚处于libvirtd上面是这么写的,所以请相信代码。 - 连接到 libvirt,并创建域管理器
NewLibvirtDomainManager(...)。 - 以 临时名称的套接字打开 cmd 服务器(
startCmdServer, :482). - 开始监视域事件并设置信号处理器。
markReady()(:80)——将临时套接字名称改为真正 名称(launcher-sock) 即为就绪信号。virt-handler 从看到该名称的套接字起便开始与其通信。waitForDomainUUID()(:525)等待定义域。如果在第 3 篇的--qemu-timeout(240 秒 + 随机时间)过去后仍不存在域,它将自行终止。这是一种防止启动停滞的 Pod 永远残留的机制。
3.2 SyncVMI:单个请求的工作内容
cmd 服务器的 SyncVirtualMachine()(cmd-server/server.go:250)会解析 JSON 格式的 VMI,并将其传递给 LibvirtDomainManager.SyncVMI()
(manager.go:1380)。该函数是 virt-launcher 的核心。
domainModifyLock。即使 handler 多次发送相同的 VMI(在实际测量中,“Synced vmi”在 1 秒内出现了四次),它也会逐个处理。generateConverterContext()(:1150)——收集翻译所需的外部事实。包括磁盘镜像的格式、是否为块设备、GPU·SR-IOV 设备的 PCI 地址(第 7 篇)、EFI 固件位置等。这种结构使 converter 不需要直接遍历文件系统,从而能够像纯函数一样测试 converter。Convert_v1_VirtualMachineInstance_To_api_Domain()(见下方 3.3)——VMI →api.Domain结构体。lookupOrCreateVirDomain()(:1619)——通过名称查找,若不存在则preStartHook()准备磁盘并定义 XML。成功后会输出“Domain defined.”(:1651).preStartHook()(:892) — 准备 cloud-init 数据、为容器磁盘与 ephemeral 卷创建 写层(3.4)、创建空磁盘 (emptyDisk)、创建 ConfigMap 和 Secret 磁盘。- 如果域已关闭且 VMI 尚未处于 Running 状态,则
startDomain()(:1596) — 创建 cloud-init ISO 并dom.CreateWithFlags()通过启动 QEMU。提示信息为“Domain started.”。 - 如果域已经在运行,则
syncDisks()与network.Sync()仅用于使热插拔的磁盘和接口保持一致。
3.3 converter:依次应用配置器的构建器
converter/converter.go 的 Convert_v1_VirtualMachineInstance_To_api_Domain()
(:967) 避免了为每个设备堆砌代码,而是 配置器(Configurator)列表并进行创建(:993)
由构建器依次调用(:1062).
列表中有 28 个(元数据、网络、TPM、vsock、时钟、随机数设备、输入设备、内存气球、图形、声音、主机设备、
看门狗、控制台、SMBIOS、OS、virtiofs、控制器、CPU、IO 线程、内存……),根据 Hypervisor 的不同,会附加一个 KVM 或 MSHV 配置器,总共 29 个。
每个配置器只有一个 Configure(vmi, domain) 方法,因此, 修改单个设备的 PR 只需要触及一个配置文件及其测试 。
converter/compute/ 以下文件名即为设备名称(watchdog.go, balloon.go, host_device.go …).
磁盘在配置器之外,在构建器之后运行 vmi.Spec.Domain.Devices.Disks 并调用各卷类型的转换函数。只需看以下三种即可。
| 卷 | 函数 | XML 的外观 |
|---|---|---|
| PVC·DataVolume(文件系统) | Convert_v1_FilesystemVolumeSource_To_api_Disk(:784) |
raw 文件 …/vmi-disks/<볼륨>/disk.img |
| 容器磁盘 | Convert_v1_ContainerDiskSource_To_api_Disk(:898) |
qcow2 写入层 + <backingStore> 镜像 |
| ephemeral(PVC 上的临时写入) | Convert_v1_EphemeralVolumeSource_To_api_Disk(:922) |
qcow2 写入层 + <backingStore> PVC |
域名由元数据配置器 <네임스페이스>_<이름> 决定
(metadata/configurator.go:34).
3.4 写入层: pkg/ephemeral-disk/ephemeral-disk.go
容器磁盘是镜像内的文件,因此是只读的。如果客户机要写入,其上方必须有一个 qcow2 写入层。
containerdisk.CreateEphemeralImages()(container-disk.go:328)会为每个容器磁盘卷调用 CreateBackedImageForVolume()
(ephemeral-disk.go:96),实际执行的命令是
(:143).
cmd := exec.Command("qemu-img",
"create",
"-f",
"qcow2",
"-b",
backingFile,
"-F",
backingFormat,
imagePath,
)
来源:kubevirt/kubevirt v1.9.0 pkg/ephemeral-disk/ephemeral-disk.go 第 145–154 行(Apache-2.0)。写入层的位置由 GetFilePath()
(:91决定的 <ephemeral-disk-dir>/<볼륨>/disk.qcow2
,该文件夹是 Pod 的 emptyDir。如果 Pod 被删除,写入层也会随之消失。由于如果文件已存在则不会重新创建,因此在同一个 Pod
内,launcher 多次接收 SyncVMI 也是安全的。《直到学生 VM 启动》第 4 篇中提到的“黄金磁盘上的薄写入层”正是
此函数的 volume.Ephemeral 分支(CreateEphemeralImages, :127)。
3.5 网络:masquerade 绑定
使用 Pod 网络的 VM,其默认绑定是 masquerade。工作分为两部分。
- virt-handler(Pod 外部) 在 Pod 的网络命名空间中创建网桥
k6t-eth0和 tap。期望的形态由masqueradeBindingSpec()(network/setup/netpod/netpod.go:415)绘制。网桥被赋予固定的 MAC 地址(02:00:00:00:00:00(link/mac.go:27)以及 VM 专用的小型私有网段(api.DefaultVMCIDR)的网关地址,并将 tap 的所有者设置为 QEMU 的 UID。此外,还在 nftables 中添加了“将从客户机地址发出的流量更改为 Pod 地址”的规则(MasqPod.Setup, masquerade.go:99). - virt-launcher(Pod 内部) 启动一个小型 DHCP 服务器,向客户机分配该网段的地址(位于
network/setup/launcher的ensureDHCP), 并且在域 XML 中写入<interface type='ethernet'>和<target dev='tap0' managed='no'/>(domainspec/generators.go:106).managed='no')。其中
意为“tap 已经由其他人创建好了,libvirt 不要创建,只需打开即可”。非 root 的 libvirt 没有权限创建 tap,这种分工使得非 root VM 成为可能。
4. 实测:读取 Pod 内部compute 我们在两个 VM 的 virt-launcher Pod(
容器)中进行了读取(2026-10-17)。VM A 是 2 vCPU·2Gi·PVC 根目录,VM B 是 8 vCPU·8Gi·容器磁盘根目录。 /proc/*/status 进程树与内存(VM A)。
| 分别为 PPid 和 VmRSS。 | PID | 父进程 | 进程 | RSS |
|---|---|---|---|---|
| 1 | — | 第 3 篇估算公式的份额 | virt-launcher-monitor | 8.7MiB |
| 13 | 1 | 25Mi | virt-launcher | 68.5MiB |
| 26 | 13 | 100Mi | virtqemud | 31.6MiB |
| 27 | 13 | 40Mi | virtlogd | 14.8MiB |
| 97 | 1 | 25Mi | qemu-kvm | 626MiB |
客户机 2Gi + 30Mi 等
QEMU 的父进程为 PID 1,这正是 3.1 节中“monitor 收容由 libvirt 像守护进程一样启动的 QEMU”的体现。除 QEMU 外的四个进程的 RSS 总和为 123.6MiB,处于估算公式的固定值(190Mi,不含 QEMU 的 30Mi)之内。鉴于 RSS 按每个进程统计共享库的特性,实际的空闲空间还要更大。客户机获得了 2Gi,但 QEMU 的 RSS 为 626MiB。客户机尚未触及的内存不会被宿主机占用。 libvirt 连接。 Connecting to libvirt daemon: qemu+unix:///session?socket=/var/run/libvirt/virtqemud-sock
(libvirt.go:680launcher 日志的 virtqemud 显示其以非 root 会话模式连接到了
。 virsh dumpxml default_fedora-vm 域 XML(VM A,已缩略)。
<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>
的前半部分和部分设备。对照代码来看是这样的。名称是 <네임스페이스>_<이름>, maxMemory 是客户机的 4 倍(内存热插拔上限的默认比例
DefaultMaxHotplugRatio = 4),磁盘对应 3.3 节表格的第一行,用户自定义名称前带有 ua- (user alias),串行控制台和 VNC 使用的不是 TCP,而是 kubevirt-private/<VMI UID>/ 下的 Unix 套接字。virt-handler 将控制台连接到此套接字。此外,还有
pcie-root-port 个控制器。部分被磁盘和 NIC 占用,剩余的是 allocateHotplugPorts() 预留用于热插拔的空槽位。
写入层(VM B)。 磁盘 XML 和 qemu-img info -U 。
| 文件 | 格式 | 虚拟大小 | 实际大小 |
|---|---|---|---|
/var/run/kubevirt/container-disks/disk_0.img(底盘,只读) |
qcow2 | 3.5GiB | 596MiB |
/var/run/kubevirt-ephemeral-disks/disk-data/root/disk.qcow2(写入层) |
qcow2, backing file = 上述文件 | 3.5GiB | 998MiB(已使用约 2.5 天) |
/var/run/libvirt/empty-disks/scratch.qcow2(emptyDisk) |
qcow2 | — | 1.6G |
写入层的路径是 GetFilePath() 的 <볼륨 이름>/disk.qcow2,backing 格式为 -F qcow2 。底部的 596MiB 与“学生虚拟机启动全过程”
第 3 篇的实测大小相同。在同一个 Pod 的 container-disks 文件夹中还有 disk_0.sock 。这是第 4 篇中 virt-handler 用于查找并挂载包含镜像的容器的标记。
网络 (VM A)。 在 Pod 内的 /sys/class/net 中有 eth0, k6t-eth0(MAC 02:00:00:00:00:00), tap0(挂载在网桥 k6t-eth0 上)、 lo
这 4 个网络。Domain XML 中的 Guest MAC 与 Pod eth0 的 MAC 相同。
时间。 在 launcher 日志中,“Marked as ready” → “Domain defined.” 为 1.20 秒 → “Domain started.” 为 0.19 秒(第 4 篇表格)。前面的 1.2 秒主要是 handler 侧的准备(网络、设备)时间,定义和启动本身非常短。
5. 局限性
- 没有绑定 GPU 和宿主机设备的域,因此未读取其 XML(第 7 篇, 未实测).
- )。通过 DHCP 分配给 Guest 的地址需要进入 Guest 内部查看,因此未进行确认。Guest Agent 报告的 VMI status 地址即为 Pod 地址。
- 内存数值是某一时刻的 RSS。随时间的变化以及与页面缓存的关系 未实测。
- CPU 模型是
host-model的展开具名模型(功能列表约 70 行),由于会暴露节点的 CPU,所以在文中略去了。
Titi 的一言。 得益于 converter 改为配置器列表,如果想修改某个设备,只需在
converter/compute/中打开该设备名称对应的单个文件即可。作为第一次贡献,其规模正合适!
参考
全部于 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 格式: https://libvirt.org/formatdomain.html
- libvirt 模块化守护进程 (virtqemud): https://libvirt.org/daemons.html
- 姊妹系列“学生虚拟机启动全过程”第 3 篇: 磁盘即 PVC,第 4 篇: PVC 视角的 Overlay