QEMU开源实战(四)

3,294 阅读11分钟

本周工作概述

  • 进入machine_run_board_init对函数行为有一个大概了解
  • 深入分析machine_run_board_init函数中第一步完成NUMA的配置,即numa_complete_configuration部分

machine 模块

void machine_run_board_init(MachineState *machine)

这是主函数中第一个调用的/hw/相关的函数。参数为MachineState类型的结构体,保存了目标虚拟的状态信息。

函数的基本内容形式如下:

void machine_run_board_init(MachineState *machine)
{
    MachineClass *machine_class = MACHINE_GET_CLASS(machine);

    if (machine_class->numa_mem_supported) {
        ...
    }

    if (machine_class->valid_cpu_types && machine->cpu_type) {
        ...
    }

    machine_class->init(machine);
}

可以看到,函数首先从machine中获取machine class信息:

MachineClass *machine_class = MACHINE_GET_CLASS(machine);

在QEMU中,MachineClass结构体代表一个虚拟机,对于PC,使用MachineClass的派生类PCMachineClass,定义在/hw/i386/pc.h中。

根据machine的不同,定义不同的PCMachineClass,如q35系列的PCMachineClas通过宏定义的方式定义在hw/i386/pc_q35.c中。函数获取machine_class利用MACHINE_GET_CLASS,也是通过宏定义方式定义了方法,之后再分析此处涉及到的QEMU中关于类的获取、构造方式。

QEMU支持的machine类型可以通过qemu-system -machine help指令查看:

Supported machines are:
pc-i440fx-zesty      Ubuntu 17.04 PC (i440FX + PIIX, 1996)
pc-i440fx-yakkety    Ubuntu 16.10 PC (i440FX + PIIX, 1996)
pc-i440fx-xenial     Ubuntu 16.04 PC (i440FX + PIIX, 1996)
pc-i440fx-wily       Ubuntu 15.04 PC (i440FX + PIIX, 1996)
pc-i440fx-trusty     Ubuntu 14.04 PC (i440FX + PIIX, 1996)
ubuntu               Ubuntu 18.04 PC (i440FX + PIIX, 1996) (alias of pc-i440fx-bionic)
pc-i440fx-bionic     Ubuntu 18.04 PC (i440FX + PIIX, 1996) (default)
pc-i440fx-bionic-hpb Ubuntu 18.04 PC (i440FX + PIIX, +host-phys-bits=true, 1996)
pc-i440fx-artful     Ubuntu 17.10 PC (i440FX + PIIX, 1996)
pc-i440fx-2.9        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.8        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.7        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.6        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.5        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.4        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.3        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.2        Standard PC (i440FX + PIIX, 1996)
pc                   Standard PC (i440FX + PIIX, 1996) (alias of pc-i440fx-2.11)
pc-i440fx-2.11       Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.10       Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.1        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-2.0        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-1.7        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-1.6        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-1.5        Standard PC (i440FX + PIIX, 1996)
pc-i440fx-1.4        Standard PC (i440FX + PIIX, 1996)
pc-1.3               Standard PC (i440FX + PIIX, 1996)
pc-1.2               Standard PC (i440FX + PIIX, 1996)
pc-1.1               Standard PC (i440FX + PIIX, 1996)
pc-1.0               Standard PC (i440FX + PIIX, 1996)
pc-0.15              Standard PC (i440FX + PIIX, 1996)
pc-0.14              Standard PC (i440FX + PIIX, 1996)
pc-0.13              Standard PC (i440FX + PIIX, 1996)
pc-0.12              Standard PC (i440FX + PIIX, 1996)
pc-0.11              Standard PC (i440FX + PIIX, 1996)
pc-0.10              Standard PC (i440FX + PIIX, 1996)
pc-q35-zesty         Ubuntu 17.04 PC (Q35 + ICH9, 2009)
pc-q35-yakkety       Ubuntu 16.10 PC (Q35 + ICH9, 2009)
pc-q35-xenial        Ubuntu 16.04 PC (Q35 + ICH9, 2009)
pc-q35-bionic        Ubuntu 18.04 PC (Q35 + ICH9, 2009)
pc-q35-bionic-hpb    Ubuntu 18.04 PC (Q35 + ICH9, +host-phys-bits=true, 2009)
pc-q35-artful        Ubuntu 17.10 PC (Q35 + ICH9, 2009)
pc-q35-2.9           Standard PC (Q35 + ICH9, 2009)
pc-q35-2.8           Standard PC (Q35 + ICH9, 2009)
pc-q35-2.7           Standard PC (Q35 + ICH9, 2009)
pc-q35-2.6           Standard PC (Q35 + ICH9, 2009)
pc-q35-2.5           Standard PC (Q35 + ICH9, 2009)
pc-q35-2.4           Standard PC (Q35 + ICH9, 2009)
q35                  Standard PC (Q35 + ICH9, 2009) (alias of pc-q35-2.11)
pc-q35-2.11          Standard PC (Q35 + ICH9, 2009)
pc-q35-2.10          Standard PC (Q35 + ICH9, 2009)
isapc                ISA-only PC
none                 empty machine
xenfv                Xen Fully-virtualized PC
xenpv                Xen Para-virtualized PC

