虚拟机创建流程-libvirt篇(下)

阿凡达2018-07-04 12:37
  • 移除原有的cgroup目录
qemuRemoveCgroup(vm);
  • 初始化图形设备 vnc/spice,我们现在主要使用的是vnc。根据配置分配vnc端口。
  • 创建虚拟机日志文件/var/log/libvirtd/qemu/虚拟机名称.log
if (virFileMakePath(cfg->logDir) < 0) {
        virReportSystemError(errno,
                             _("cannot create log directory %s"),
                             cfg->logDir);
        goto cleanup;
    }
    if ((logfile = qemuDomainCreateLog(driver, vm, false)) < 0)
        goto cleanup;
  • 检查宿主机是否支持kvm,判断条件为/dev/kvm设备文件是否存在
if (vm->def->virtType == VIR_DOMAIN_VIRT_KVM) {
        VIR_DEBUG("Checking for KVM availability");
        if (!virFileExists("/dev/kvm")) {
            virReportError(VIR_ERR_CONFIG_UNSUPPORTED, "%s",
                           _("Domain requires KVM, but it is not available. "
                             "Check that virtualization is enabled in the host BIOS, "
                             "and host configuration is setup to load the kvm modules."));
            goto cleanup;
        }
    }
  • 检查vcpu配置的合法性。配置的maxvcpus数量不能超过宿主机配置的最大vcpu数量
if (!qemuValidateCpuMax(vm->def, priv->qemuCaps))
        goto cleanup;
  • 为所有的设备分配别名
if (qemuAssignDeviceAliases(vm->def, priv->qemuCaps) < 0)
        goto cleanup;
    #分配的别名可以通过virsh dumpxml命令查看
  • 检查磁盘设备的后端文件是否存在
  • 设置numa配置。numa是CPU/内存亲和性的配置,宿主机的内存一般分配为两个numa node,每个numa node对应一个CPU socket。如果cpu访问的是对应numa node上的内存会带来性能提升。lscpu命令可以看到宿主机的numa配置
#如果配置文件中指定numa为自动模式,会从numad中获取自动分配的结果。
    if ((vm->def->placement_mode ==
         VIR_DOMAIN_CPU_PLACEMENT_MODE_AUTO) ||
        (vm->def->numatune.memory.placement_mode ==
         VIR_NUMA_TUNE_MEM_PLACEMENT_MODE_AUTO)) {
        nodeset = virNumaGetAutoPlacementAdvice(vm->def->vcpus,
                                                vm->def->mem.max_balloon);
        if (!nodeset)
            goto cleanup;

        VIR_DEBUG("Nodeset returned from numad: %s", nodeset);

        if (virBitmapParse(nodeset, 0, &nodemask,
                           VIR_DOMAIN_CPUMASK_LEN) < 0)
            goto cleanup;
    }
    #在hook中记录对应的numa配置
    hookData.nodemask = nodemask;
  • 设置qemu monitor。qemu monitor是libvirtd与qemu之间的socket通信管道,libvirt对qemu的操作,qemu进程的状态监控等都要通过这个管道使用qmp通信协议进行。
if (VIR_ALLOC(priv->monConfig) < 0)
        goto cleanup;
    if (qemuProcessPrepareMonitorChr(cfg, priv->monConfig, vm->def->name) < 0)
        goto cleanup;
    priv->monJSON = virQEMUCapsGet(priv->qemuCaps, QEMU_CAPS_MONITOR_JSON);
    priv->monError = false;
    priv->monStart = 0;
    priv->gotShutdown = false;
  • 配置当前虚拟机的pidfile,这个文件用于检测虚拟机是否正在运行。注意,此处并没有真正创建该文件,qemu进程还未拉起,无法获取qemu进程pid。
VIR_FREE(priv->pidfile);
    if (!(priv->pidfile = virPidFileBuildPath(cfg->stateDir, vm->def->name))) {
        virReportSystemError(errno,
                             "%s", _("Failed to build pidfile path."));
        goto cleanup;
    }
    if (unlink(priv->pidfile) < 0 &&
        errno != ENOENT) {
        virReportSystemError(errno,
                             _("Cannot remove stale PID file %s"),
                             priv->pidfile);
        goto cleanup;
    }
  • 为pci设备分配插槽号。正常情况下,在虚拟机define之后就已经完成了插槽号的分配。此处再分配一次的目的是为了解决一些升级的问题,并为热插操作预留插槽。PCI是计算机中的总线设备,用于连接外围设备与CPU。默认每个PCI设备支持连接32个外围设备并且支持PCI设备的桥接。libvirt目前仅支持在pci root设备上做pci桥接,不支持pci设备的多级级联。设备插槽号包括bus,slot和function三个层级。bus表示在第几个pci总线设备,slot表示在当前pci总线的第几个槽位,function表示是当前槽位设备上的第几个function设备。(多function设备,在一个插槽上可以集成多个功能设备,在kvm虚拟机里面最典型的就是ISA总线,IDE控制器,USB控制器和ACPI高级电源管理几个设备都是集成在同一个插槽上的)
