- 移除原有的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脚本。