TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

KubeVirt 源码阅读 5:virt-launcher — 域 XML 与 QEMU

状态:草稿(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 中包含三个角色。

第 4 篇中的 virt-handler 通过 gRPC 呼叫“启动”之后,后续的所有工作都在此 Pod 内发生。

2. 图解:一次 SyncVMI

virt-launcher — VMI 를 도메인 XML 로 바꿔 띄우기 SyncVMI() 한 번이 하는 일, 위에서 아래로 파드 안의 프로세스 (실측 RSS) monitor 8.7Mi launcher 68.5Mi virtqemud 31.6Mi qemu-kvm 626Mi virtlogd 14.8Mi ← QEMU 의 부모는 PID 1 cmd 서버 SyncVirtualMachine cmd-server/server.go generateConverterContext() 디스크 정보 · GPU · SR-IOV · EFI 를 모은다 Convert_v1_VirtualMachineInstance_To_api_Domain 설정기 29개를 차례로, 이어서 디스크 루프 lookupOrCreateVirDomain() 없으면 preStartHook → DomainDefineXML preStartHook() cloud-init ISO · qemu-img 쓰기 층 · 빈 디스크 startDomain() → CreateWithFlags QEMU 프로세스 시작 syncDisks · network.Sync 핫플러그와 차이 맞추기 실측: Ready → Domain defined 1.20초 → Domain started 0.19초
图:virt-launcher 内的进程与 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) 按此顺序进行准备。

  1. 创建文件夹(initializeDirs),并启动 libvirt 守护进程 virtqemud (:432)。同时启动日志守护进程 virtlogd 。 v1.9.0 使用的不是以往的单体式 libvirtd ,而是只负责 QEMU 驱动的模块化守护进程。 docs/components.md 尚处于 libvirtd 上面是这么写的,所以请相信代码。
  2. 连接到 libvirt,并创建域管理器 NewLibvirtDomainManager(...) 。
  3. 以 临时名称的套接字打开 cmd 服务器(startCmdServer, :482).
  4. 开始监视域事件并设置信号处理器。
  5. markReady()(:80)——将临时套接字名称改为真正 名称(launcher-sock) 即为就绪信号。virt-handler 从看到该名称的套接字起便开始与其通信。
  6. 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 的核心。

  1. domainModifyLock 。即使 handler 多次发送相同的 VMI(在实际测量中,“Synced vmi”在 1 秒内出现了四次),它也会逐个处理。
  2. generateConverterContext()(:1150)——收集翻译所需的外部事实。包括磁盘镜像的格式、是否为块设备、GPU·SR-IOV 设备的 PCI 地址(第 7 篇)、EFI 固件位置等。这种结构使 converter 不需要直接遍历文件系统,从而能够像纯函数一样测试 converter。
  3. Convert_v1_VirtualMachineInstance_To_api_Domain()(见下方 3.3)——VMI → api.Domain 结构体。
  4. lookupOrCreateVirDomain()(:1619)——通过名称查找,若不存在则 preStartHook() 准备磁盘并定义 XML。成功后会输出“Domain defined.”(:1651).
  5. preStartHook()(:892) — 准备 cloud-init 数据、为容器磁盘与 ephemeral 卷创建 写层(3.4)、创建空磁盘 (emptyDisk)、创建 ConfigMap 和 Secret 磁盘。
  6. 如果域已关闭且 VMI 尚未处于 Running 状态,则 startDomain() (:1596) — 创建 cloud-init ISO 并 dom.CreateWithFlags() 通过启动 QEMU。提示信息为“Domain started.”。
  7. 如果域已经在运行,则 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

도메인 XML 이 가리키는 파일과 장치 실측한 두 VM 의 <disk> · <interface> 를 그렸다 디스크 세 갈래 컨테이너 디스크 읽기 전용 바탕 disk_0.img 596Mi 쓰기 층 disk.qcow2 — 998Mi …/ephemeral-disks/disk-data/root/ qemu-img create -b 바탕 파드가 지워지면 emptyDir 와 함께 쓰기 층도 사라진다 PVC 디스크 — raw disk.img (10 GiB) /var/run/kubevirt-private/vmi-disks/root/disk.img 빈 디스크 emptyDisk scratch.qcow2 1.6G cloud-init noCloud.iso (raw) driver: cache=none · error_policy=stop · discard=unmap 네트워크 (masquerade 바인딩) eth0 파드 IP k6t-eth0 브리지 · NAT tap0 QEMU 가 연다 <interface type='ethernet'> target dev='tap0' managed='no' 브리지 MAC 은 고정값 02:00:00:00:00:00 (link/mac.go) 게스트 NIC 의 MAC 은 파드 eth0 의 MAC 을 그대로 쓴다(실측)
图:域 XML 所指向的三条磁盘分支与 masquerade 网络

容器磁盘是镜像内的文件,因此是只读的。如果客户机要写入,其上方必须有一个 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。工作分为两部分。

意为“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. 局限性

Titi 的一言。 得益于 converter 改为配置器列表,如果想修改某个设备,只需在 converter/compute/ 中打开该设备名称对应的单个文件即可。作为第一次贡献,其规模正合适!

参考

全部于 2026-10-17 访问。

登录后即可点赞

评论

还没有评论。

登录后即可发表评论