可以在QEMU的参数中增加-machine参数来选择machine的类型(参考链接1参考链接2),如:

-machine pc-i440fx-2.11

或者

-machine type=q35,accel=kvm



在我的参数中,使用了q35machine

在确定MachineClass之后,machine_run_board_init做了两次条件检查,当满足特定条件时执行某些动作。最后调用对应目标虚拟机的初始化函数,完成对主板以及CPU基本外设的虚拟化,对于不同类型的主板有不同的初始化函数。

先看第一个条件检查,检查和numa相关的信息:

if (machine_class->numa_mem_supported) {
    numa_complete_configuration(machine);
    if (machine->numa_state->num_nodes) {
        machine_numa_finish_cpu_init(machine);
    }
}

回到代码中的判断,machine_class类有一个成员numa_mem_supported

@numa_mem_supported:

true if '--numa node.mem' option is supported and false otherwise

在我的调试信息中可以看到numa_mem_supportedTrue

进而完成numa_complete_configuration

numa模块

为测试NUMA,QEMU的启动配置设置如下:

-m
1024
-smp
cpus=4
-numa
node,cpus=0-1
-numa
node,cpus=2-3
--enable-kvm
-drive
if=virtio,file=test.qcow2,cache=none
-vnc
:1

void numa_complete_configuration(MachineState *ms)

什么是NUMA

这要先看一下numa的概念。

Non-Uniform Memory Access (NUMA) refers to multiprocessor systems whose memory is divided into multiple memory nodes. The access time of a memory node depends on the relative locations of the accessing CPU and the accessed node. (This contrasts with a symmetric multiprocessor system, where the access time for all of the memory is the same for all CPUs.) Normally, each CPU on a NUMA system has a local memory node whose contents can be accessed faster than the memory in the node local to another CPU or the memory on a bus shared by all CPUs.

简言之,这是一种内存架构,对于计算机的每个CPU,有一个local memory node,对于自身的local memory node的访存速度要快于non-local memory node

在上图中,不同的节点之间通过QPI访问remote memory的速度远低于节点通过IMC Bus访问自的local memory。故设置“距离”的概念,称为numa distance,距离越大,则node对于memory的访问速度越慢。

我们可以看一下本机NUMA架构信息:

ethan@Ethan-Laptop:~$ numactl --hardware
available: 1 nodes (0)
node 0 cpus: 0 1 2 3
node 0 size: 7856 MB
node 0 free: 288 MB
node distances:
node   0 
  0:  10 

然后发现我的电脑并没有配置多个NUMA节点……

更多参考资料

函数主体

下面分析NUMA模块中的函数。在machine_run_board_init中,若支持NUMA,则进入NUMA配置,函数如下:

/*qemu-4.2.0/hw/core/numa.c*/

void numa_complete_configuration(MachineState *ms)
{
    int i;
    MachineClass *mc = MACHINE_GET_CLASS(ms);
    NodeInfo *numa_info = ms->numa_state->nodes;

    if (ms->numa_state->num_nodes == 0 &&
        ((ms->ram_slots > 0 &&
        mc->auto_enable_numa_with_memhp) ||
        mc->auto_enable_numa)) {
            NumaNodeOptions node = { };
            parse_numa_node(ms, &node, &error_abort);
            numa_info[0].node_mem = ram_size;
    }

    assert(max_numa_nodeid <= MAX_NODES);

    for (i = max_numa_nodeid - 1; i >= 0; i--) {
        if (!numa_info[i].present) {
            error_report("numa: Node ID missing: %d", i);
            exit(1);
        }
    }

    assert(ms->numa_state->num_nodes == max_numa_nodeid);

    if (ms->numa_state->num_nodes > 0) {
        uint64_t numa_total;

        if (ms->numa_state->num_nodes > MAX_NODES) {
            ms->numa_state->num_nodes = MAX_NODES;
        }

        for (i = 0; i < ms->numa_state->num_nodes; i++) {
            if (numa_info[i].node_mem != 0) {
                break;
            }
        }
        if (i == ms->numa_state->num_nodes) {
            assert(mc->numa_auto_assign_ram);
            mc->numa_auto_assign_ram(mc, numa_info,
                                     ms->numa_state->num_nodes, ram_size);
            if (!qtest_enabled()) {
                warn_report("Default splitting of RAM between nodes is deprecated,"
                            " Use '-numa node,memdev' to explictly define RAM"
                            " allocation per node");
            }
        }

        numa_total = 0;
        for (i = 0; i < ms->numa_state->num_nodes; i++) {
            numa_total += numa_info[i].node_mem;
        }
        if (numa_total != ram_size) {
            error_report("total memory for NUMA nodes (0x%" PRIx64 ")"
                         " should equal RAM size (0x" RAM_ADDR_FMT ")",
                         numa_total, ram_size);
            exit(1);
        }

        if (ms->numa_state->have_numa_distance) {
            validate_numa_distance(ms);
            complete_init_numa_distance(ms);
        }
    }
}

获得NUMA信息

接收参数为一个MachineState结构体ms,前面说过,该结构体保存了目标虚拟机的状态信息。

之后获得MachineClass,由mc指针指向。之后获取ms->numa_state,我们看一下numa_state的结构体定义:

/*qemu-4.2.0/include/sysemu/numa.h*/

struct NumaState {
    /* Number of NUMA nodes */
    int num_nodes;

    /* Allow setting NUMA distance for different NUMA nodes */
    bool have_numa_distance;

    /* NUMA nodes information */
    NodeInfo nodes[MAX_NODES];
};

num_nodes:NUMA节点个数

have_numa_distance:是否为多个节点之间的内存访问设置numa distance

nodes[]:存储每个节点的信息,NodeInfo结构如下:

struct NodeInfo {
    uint64_t node_mem;
    struct HostMemoryBackend *node_memdev;
    bool present;
    uint8_t distance[MAX_NODES];
};

保存了节点的内存大小,在主机上内存映射关系,该节点是否启用,以及该节点其它各个节点之间的距离。

默认节点配置

回到numa_complete_configuration函数中,numa_info指向了目标虚拟机中保存节点信息的数组。

之后判断,当目标机器没有设置numa node并且开启了auto enable numa的时候,进行对numa的配置。这里的“开启了auto enable numa”包括两种情况:

  • 支持热插拔并且设置了在热插拔情况下auto enable numa with memory hotplug
  • 设置了auto enable numa

其中,对于是否支持热插拔,判断标准是ms->ram_slots > 0slot的含义是DIMM上插槽的数量。

DIMM全称Dual-Inline-Memory-Modules,中文名叫双列直插式存储模块,是指奔腾CPU推出后出现的新型内存条,它提供了64位的数据通道。

这部分代码如下:

if (ms->numa_state->num_nodes == 0 &&
    ((ms->ram_slots > 0 &&
    mc->auto_enable_numa_with_memhp) ||
    mc->auto_enable_numa)) {
        NumaNodeOptions node = { };
        parse_numa_node(ms, &node, &error_abort);
        numa_info[0].node_mem = ram_size;
    }

其中,ram_sizeqemu-4.2.0/tests/fw_cfg-test.c中定义:

static uint64_t ram_size = 128 << 20;

默认情况下是2^{27}bit, 在我的调试信息中设置的是1024MiB,即2^{30}bit。

鲁棒性检查

之后判断numa node数量是否大于最大支持的节点数量,若是,停止程序运行。

assert(max_numa_nodeid <= MAX_NODES);

这里注意一下assert函数的用法,是C提供的标准库函数,用于判断传递的条件是否成立,若不成立则直接终止程序运行。

QEMU暂时不支持sparse NUMA node id,即对于N个node来说,其node id需要从1到N,表现在numa_info数组中下标为0到N-1,QEMU不支持除此之外的情况(稀疏节点ID)。故要对此进行判断,若是sparse NUMA node id,需要报错。如下:

/* No support for sparse NUMA node IDs yet: */
for (i = max_numa_nodeid - 1; i >= 0; i--) {
    /* Report large node IDs first, to make mistakes easier to spot */
    if (!numa_info[i].present) {
        error_report("numa: Node ID missing: %d", i);
        exit(1);
    }
}

