虚拟机创建流程 – libvirt篇

1. 虚拟机创建流程 – libvirt篇

 
  • 1. libvirt的架构
    • 1.1. 基本架构图
    • 1.2. 接口调用方式
  • 2. 从nova到libvirt
  • 3. libvirt的接口调用流程
  • 4. qemuProcessStart()启动qemu进程
  • 5. 参考

 

 

 

libvirt是CS架构应用,用户通过client与server交互,server与client通过socket连接通信。

 

基本架构图如下所示:

2019-11-23-11-01-45.png

  • libvirt分为clientdeamon两个部分
  • libvirt deamon中还包含了rpcacl事件机制线程池等公共组件。
    • 基于rpc可以实现libvirt remote client对本地虚拟机的操作。
    • acl实现了访问控制标签
    • 事件机制是libvirt所有动作的基础,所有的请求,消息转发,事件触发都是通过事件机制传递的。
  • libvirt deamon中通过事件机制监听某个端口的消息。client发出的请求会通过socket连接发送到libvirt api
  • libvirt deamon在启动时会加载部署的hypervisor驱动,libvirt api接收到的请求会路由到conn对象指定的驱动程序中。
  • 驱动程序接收到转发的请求之后会与hypervisor交互实现对虚拟机的具体操作。
  • libvirt中目前实现了多种hypervisor的驱动,其中qemu_driver对应kvmlxc对应容器
  • 对于kvm而言,一个虚拟机对应一个qemu进程。qemu进程通过软件模拟计算机的主板CPU南北桥内存设备。虚拟机操作系统就运行在qemu进程内。
  • libvirt独立实现了lxc driver来管理容器。lxc driver启动一个独立的进程并使用这个进程拉起一个init子进程,这个子进程有其独立的namespace并与cgroup结合实现了容器资源的隔离和限制。

 

在libvirt中接口的调用方式分为两种:

远程调用

2019-11-23-11-29-42.png

 

 

本地调用

2019-11-23-11-35-34.png

 

openstack是基于Python实现的,而libvirt是基于C实现的。那么C和Python之间是如何转换的呢。

下面以启动虚拟机实例来看一下在openstack中如何调用libvirt接口:

  1. import python-libvirt库
1
2
if libvirt is None:
    libvirt = __import__('libvirt')
  1. 通过openAuth获取与libvirtd进程的连接conn
1
2
3
4
return tpool.proxy_call(
    (libvirt.virDomain, libvirt.virConnect),
    libvirt.openAuth, uri, auth, flags)
)

 

 

  1. 调用define接口创建一个虚拟机实例, 获取domain对象
1
domain = self._conn.defineXML(xml)
  1. 通过domain对象启动虚拟机实例
1
domain.createWithFlags(launch_flags)

由这个流程我们可以看到,openstack中主要通过与libvirtd进程交互,完成对虚拟机实例的操作。

libvirt-python是由libvirt提供的一个面向python client的连接组件,包含以下内容:

1
2
3
4
5
6
/usr/share/pyshared/libvirt.py #libvirt python接口文件,包含大部分的libvirt接口
/usr/share/pyshared/libvirt_lxc.py #lxc接口文件,因为这部分接口参数不能自动转换,所以通过手动重写完成转换
/usr/share/pyshared/libvirt_qemu.py #与上面的类似,qemu相关的。
/usr/lib/python2.7/dist-packages/libvirtmod_qemu.so
/usr/lib/python2.7/dist-packages/libvirtmod_lxc.so
/usr/lib/python2.7/dist-packages/libvirtmod.so

libvirt代码中有一个专门的目录用于存放接口python化相关的代码。

所有的libvirt接口被分为了两个部分:

  1. 可以直接自动转换的接口,使用generator.py直接封装python接口
  2. 无法直接自动转换的接口,通过libvirt-override.c等文件对C接口做一层封装再封装python接口。

 

 

libvirt-python工程会将未重写和重写过的接口编译到一个动态库中,并且和生成的py文件一起打包到python-libvirt包中。然后我们就可以通过引入这个python库的方式调用libvirt的C接口了。

 