if (virQEMUCapsGet(priv->qemuCaps, QEMU_CAPS_DEVICE)) {
        VIR_DEBUG("Assigning domain PCI addresses");
        if ((qemuDomainAssignAddresses(vm->def, priv->qemuCaps, vm)) < 0)
            goto cleanup;
    }
  • 组装qemu命令。经过上面的步骤以后,可以根据配置文件把qemu命令的命令行组装起来了
if (!(cmd = qemuBuildCommandLine(conn, driver, vm->def, priv->monConfig,
                                     priv->monJSON, priv->qemuCaps,
                                     migrateFrom, stdin_fd, snapshot, vmop,
                                     &buildCommandLineCallbacks)))
        goto cleanup;
  • qemu命令组装完成之后就可以开始运行qemu进程了,此时需要先触发qemu启动事件的hook脚本
if (virHookPresent(VIR_HOOK_DRIVER_QEMU)) {
        char *xml = qemuDomainDefFormatXML(driver, vm->def, 0);
        int hookret;
        hookret = virHookCall(VIR_HOOK_DRIVER_QEMU, vm->def->name,
                              VIR_HOOK_QEMU_OP_START, VIR_HOOK_SUBOP_BEGIN,
                              NULL, xml, NULL);
        VIR_FREE(xml);
        if (hookret < 0)
            goto cleanup;
    }
  • 向qemu日志中写入启动日志(时间和qemu command命令行)
if ((timestamp = virTimeStringNow()) == NULL) {
        goto cleanup;
    } else {
        if (safewrite(logfile, timestamp, strlen(timestamp)) < 0 ||
            safewrite(logfile, START_POSTFIX, strlen(START_POSTFIX)) < 0) {
            VIR_WARN("Unable to write timestamp to logfile: %s",
                     virStrerror(errno, ebuf, sizeof(ebuf)));
        }

        VIR_FREE(timestamp);
    }

    virCommandWriteArgLog(cmd, logfile);
  • 向日志文件中写入一些告警信息(主要是一些有危险的配置告警,没有什么影响)
qemuDomainObjCheckTaint(driver, vm, logfile);
  • 记录qemu日志文件的最后位置,后面会用到
if ((pos = lseek(logfile, 0, SEEK_END)) < 0)
        VIR_WARN("Unable to seek to end of logfile: %s",
                 virStrerror(errno, ebuf, sizeof(ebuf)));
  • 为qemu cmd设置一些标志位
virCommandSetPreExecHook(cmd, qemuProcessHook, &hookData);
    virCommandSetMaxProcesses(cmd, cfg->maxProcesses);
    virCommandSetMaxFiles(cmd, cfg->maxFiles);

    VIR_DEBUG("Setting up security labelling");
    if (virSecurityManagerSetChildProcessLabel(driver->securityManager,
                                               vm->def, cmd) < 0) {
        goto cleanup;
    }
    #qemu的标准输出定向到日志文件
    virCommandSetOutputFD(cmd, &logfile);
    #qemu错误输出定向到日志文件
    virCommandSetErrorFD(cmd, &logfile);
    virCommandNonblockingFDs(cmd);
    virCommandSetPidFile(cmd, priv->pidfile);
    virCommandDaemonize(cmd);
    #创建一个握手连接,用于qemu和libvirt之间通信。可以确保hook的执行时间可以由libvirtd控制。当qemu进程启动,但是还未完成的时候,libvirtd没有通过这个连接发送信号,qemu的hook不会执行。qemu进程启动完成之后,libvirtd检测到并且发送信号,这时候才去执行qemu的hook脚本。
    virCommandRequireHandshake(cmd);
  • 启动qemu进程(到这里终于真的启动了qemu进程,qemu根据传入的参数创建各种设备,创建vcpu线程,申请内存,这些操作完成之后相当于硬件准备完成,主板发送上电信号,引导主板上的bios程序并进一步引导磁盘设备上的bootloader。)
ret = virCommandRun(cmd, NULL);
  • libvirt通过fork函数启动qemu进程。fork执行完毕之后要判断qemu进程是否正常拉起。
#通过fork返回值和pid file内容判断
    if (ret == 0) {
        if (virPidFileReadPath(priv->pidfile, &vm->pid) < 0) {
            virReportError(VIR_ERR_INTERNAL_ERROR,
                           _("Domain %s didn't show up"), vm->def->name);
            ret = -1;
        }
        VIR_DEBUG("QEMU vm=%p name=%s running with pid=%llu",
                  vm, vm->def->name, (unsigned long long)vm->pid);
    } else {
        VIR_DEBUG("QEMU vm=%p name=%s failed to spawn",
                  vm, vm->def->name);
    }
  • 保存虚拟机在线配置