这里或许是我们可以对QEMU开源社区做贡献的点。

确保所有节点都已激活(状态为present):

/* This must be always true if all nodes are present: */
assert(ms->numa_state->num_nodes == max_numa_nodeid);

可以发现,QEMU的鲁棒性非常好。

完成节点属性配置

接下来是对于numa node数量大于0的情况做处理。

其中,以下的判断或可以省略:

if (ms->numa_state->num_nodes > MAX_NODES) {
    ms->numa_state->num_nodes = MAX_NODES;
}

因为在前面为保证鲁棒性,已经确保了:

  • max_numa_nodeid <= MAX_NODES
  • ms->numa_state->num_nodes == max_numa_nodeid

可以得到:ms->numa_state->num_nodes == max_numa_nodeid <= MAX_NODES

这与判断条件中的ms->numa_state->num_nodes > MAX_NODES相矛盾,即此分支条件永远不会成立。我认为此处代码存在冗余,并且分支中的做法将num_nodes设为max_numa_nodeid也没有道理。

内存分配

下面对每个numa node进行内存的分配:

for (i = 0; i < ms->numa_state->num_nodes; i++) {
    if (numa_info[i].node_mem != 0) {
        break;
    }
}
if (i == ms->numa_state->num_nodes) {
    assert(mc->numa_auto_assign_ram);
    mc->numa_auto_assign_ram(mc, numa_info,
                             ms->numa_state->num_nodes, ram_size);
    if (!qtest_enabled()) {
        warn_report("Default splitting of RAM between nodes is deprecated,"
                    " Use '-numa node,memdev' to explictly define RAM"
                    " allocation per node");
    }
        }

可以看到,如果用户的参数中对于所有的节点都没有制定内存分配的大小,则使用默认的均分策略。

在我的调试参数中,没有指定节点内存分配策略,故会进入这种情况并给出warning

启动QEMU时给的总内存是1024MiB,即2^{30}bit,并且分了两个节点,按照上面的策略分配之后,每个节点应该有2^{29}bit,转换为十进制即536870912,观察调试信息,与推测相同:

之后通过配置好的各个节点的memory大小计算出总NUMA 内大小,和用户设置的ram_size比较,若不一致,则报错。

numa_total = 0;
for (i = 0; i < ms->numa_state->num_nodes; i++) {
    numa_total += numa_info[i].node_mem;
}
if (numa_total != ram_size) {
    error_report("total memory for NUMA nodes (0x%" PRIx64 ")"
                 " should equal RAM size (0x" RAM_ADDR_FMT ")",
                 numa_total, ram_size);
    exit(1);
        }

配置NUMA distance table

最后,当启用了NUMA distance这一特性时,需要配置其distance table。QEMU使用的规则如下:

  • 节点访问自身的distance恒为10,故用户可不提供节点到自身的距离值
  • 在默认情况下,distance table是一个symmetric matrix,即A->B == B->A,此时,对于一对节点来说,用户只需要提供一个值即可
  • 若用户同时提供了一对节点相互访问内存的距离,且两距离不等,则QEMU认为distance table是非对称的,此时用户需要提供完整的distance table

代码如下:

if (ms->numa_state->have_numa_distance) {
    /* Validate enough NUMA distance information was provided. */
    validate_numa_distance(ms);

    /* Validation succeeded, now fill in any missing distances. */
    complete_init_numa_distance(ms);
}

即当目标虚拟机需要设置numa distance时(默认为否),先验证用户是否提供了足够的初始化信息,若是,则完成对distance table的配置。

最后,将numa_complete_configuration函数总结如下:

  1. 获得目标机器的NUMA配置信息

  2. 判断目标虚拟机是否设置了auto enable numa,若是,进行预设配置节点参数

  3. 保证鲁棒性(节点数量、非稀疏节点ID、节点激活状态判断)

  4. numa node > 0 的情况进行配置

    • 若用户没有指定分配策略,使用默认分配策略进行节点的内存分配
    • 内存配置正确性检验
    • 配置节点间的distance table

总结

  • 本周分析了machine模块的第一部分,完成对于NUMA的配置,下周将继续分析如何通过节点信息完成numa cpu初始化
  • 通过对于NUMA配置的分析我们可以看到,一个成熟的项目需要有很强的鲁棒性,需要随时做边界检查,防止出现未知错误
  • 在分析的过程中我们看到,QEMU有一些尚未支持的特性,这为我们参与到开源社区中提供了思路
  • 下周将继续深入分析machine_run_board_init函数的行为