下面继续以创建虚拟机为例说明libvirt中接口调用的流程

  1. libvirt中接收xml格式定义的虚拟机实例配置,nova通过defineXML接口定义虚拟机。该接口返回一个虚拟机的domain对象,用户接下来可以通过这个对象操作虚拟机。
1
domain = self._conn.defineXML(xml)
  1. 第一步只是执行了定义操作,相当于libvirt开始管理这台虚拟机。但是此时实际的虚拟机还没有运行,用户还无法使用。nova中调用domain.createWithFlags(launch_flags)接口,用第一步中定义的虚拟机规格在hypervisor层把虚拟机真正创建起来。

  2. createWithFlags调用libvirt-python封装的virDomainCreateWithFlags

1
2
3
4
def createWithFlags(self, flags=0):
        ret = libvirtmod.virDomainCreateWithFlags(self._o, flags)
        if ret == -1: raise libvirtError ('virDomainCreateWithFlags() failed', dom=self)
        return ret
  1. 在libvirt-python中,createWithFlags接口是直接封装的,参数不需要转换。下一步会在转换中调用到libvirt.c中的virDomainCreateWithFlags接口,由此进入libvirt api层。 传入的flag值为0,flag取值范围及对应含义如下:
1
2
3
4
5
VIR_DOMAIN_NONE               = 0,      /* Default behavior */
VIR_DOMAIN_START_PAUSED       = 1 << 0, /* Launch guest in paused state */
VIR_DOMAIN_START_AUTODESTROY  = 1 << 1, /* Automatically kill guest when virConnectPtr is closed */
VIR_DOMAIN_START_BYPASS_CACHE = 1 << 2, /* Avoid file system cache pollution */
VIR_DOMAIN_START_FORCE_BOOT   = 1 << 3, /* Boot, discarding any managed save */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int
virDomainCreateWithFlags(virDomainPtr domain, unsigned int flags) {
    virConnectPtr conn;

    VIR_DOMAIN_DEBUG(domain, "flags=%x", flags);

    virResetLastError();#重置错误码。
    #libvirt中采用了线程池机制,每次从线程池中取出一个线程执行当前的请求。
    #线程中会保存当前线程最后产生的错误码,因此在请求最开始的位置就要把原有的错误重置,防止误报。

    #合法性检查,传入的domain指针及其中的conn指针是否为正确的类型。
    if (!VIR_IS_CONNECTED_DOMAIN(domain)) {
        virLibDomainError(VIR_ERR_INVALID_DOMAIN, __FUNCTION__);
        virDispatchError(NULL);
        return -1;
    }
    #获取domain中的conn指针,如果conn是只读的,则设置错误码并直接退出。因为创建虚拟机属于修改操作。
    conn = domain->conn;
    if (conn->flags & VIR_CONNECT_RO) {
        virLibDomainError(VIR_ERR_OPERATION_DENIED, __FUNCTION__);
        goto error;
    }
    #从这里跳转到具体的driver中执行。驱动在libvirtd启动的时候加载,映射关系由conn指针初始化的时候指定。在配置文件中可以配置默认的conn driver,也可以在创建conn的时候通过接口参数指定。
    if (conn->driver->domainCreateWithFlags) {
        int ret;
        ret = conn->driver->domainCreateWithFlags(domain, flags);
        if (ret < 0)
            goto error;
        return ret;
    }
    #如果驱动中没有实现对应的方法,直接报no support错误。
    virLibConnError(VIR_ERR_NO_SUPPORT, __FUNCTION__);

error:
    virDispatchError(domain->conn);
    return -1;
}
  1. libvirt中每一个driver都有一张映射关系表,用于对应driver中的函数指针和具体的driver函数。第4步中从api映射到了具体的driver。在qemu_driver.c中查找该函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static int
