本周工作概述
- 了解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等。
再议machine和machine_class
这周的分析中,又遇到了从machine获取machine_class的操作,此处深入说明一下
上周我们看到在machine_run_board_init函数中首先做了如下操作:
MachineClass *machine_class = MACHINE_GET_CLASS(machine);
实际上,MACHINE_GET_CLASS在boards.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_mapping为False
完成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_mapping为True,故不会进入第二层分支,在获取完每个CPU的属性信息之后,使用machine_set_cpu_node完成配置,其属于的NUMA ID由mc->get_default_cpu_node_id获得,使用模除的方式为所有的NUMA节点相对平分CPU
对于第二种情况,用户设置了部分CPU所属的NUMA节点,则default_mapping为False,会进入第二层分支。此时,需要给出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:
