QEMU开源实战(五)

1,309 阅读7分钟

本周工作概述

  • 了解C语言的glib库相关
  • 了解C语言在结构体内封装函数的方法,了解C实现类和对象的基本思想
  • 完成NUMA模块的分析

numa模块

首先回顾一下上周我们分析第一层的函数,machine模块中的machine_run_board_init

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);
}

上周我们进入了第一个条件判断中,即:

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

这部分判断虚拟机是否支持NUMA,并进行配置和初始化。

上周我们已经详细查看了第一步的配置细节,即numa_complete_configuration函数,下面我们继续分析NUMA初始化函数。

numa_complete_configuration函数中我们看到,默认情况下NUMA节点数量为零,当用户指定了NUMA的配置信息或在特定情况下(参见上周默认节点配置),NUMA经过配置后,其节点数量不为零,即machine->numa_state->num_nodes > 0,此时进入machine_numa_finish_cpu_init函数

static void machine_numa_finish_cpu_init(MachineState *machine)

首先看函数第一部分声明:

int i;
bool default_mapping;
GString *s = g_string_new(NULL);
MachineClass *mc = MACHINE_GET_CLASS(machine);
const CPUArchIdList *possible_cpus = mc->possible_cpu_arch_ids(machine);

使用glib和其他库

我们知道,C语言中没有bool类型,此处声明的bool default_mapping源自于stdbool.h头文件:

stdbool.h是C标准函数库中一个常用的头文件。它定义了一个布尔类型,于C99中加入。

同样,C语言中原生没有类似于C++中字符串的概念,而第三行声明的GString类字符串数据类型,源自于glib,GString类似于标准C的字符串类型,但是GString能够自动增长,这些特性可以防止程序中的缓冲区溢出。

glib是Linux平台下最常用的C语言函数库,它具有很好的可移植性和实用性。同时,其具有很好的跨平台性,可以在多个平台下使用,比如Linux、Unix、Windows等。

再议machinemachine_class

这周的分析中,又遇到了从machine获取machine_class的操作,此处深入说明一下

上周我们看到在machine_run_board_init函数中首先做了如下操作:

MachineClass *machine_class = MACHINE_GET_CLASS(machine);

实际上,MACHINE_GET_CLASSboards.h中宏定义如下:

#define MACHINE_GET_CLASS(obj) \
    OBJECT_GET_CLASS(MachineClass, (obj), TYPE_MACHINE)

开始不理解其中的MachineClass到底是什么,因为这实际上前面并没有定义,只是一个单纯的结构体定义,相当于一个数据类型。后来明白在OBJECT_GET_CLASS中做了一次类型判断,而这里需要用到MachineClass。也就是说,这里就是把MachineCLass这个结构体定义传了进来,以便做类型检查。

而注释中对OBJECT_GET_CLASS描述如下:

This function will return a specific class for a given object. Its generally used by each type to provide a type safe macro to get a specific class type from an object.

也就是说,此函数传入一个对象,经过类型检查(是否是父类的一个子类),即文中的安全性检查,返回这个对象的类的信息。

所以在machine模块中,一个machine代表了全部的虚拟机信息,但是关于主板类型的通用信息是由该machine类决定的,故需要先获取这个虚拟机对象的类的信息,从而获得主板的通用信息,如上周的machine_class->numa_supported的值。

在结构体内部定义函数

在继续深入一个结构体定义之前,先看一下C语言在结构体内部定义函数的方法:参考链接

如下是一个示例代码:

#include<stdio.h>
#include<stdlib.h>
typedef struct Hello{
    void (*say_hello)(char * words);
}Hello;

void say_hello(char * words){
    printf("%s", words);
}

int main(){
    char * content = "hello2";
    Hello * ph = (Hello *)malloc(sizeof(Hello));
    ph->say_hello = say_hello;
    ph->say_hello(content);
}

在编写示例代码的过程中,加深了对于大一上学期C语言学习中指针的理解。如主函数的malloc内存分配,返回值是一个地址,而使用Hello *进行强制类型转换,保证了之后对于指针运算的正确性。不过参考C Primer Plus后得知,在C中不一定要使用强制类型转换,但是在C++中必须使用。

我们知道,函数名实际上是一个指向函数起始的地址。故我们可以使用一个指针表示一个函数,这为我们在结构体内“定义函数”提供方便,只需要在结构体中声明一个指向函数的指针即可。

回到函数的声明部分,mc->possible_cpu_arch_ids返回一个cpulist,里面包含了用户配置的虚拟机中,使用当前架构的CPU的id list。我们看一下这个CPUArchIdList的结构:

typedef struct {
    int len;
    CPUArchId cpus[0];
} CPUArchIdList;

第一项len表示该List的长度,第二项CPUArchID结构体定义如下:

typedef struct CPUArchId {
    uint64_t arch_id;
    int64_t vcpus_count;
    CpuInstanceProperties props;
    Object *cpu;
    const char *type;
} CPUArchId;
  • arch_id: architecture-dependent CPU ID of present or possible CPU
  • cpu:pointer to corresponding CPU object if it's present on NULL otherwise
  • type: QOM class name of possible @cpu object
  • props: CPU object properties, initialized by board
  • vcpus_count: number of threads provided by @cpu object