if (virDomainSaveStatus(driver->xmlopt, cfg->stateDir, vm) < 0) {
        goto cleanup;
    }
  • 监听之前创建的握手socket,等待qemu进程发出的握手信号
if (virCommandHandshakeWait(cmd) < 0) {
        goto cleanup;
    }
  • 收到握手信号之后表明qemu进程已经启动完成,接下来可以设置该进程的cgroup参数。
#首先要初始化当前虚拟机的cgroup目录,在每一个cgroup子系统的machine层级下创建虚拟机对应的层级。
    #device子系统,设置当前虚拟机可以访问的设备号。
    #blkio子系统,设置磁盘qos参数。
    #memory子系统,设置内存qos参数,这个目前暂时没有配置。
    #cpu子系统,设置cpu qos参数。只是设置其中的share参数,即CPU权重,同样VCPU数量的前提下,权重越大,获得的CPU时间越多。
    #cpuset子系统的设置项较多,包括:
    #如果配置文件中指定了numatune配置,则使用指定的参数。如果没有指定,则使用默认生成的推荐参数。
    #如果配置文件中指定CPU绑定方式为auto,则会根据默认生成的numa配置参数配置相应的CPU绑定关系。如果指定了CPU绑定关系,则按照指定的绑定关系配置。
    if (qemuSetupCgroup(driver, vm, nodemask) < 0)
        goto cleanup;
  • 通过taskset命令直接指定qemu进程的CPU亲和性。要注意的是这里的设置是针对整个qemu进程的。
if (!vm->def->cputune.emulatorpin &&
        qemuProcessInitCpuAffinity(driver, vm, nodemask) < 0)
        goto cleanup;
  • 完成上面的配置之后,qemu进程已经可以继续运行了。通过上面创建的握手socket连接通知qemu进程继续运行。如果在设置cgroup参数之前qemu进程就开始运行,可能会导致qemu进程占用内存过多被kill掉。
if (virCommandHandshakeNotify(cmd) < 0) {
        goto cleanup;
    }
  • 如果当前启动是热迁移目的端启动的虚拟机,在启动之后要等待源端拷贝内存,因此启动之后CPU不能直接运行,要设置虚拟机状态为pause。
if (migrateFrom)
        flags |= VIR_QEMU_PROCESS_START_PAUSED;
  • 连接qemu monitor。在前面的步骤中,只是初始化了libvirt中记录的qemu monitor信息,真正的socket创建是在qemu中,libvirtd在这里等待创建并连接。
if (qemuProcessWaitForMonitor(driver, vm, priv->qemuCaps, pos) < 0)
        goto cleanup;
  • 连接qemu guest agent。如果define虚拟机的配置中包含qemu-ga的配置,qemu进程会模拟一个串口设备,并将串口设备的输出定位到配置指定的socket文件中。这里就是与socket文件建立连接。
if (qemuConnectAgent(driver, vm) < 0) {
        VIR_WARN("Cannot connect to QEMU guest agent for %s",
                 vm->def->name);
        virResetLastError();
        priv->agentError = true;
    }
  • 在libvirt的配置中,有两处可以设置虚拟机的CPU绑定关系,分别是
<vcpu placement='static' cpuset="1-4,^3,6" current="1">2</vcpu>

<cputune>
    <vcpupin vcpu="0" cpuset="1-4,^2"/>
    <vcpupin vcpu="1" cpuset="0,1"/>
    <vcpupin vcpu="2" cpuset="2,3"/>
    <vcpupin vcpu="3" cpuset="0,4"/>
    <emulatorpin cpuset="1-3"/>
    <iothreadpin iothread="1" cpuset="5,6"/>
    <iothreadpin iothread="2" cpuset="7,8"/>
    <shares>2048</shares>
    <period>1000000</period>
    <quota>-1</quota>
    <emulator_period>1000000</emulator_period>
    <emulator_quota>-1</emulator_quota>
    <iothread_period>1000000</iothread_period>
    <iothread_quota>-1</iothread_quota>
    <vcpusched vcpus='0-4,^3' scheduler='fifo' priority='1'/>
    <iothreadsched iothreads='2' scheduler='batch'/>
  </cputune>

上面我们已经根据vcpu的placement设置过一次亲和性,那一次是设置整个qemu进程的亲和性。libvirt同时还提供了更细粒度的设置方式cputune。libvirt的策略是两处同时指定的话,cputune会覆盖vcpu placement的配置。