qemuDomainCreateWithFlags(virDomainPtr dom, unsigned int flags)
{
    virQEMUDriverPtr driver = dom->conn->privateData;
    virDomainObjPtr vm;
    int ret = -1;
    #首先检查传入flag参数的合法性,必须是上面提到的几个可选值之一。这里是一个宏来实现的,如果出错直接返回-1。
    virCheckFlags(VIR_DOMAIN_START_PAUSED |
                  VIR_DOMAIN_START_AUTODESTROY |
                  VIR_DOMAIN_START_BYPASS_CACHE |
                  VIR_DOMAIN_START_FORCE_BOOT, -1);
    #获取虚拟机的vm指针
    if (!(vm = qemuDomObjFromDomain(dom)))
        return -1;
    #访问控制,判断当前conn是否有权限执行该操作。目前配置的访问控制标签默认为None,即所有用户都有最高权限。
    if (virDomainCreateWithFlagsEnsureACL(dom->conn, vm->def) < 0)
        goto cleanup;
    #获取虚拟机job锁,类型为MODIFY,可选类型如后所示。只有获得该锁才能继续执行。
    if (qemuDomainObjBeginJob(driver, vm, QEMU_JOB_MODIFY) < 0)
        goto cleanup;
    #检查虚拟机是否已经处于运行状态
    if (virDomainObjIsActive(vm)) {
        virReportError(VIR_ERR_OPERATION_INVALID,
                       "%s", _("domain is already running"));
        goto endjob;
    }
    #启动虚拟机
    if (qemuDomainObjStart(dom->conn, driver, vm, flags) < 0)
        goto endjob;

    ret = 0;

endjob:
    #该job是同步操作,任务结束之后要释放job锁。
    if (!qemuDomainObjEndJob(driver, vm))
        vm = NULL;

cleanup:
    if (vm)
        virObjectUnlock(vm);
    return ret;
}

qemu job的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
QEMU_JOB_NONE = 0,  /* Always set to 0 for easy if (jobActive) conditions */
QEMU_JOB_QUERY,         /* Doesn't change any state */
QEMU_JOB_DESTROY,       /* Destroys the domain (cannot be masked out) */
QEMU_JOB_SUSPEND,       /* Suspends (stops vCPUs) the domain */
QEMU_JOB_MODIFY,        /* May change state */
QEMU_JOB_ABORT,         /* Abort current async job */
QEMU_JOB_MIGRATION_OP,  /* Operation influencing outgoing migration */

/* The following two items must always be the last items before JOB_LAST */
QEMU_JOB_ASYNC,         /* Asynchronous job */
QEMU_JOB_ASYNC_NESTED,  /* Normal job within an async job */

QEMU_JOB_LAST
};
  1. 第5步中调用到了qemuDomainObjStart,这个函数处理了虚拟机wakeup的逻辑并且在虚拟机启动成功之后发送事件通知。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