其中,比较重要的是props,保存了CPU的特性:

struct CpuInstanceProperties {
    bool has_node_id;
    int64_t node_id;
    bool has_socket_id;
    int64_t socket_id;
    bool has_die_id;
    int64_t die_id;
    bool has_core_id;
    int64_t core_id;
    bool has_thread_id;
    int64_t thread_id;
};

可以看到,当主板初始化CPU时,其是否须有NUMA的node_id正是保存在这里,故之后完成NUMA对于CPU的初始化的时候,需要从这里获取信息。

mc->possible_cpu_arch_ids函数会检索用户是否进行了虚拟机CPU的设置,例如设置socket, thread,是否指定CPU数量。若未指定,其默认返回值为1,即只有一个CPU。

检查用户是否已设置NUMA节点和CPU信息

assert(machine->numa_state->num_nodes);
for (i = 0; i < possible_cpus->len; i++) {
    if (possible_cpus->cpus[i].props.has_node_id) {
        break;
    }
}
default_mapping = (i == possible_cpus->len);

首先,由于这部分是要完成CPU的NUMA初始化,进入machine_numa_finish_cpu_init模块的条件是machine->numa_state->numa_nodes不为零。为了以防在虚拟机运行过程中其配置信息被修改掉,此处要做一次鲁棒性检查

之后进入循环遍历possible_cpus,循环退出的条件是possible_cpus的某个CPU已经分配了NUMA节点

若循环结束i == possible_cpus->len,即循环自然结束,意味着用户并未给CPU配置NUMA信息,此时设置default_mapping,需要之后由QEMU按照默认设置初始化CPU的 NUM设置;若循环结束后i != possible_cpus->len,则意味着用户已经完成了部分或全部的CPU的NUMA配置,置default_mappingFalse

完成CPU的NUMA初始化

for (i = 0; i < possible_cpus->len; i++) {
    const CPUArchId *cpu_slot = &possible_cpus->cpus[i];

    if (!cpu_slot->props.has_node_id) {
        /* fetch default mapping from board and enable it */
        CpuInstanceProperties props = cpu_slot->props;

        props.node_id = mc->get_default_cpu_node_id(machine, i);
        if (!default_mapping) {
            /* record slots with not set mapping,
             * TODO: make it hard error in future */
            char *cpu_str = cpu_slot_to_string(cpu_slot);
            g_string_append_printf(s, "%sCPU %d [%s]",
                                   s->len ? ", " : "", i, cpu_str);
            g_free(cpu_str);

            /* non mapped cpus used to fallback to node 0 */
            props.node_id = 0;
        }

        props.has_node_id = true;
        machine_set_cpu_numa_node(machine, &props, &error_fatal);
    }
}

用户在设置虚拟机的时候,若设置了NUMA节点,则对于CPU和NUMA节点的配置有以下几种情况:

  • 完全配置了CPU对NUMA的映射关系
  • 配置了部分CPU所属的NUMA节点
  • 未配置任何CPU和NUMA的映射

对于以上三种情况,QEMU都需要做处理

其中最简单的情况是第一种,即用户给每个CPU都设置了NUMA节点的ID号,则在此部分的判断条件!cpu_slot->props.has_node_id恒为False,不会进入分支。

对于第三种情况,用户指定了NUMA的数量,但是没有设置CPU和NUMA的映射关系。此时default_mappingTrue,故不会进入第二层分支,在获取完每个CPU的属性信息之后,使用machine_set_cpu_node完成配置,其属于的NUMA ID由mc->get_default_cpu_node_id获得,使用模除的方式为所有的NUMA节点相对平分CPU

对于第二种情况,用户设置了部分CPU所属的NUMA节点,则default_mappingFalse,会进入第二层分支。此时,需要给出warning,并将所有未分配给NUMA节点的CPU默认分配到0号NUMA节点上,即设置props.node_id = 0

需要注意的是,在第二种情况的处理代码中,编写者添加了TODO信息:

TODO: make it hard error in future

此处表明在未来的版本中,需要硬报错,即不允许上述第二种情况的出现。故我们此时我们又发现一个可以贡献的TODO

对于不完全配置给出WARNING信息

if (s->len && !qtest_enabled()) {
        warn_report("CPU(s) not present in any NUMA nodes: %s",
                    s->str);
        warn_report("All CPU(s) up to maxcpus should be described "
                    "in NUMA config, ability to start up with partial NUMA "
                    "mappings is obsoleted and will be removed in future");
    }
g_string_free(s, true);

debug中若给出以下配置信息:

-smp
cpus=4
-numa
node,nodeid=0,cpus=0-1
-numa
node,nodeid=1

即未配置2-3两个CPU的NUMA节点信息,QEMU给出warning:

表示用户部分配置CPU和NUMA映射的方法将被禁止。