#因为vcpu实际上是qemu进程中的线程,通过线程号来绑定vcpu的亲和性,所以需要先获取qemu中所有的线程号,包括emulator和vcpu。
    if (qemuProcessDetectVcpuPIDs(driver, vm) < 0)
        goto cleanup;
    #设置vcpu的pin,quota和period等参数
    if (qemuSetupCgroupForVcpu(vm) < 0)
        goto cleanup;
    #设置emulator的cputune参数
    if (qemuSetupCgroupForEmulator(driver, vm, nodemask) < 0)
        goto cleanup;
    #通过taskset设置vcpu线程和emulator的cpu亲和性。如果没有配置单独的vcpupin直接返回,否则按照vcpupin的配置设置线程亲和性。如果cputune中配置了emulatorpin信息优先使用此配置,否则尝试使用vcpu placement中的cpuset信息,如果都没有直接返回。
    #这两步设置不是很清楚具体的原因。个人理解是首先尝试设置cgroup,如果cgroup不存在则继续通过taskset设置。如果存在则设置两次。
    if (qemuProcessSetVcpuAffinities(conn, vm) < 0)
        goto cleanup;
    if (qemuProcessSetEmulatorAffinities(conn, vm) < 0)
        goto cleanup;
  • 设置密码,包括终端设备(vnc或者spice),qcow磁盘设备。
if (qemuProcessInitPasswords(conn, driver, vm) < 0)
        goto cleanup;
  • 如果qemu中有一些设备,在libvirt中没有自动分配pci插槽号。在这里libvirt通过qemu monitor获取qemu中所有的设备列表,并补齐所有的设备插槽号。
if (!virQEMUCapsGet(priv->qemuCaps, QEMU_CAPS_DEVICE)) {
        VIR_DEBUG("Determining domain device PCI addresses");
        if (qemuProcessInitPCIAddresses(driver, vm) < 0)
            goto cleanup;
    }
  • 设置网卡的默认连接状态
qemuDomainObjEnterMonitor(driver, vm);
    if (qemuProcessSetLinkStates(vm) < 0) {
        qemuDomainObjExitMonitor(driver, vm);
        goto cleanup;
    }

    qemuDomainObjExitMonitor(driver, vm);
  • 获取qemu中所有的设备列表
if (qemuDomainUpdateDeviceList(driver, vm) < 0)
        goto cleanup;
  • 设置内存balloon参数。balloon的操作是在qemu中实现的,libvirtd在这里只是通过qmp协议设置balloon的参数。
cur_balloon = vm->def->mem.cur_balloon;
    if (cur_balloon != vm->def->mem.cur_balloon) {
        virReportError(VIR_ERR_OVERFLOW,
                       _("unable to set balloon to %lld"),
                       vm->def->mem.cur_balloon);
        goto cleanup;
    }
    qemuDomainObjEnterMonitor(driver, vm);
    if (vm->def->memballoon && vm->def->memballoon->period)
        qemuMonitorSetMemoryStatsPeriod(priv->mon, vm->def->memballoon->period);
    if (qemuMonitorSetBalloon(priv->mon, cur_balloon) < 0) {
        qemuDomainObjExitMonitor(driver, vm);
        goto cleanup;
    }
    qemuDomainObjExitMonitor(driver, vm);
  • 如果没有指定虚拟机启动后pause,则开始执行qemu的vcpu线程,相当于硬件上电。并设置虚拟机状态为running,否则设置为pause。
if (!(flags & VIR_QEMU_PROCESS_START_PAUSED)) {
        if (qemuProcessStartCPUs(driver, vm, conn,
                                 VIR_DOMAIN_RUNNING_BOOTED,
                                 QEMU_ASYNC_JOB_NONE) < 0) {
            if (virGetLastError() == NULL)
                virReportError(VIR_ERR_INTERNAL_ERROR,
                               "%s", _("resume operation failed"));
            goto cleanup;
        }
    } else {
        virDomainObjSetState(vm, VIR_DOMAIN_PAUSED,
                             migrateFrom ?
                             VIR_DOMAIN_PAUSED_MIGRATION :
                             VIR_DOMAIN_PAUSED_USER);
    }
  • 如果指定了相关参数,设定qemu进程的autodestroy标志位。如果设定了autodestroy,在conn指针断开连接的时候会将这个虚拟机destroy掉。
  • 保存虚拟机的在线配置
if (virDomainSaveStatus(driver->xmlopt, cfg->stateDir, vm) < 0)
        goto cleanup;
  • 至此,虚拟机qemu进程已经启动并设置完成,虚拟机状态变为running。最后执行当前阶段注册的hook脚本。

相关阅读:虚拟机创建流程-libvirt篇(上)

本文来自网易实践者社区,经作者岳文远授权发布。