static int
qemuDomainObjStart(virConnectPtr conn, virQEMUDriverPtr driver, virDomainObjPtr vm, unsigned int flags)
{
    int ret = -1;
    char *managed_save;
    #根据传入的参数确定虚拟机的启动模式
    bool start_paused = (flags & VIR_DOMAIN_START_PAUSED) != 0;
    bool autodestroy = (flags & VIR_DOMAIN_START_AUTODESTROY) != 0;
    bool bypass_cache = (flags & VIR_DOMAIN_START_BYPASS_CACHE) != 0;
    bool force_boot = (flags & VIR_DOMAIN_START_FORCE_BOOT) != 0;
    unsigned int start_flags = VIR_QEMU_PROCESS_START_COLD;

    start_flags |= start_paused ? VIR_QEMU_PROCESS_START_PAUSED : 0;
    start_flags |= autodestroy ? VIR_QEMU_PROCESS_START_AUTODESTROY : 0;

    #组装hibernate文件的路径
    managed_save = qemuDomainManagedSavePath(driver, vm);

    if (!managed_save)
        goto cleanup;
    #如果存在hibernate文件,则从该文件恢复虚拟机
    if (virFileExists(managed_save)) {
        #启动时可以指定强制启动,此时移除hibernate文件并按照正常流程启动虚拟机
        if (force_boot) {
            if (unlink(managed_save) < 0) {
                virReportSystemError(errno,
                                     _("cannot remove managed save file %s"),
                                     managed_save);
                goto cleanup;
            }
            vm->hasManagedSave = false;
        } else {
        #从hibernate文件恢复虚拟机,因为我们目前还不支持内存快照的功能,暂时不跟进了。
            ret = qemuDomainObjRestore(conn, driver, vm, managed_save,
                                       start_paused, bypass_cache);
            #恢复成功,移除suspend文件
            if (ret == 0) {
                if (unlink(managed_save) < 0)
                    VIR_WARN("Failed to remove the managed state %s", managed_save);
                else
                    vm->hasManagedSave = false;
            }
            #如果恢复失败,则忽略suspend文件直接按正常流程启动虚拟机
            if (ret > 0)
                VIR_WARN("Ignoring incomplete managed state %s", managed_save);
            else
                goto cleanup;
        }
    }
    #启动qemu进程
    ret = qemuProcessStart(conn, driver, vm, NULL, -1, NULL, NULL,
                           VIR_NETDEV_VPORT_PROFILE_OP_CREATE, start_flags);
    #虚拟机启动完成之后,验证对应的启动参数,并且在/var/run/libvirt/qemu目录下保存一份运行状态的配置文件,这个文件的内容在虚拟机配置改变的时候会随之改变,
    virDomainAuditStart(vm, "booted", ret >= 0);
    if (ret >= 0) {
        #向事件队列发送虚拟机启动事件。如果此时有程序在监听此事件就会收到相应的通知。
        virDomainEventPtr event =
            virDomainEventNewFromObj(vm,
                                     VIR_DOMAIN_EVENT_STARTED,
                                     VIR_DOMAIN_EVENT_STARTED_BOOTED);
        if (event) {
            qemuDomainEventQueue(driver, event);
            #如果指定了启动之后pause虚拟机,同时还要发送一个虚拟机pause事件。
            if (start_paused) {
                event = virDomainEventNewFromObj(vm,
                                                 VIR_DOMAIN_EVENT_SUSPENDED,
                                                 VIR_DOMAIN_EVENT_SUSPENDED_PAUSED);
                if (event)
                    qemuDomainEventQueue(driver, event);
            }
        }
    }

cleanup:
    VIR_FREE(managed_save);
    return ret;
}
  1. 接下来我们来分析一下qemuProcessStart函数,这个函数处理qemu进程启动的主逻辑流程。由于这个函数中逻辑比较长,就不直接贴代码了,只选取其中关键部分了解一下。

 

qemu/qemu_process.c

首先,老规矩检查输入参数。

1
2
3
virCheckFlags(VIR_QEMU_PROCESS_START_COLD |
                  VIR_QEMU_PROCESS_START_PAUSED |
                  VIR_QEMU_PROCESS_START_AUTODESTROY, -1);

再次检查虚拟机是否处于运行状态。在api中检查的时候并未持有job锁,虚拟机可能正在执行启动操作。在拿到虚拟机job锁后做最后一次检查,如果没有启动则可以保证在本次启动过程中不会有其他的启动操作了。

1
2
3
4
5
6
if (virDomainObjIsActive(vm)) {
        virReportError(VIR_ERR_OPERATION_INVALID,
                       "%s", _("VM is already active"));
        virObjectUnref(cfg);
        return -1;
    }

复制xml文件下发的配置,作为虚拟机的在线配置

在libvirt中,虚拟机配置分为在线配置离线配置两种。

  • 在线配置记录在内存中,与虚拟机实时状态保持一致(比如执行网卡热插拔之后,在线配置也会同步更新)。
  • 离线配置则作为一个持久化配置记录在宿主机磁盘上,虚拟机关机之后仍然存在直到虚拟机被undefine下一次启动的时候使用该配置。离线插拔设备等操作会更新离线配置信息,虚拟机关机的时候也会把在线配置更新到离线配置中。
1
2
if (virDomainObjSetDefTransient(caps, driver->xmlopt, vm, true) < 0)
        goto cleanup;

获取虚拟机vm-id。这个ID与nova中的instance uuid不是一回事,仅有运行状态的虚拟机有这个ID。宿主机唯一,宿主机重启之后会重新计算。

1
vm->def->id = qemuDriverAllocateID(driver);

设置虚拟机的fakereboot标志位,正常reboot虚拟机的时候,qemu进程会被kill掉并重新启动。而如果fakereboot被设置为true时,只是重置当前qemu进程。

1
qemuDomainSetFakeReboot(driver, vm, false);

设置虚拟机状态。libvirt中有一套虚拟机状态管理机制,分为stat和reason。并提供了相应的查询接口,可以查询虚拟机当前状态以及进入当前状态的原因。

1
virDomainObjSetState(vm, VIR_DOMAIN_SHUTOFF, VIR_DOMAIN_SHUTOFF_UNKNOWN);

执行hook脚本。libvirt提供了hook机制,允许用户在某些事件发生时执行预先自定义的脚本文件。目前我们的默认配置均为空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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_PREPARE, VIR_HOOK_SUBOP_BEGIN,
                              NULL, xml, NULL);
        VIR_FREE(xml);

        /*
         * If the script raised an error abort the launch
         */
        if (hookret < 0)
            goto cleanup;
    }

获取宿主机上安装的qemu支持的特性列表,用于后续对虚拟机执行某些操作时判断兼容性。

1
2
3
if (!(priv->qemuCaps = virQEMUCapsCacheLookupCopy(driver->qemuCapsCache,
                                                      vm->def->emulator)))
        goto cleanup;

 

 

预处理配置文件中的虚拟设备。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#处理配置文件中的直通网卡。虽然在配置文件中指定设备类型为interface,但是实际上直通网卡还是一个PCI设备,因此将其加入hostdev设备中。
    if (qemuNetworkPrepareDevices(vm->def) < 0)
       goto cleanup;
    #处理直通设备。直通设备分为三类:PCI设备,USB设备及scsi设备。
    #PCI设备的处理逻辑比较复杂,大致流程为
    # - 检查配置的PCI设备是否已经直通到其他虚拟机
    # - 移除这些设备的原有驱动
    # - 重置这些设备
    # - 对于SRIOV的网卡直通设备,需要额外设置一些网络相关的参数
    # - 在qemu驱动中将这些设备设置为active状态
    # - 在qemu驱动的未启用设备列表中移除这些设备
    # - 在qemu驱动中记录当前使用这些设备的虚拟机
    # - 记录这些设备的原始状态
    # - 从host上隐藏这些设备
    #经过以上处理之后,配置的PCI设备就可以作为一个普通的虚拟机设备供虚拟机使用了。
    #对于USB直通设备不需要这么复杂,只要确保设备存在并且在qemu驱动中记录使用这些设备的虚拟机。
    #
    if (qemuPrepareHostDevices(driver, vm->def, priv->qemuCaps,
                               !migrateFrom) < 0)
        goto cleanup;
    #处理字符设备,包括serial,parallels,channel,console等设备类型,主要是检查这些设备是否存在
    if (virDomainChrDefForeach(vm->def,
                               true,
                               qemuProcessPrepareChardevDevice,
                               NULL) < 0)
        goto cleanup;

安全相关的,这块没有接触过。

移除原有的cgroup目录

1
qemuRemoveCgroup(vm);

初始化图形设备 vnc/spice,我们现在主要使用的是vnc。根据配置分配vnc端口。

创建虚拟机日志文件/var/log/libvirtd/qemu/虚拟机名称.log

1
2
3
4
5
6
7
8
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设备文件是否存在

1
2
3
4
5
6
7
8
9
10
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数量

1
2
    if (!qemuValidateCpuMax(vm->def, priv->qemuCaps))
        goto cleanup;

为所有的设备分配别名

1
2
3
    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配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#如果配置文件中指定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通信协议进行。

1
2
3
4
5
6
7
8
    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。

1
2
3
4
5
6
7
8
9
10
11
12
13
    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高级电源管理几个设备都是集成在同一个插槽上的)

1
2
3
4
5
    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命令的命令行组装起来了

1
2
3
4
5
    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脚本

1
2
3
4
5
6
7
8
9
10
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命令行)

1
2
3
4
5
6
7
8
9
10
11
12
13
    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);

向日志文件中写入一些告警信息(主要是一些有危险的配置告警,没有什么影响)

1
qemuDomainObjCheckTaint(driver, vm, logfile);

记录qemu日志文件的最后位置,后面会用到

1
2
3
    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设置一些标志位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    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。)

1
    ret = virCommandRun(cmd, NULL);

libvirt通过fork函数启动qemu进程。fork执行完毕之后要判断qemu进程是否正常拉起。

1
2
3
4
5
6
7
8
9
10
11
12
13
    #通过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);
    }

保存虚拟机在线配置

1
2
3
    if (virDomainSaveStatus(driver->xmlopt, cfg->stateDir, vm) < 0) {
        goto cleanup;
    }

 

 

监听之前创建的握手socket,等待qemu进程发出的握手信号

1
2
3
    if (virCommandHandshakeWait(cmd) < 0) {
        goto cleanup;
    }

收到握手信号之后表明qemu进程已经启动完成,接下来可以设置该进程的cgroup参数。

1
2
3
4
5
6
7
8
9
10
    #首先要初始化当前虚拟机的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进程的。

1
2
3
    if (!vm->def->cputune.emulatorpin &&
        qemuProcessInitCpuAffinity(driver, vm, nodemask) < 0)
        goto cleanup;

完成上面的配置之后,qemu进程已经可以继续运行了。通过上面创建的握手socket连接通知qemu进程继续运行。如果在设置cgroup参数之前qemu进程就开始运行,可能会导致qemu进程占用内存过多被kill掉。

1
2
3
    if (virCommandHandshakeNotify(cmd) < 0) {
        goto cleanup;
    }

如果当前启动是热迁移目的端启动的虚拟机,在启动之后要等待源端拷贝内存,因此启动之后CPU不能直接运行,要设置虚拟机状态为pause。

1
2
    if (migrateFrom)
        flags |= VIR_QEMU_PROCESS_START_PAUSED;

连接qemu monitor。在前面的步骤中,只是初始化了libvirt中记录的qemu monitor信息,真正的socket创建是在qemu中,libvirtd在这里等待创建并连接。

1
2
    if (qemuProcessWaitForMonitor(driver, vm, priv->qemuCaps, pos) < 0)
        goto cleanup;

连接qemu guest agent。如果define虚拟机的配置中包含qemu-ga的配置,qemu进程会模拟一个串口设备,并将串口设备的输出定位到配置指定的socket文件中。这里就是与socket文件建立连接。

1
2
3
4
5
6
    if (qemuConnectAgent(driver, vm) < 0) {
        VIR_WARN("Cannot connect to QEMU guest agent for %s",
                 vm->def->name);
        virResetLastError();
        priv->agentError = true;
    }

在libvirt的配置中,有两处可以设置虚拟机的CPU绑定关系,分别是

1
<vcpu placement='static' cpuset="1-4,^3,6" current="1">2</vcpu>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  <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的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    #因为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磁盘设备。

1
2
    if (qemuProcessInitPasswords(conn, driver, vm) < 0)
        goto cleanup;

如果qemu中有一些设备,在libvirt中没有自动分配pci插槽号。在这里libvirt通过qemu monitor获取qemu中所有的设备列表,并补齐所有的设备插槽号。

1
2
3
4
5
    if (!virQEMUCapsGet(priv->qemuCaps, QEMU_CAPS_DEVICE)) {
        VIR_DEBUG("Determining domain device PCI addresses");
        if (qemuProcessInitPCIAddresses(driver, vm) < 0)
            goto cleanup;
    }

设置网卡的默认连接状态

1
2
3
4
5
6
7
    qemuDomainObjEnterMonitor(driver, vm);
    if (qemuProcessSetLinkStates(vm) < 0) {
        qemuDomainObjExitMonitor(driver, vm);
        goto cleanup;
    }

    qemuDomainObjExitMonitor(driver, vm);

获取qemu中所有的设备列表

1
2
    if (qemuDomainUpdateDeviceList(driver, vm) < 0)
        goto cleanup;

设置内存balloon参数。balloon的操作是在qemu中实现的,libvirtd在这里只是通过qmp协议设置balloon的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    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);
    }
posted @ 2022-11-04 10:53  aaron_agu  阅读(2251)  评论(0编辑  收藏  举报