问题描述:
最近测试部报了一个问题,云平台中设置大于4GB的内存并设置numa,启动linux2.6.32内核的客户机,之后操作系统中查看实际内存是1.9G,比设置内存小了大概2.1GB。
使用版本信息如下:
QEMU version: 3.0.0
guest os kernel version: 2.6.32
host kernel version: 4.9.0
问题排查如下:
1)在系统中排查问题
通过如下命令查看60系统下内存槽硬件信息,说明内存卡硬件识别正常:
$ dmidecode -t memory
通过$ free -m查看,系统识别出来却有问题:
通过测试如下命令启动QEMU虚拟机必然复现该问题:
$ qemu-system-x86_64 -enable-kvm -name guest=vm1,debug-threads=on -machine pc-i440fx-2.6,accel=kvm,usb=off,dump-guest-core=off -cpu Westmere -m size=4194304k,slots=16,maxmem=16777216k -realtime mlock=off -smp 1,sockets=1,cores=1,threads=1 -numa node,nodeid=0,cpus=0 -uuid fd3535db-2558-43e9-b067-314f48211343 -no-user-config -rtc base=localtime -no-shutdown -boot menu=on,strict=on -device piix3-usb-uhci,id=usb,bus=pci.0,addr=0x1.0x2 -drive file=/opt/issue32/generic.qcow2,format=raw,if=none,id=drive-ide0-0-0 -device ide-hd,bus=ide.0,unit=0,drive=drive-ide0-0-0,id=ide0-0-0,bootindex=1 -spice port=5900,addr=0.0.0.0,disable-ticketing,seamless-migration=on -k en-us -device cirrus-vga,id=video0,bus=pci.0,addr=0x4 -device virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x3 -msg timestamp=on
(这里的slots为16,所以先抛下一个问题:“为什么实测slots为3的时候不会出现2.1G的内存占用?”)
2)接下来在2.6.32内核源码中排查该问题:
通过debug发现是bootmem释放未占用的内存到buddy内存系统的时候少了2.1G,接下来继续研究bootmem的内存分配。
setup_node_bootmem->init_bootmem_node->free_boot_mem_with_active_regions->early_res_to_bootmem(这里干的好事)
bootmem是系统初始化期间的临时内存分配系统,通过位图来标识内存占用,bootmem自身的初始化很简单,首先将位图所有位置1,表示所有页已保留,之后通过free_boot_mem_with_active_regions将可用页帧置0,而free_boot_mem_with_active_regions中会调用early_res_to_bootmem将early_res分配器占用的内存页继承到bootmem中来,early_res是在bootmem之前更早的内存分配器,排查发现是early_res分配器使用过程中占用了2.1G的内存页,而bootmem继承了它导致未释放至buddy子系统。
因此继续向前排查,在这里发现了问题:
start_kernel->setup_arch->initmem_init->acpi_scan_nodes->compute_hash_shift->allocate_cachealigned_memnodemap->reserve_early
CONFIG_ACPI_NUMA确定此选项后内核会编译acpi_numa_init函数,获取numa支持,从硬件系统的acpi表中得到物理硬件的nodes信息,ACPI是在系统启动阶段由BIOS/UEFI收集各方面信息并创建的。而2.1G的内存分配正是在allocate_cachealigned_memnodemap中通过reserve_early完成的。
allocate_cachealigned_memnodemap分配的大小计算如下:
static int __init allocate_cachealigned_memnodemap(void)
{
。。。
nodemap_size = roundup(sizeof(s16) * memnodemapsize, L1_CACHE_BYTES);
。。。
reserve_early(nodemap_addr, nodemap_addr + nodemap_size, "MEMNODEMAP");
。。。
}
memnodemapsize定义如下:
#define memnodemapsize memnode.mapsize
而memnode.mapsize的计算和设置是在compute_hash_shift->extract_lsb_from_nodes中完成的:
/*
* The LSB of all start and end addresses in the node map is the value of the
* maximum possible shift.
*/
static int __init extract_lsb_from_nodes(const struct bootnode *nodes,
int numnodes)
{
int i, nodes_used = 0;
unsigned long start, end;
unsigned long bitfield = 0, memtop = 0;
for (i = 0; i < numnodes; i++) {
start = nodes[i].start;
end = nodes[i].end;
if (start >= end)
continue;
bitfield |= start;
nodes_used++;
if (end > memtop)
memtop = end;
}
if (nodes_used <= 1)
i = 63;
else
i = find_first_bit(&bitfield, sizeof(unsigned long)*8);
memnodemapsize = (memtop >> i)+1;
return i;
}
extract_lsb_from_nodes中将所有内存节点的start值进行了一个位或。之前埋下过一个问题也可以解释了“为什么slots为3的时候不会出现2.1G的内存reserve?”其实答案在于最后memnodemapsize = (memtop >> i)+1中,将unsigned long转换成了unsigned int,而i=0,直接导致了高32位的精度丢失,而在slot=3的时候恰好1全在高32位上面,导致mapsize=1,因此没有内存占用。所以这里又可以抛出一个问题了 ;)
(为什么内存区域会随着slots个数而变化?)
node区域范围有如下四个:
a) 0-655,360
b)1,048,576-3,221,225,472
c)4,200,000,000-5,300,000,000
d)35,433,480,192-35,433,480,191
最后一个区域范围恰好是hotplug的内存区域范围,很明显这里的hotplug区域范围有问题,怎么可能只有1。由于start只比end小1,也就直接导致了根据start位与计算的memnodemapsize值偏大,并且还出现了精度丢失!
好了,现在知道问题的直接导致是热插拔内存区设置的有问题,但是为什么会有问题? hotplug内存区参数是怎么读取的,之后想起开机启动的时候好像也报了和hotplug区相关的问题:
系统启动过程报hotplug区的过小,而hotplug参数也是来自于SRAT表的解析,SRAT(静态资源亲和性表)是ACPI规范的一部分,SRAT表解析流程如下:
start_kernel->setup_arch()->acpi_numa_init()->acpi_table_parse_srat()->acpi_table_parse_entries()
->acpi_parse_memory_affinity()->acpi_numa_memory_affinity_init()->update_nodes_add()
update_nodes_add函数中:
if ((signed long)(end - start) < NODE_MIN_SIZE) {
printk(KERN_ERR "SRAT: Hotplug area too small\n");
return;
}
这里的SRAT表是前期BIOS存放在内存中的,其根本来源还是qemu。
3)因此就开始了在qemu源码中排查问题的旅途
qemu中建立SRAT表的流程:
main->qemu_run_machine_init_done_notifiers->notifier_list_notify->pc_machine_done->acpi_setup->acpi_build->build_srat
build_srat是构建SRAT表的函数,其中有:
if (hotplugabble_address_space_size) {
build_srat_hotpluggable_memory(table_data, machine->device_memory->base,
hotplugabble_address_space_size,
pcms->numa_nodes - 1);
}
这里的hotplug memory的基址是machine->device_memory->base=5,368,709,120,这个数值从前面的区域范围表中看起来是合理的,为什么这个base addr进入虚拟机后就变成
35,433,480,191了?
ram_addr_t hotplugabble_address_space_size =
object_property_get_int(OBJECT(pcms), PC_MACHINE_DEVMEM_REGION_SIZE,
NULL);
往前看,前面获取的hotplugaable_address_space_size=30064771072恰好是(35,433,480,192 - 5,368,709,120),也就是说end地址传入虚拟机是正确的。
最终在如下代码段找到了问题所在,在info为null是,build_srat_memory设置的start=end-1,size=1,和虚拟机中读取到的完全相符,看来是这里导致的该bug。
static void build_srat_hotpluggable_memory(GArray *table_data, uint64_t base,
uint64_t len, int default_node)
{
MemoryDeviceInfoList *info_list = qmp_memory_device_list();
MemoryDeviceInfoList *info;
。。。
for (cur = base, info = info_list;
cur < end;
cur += size, info = info->next) {
numamem = acpi_data_push(table_data, sizeof *numamem);
if (!info) {
build_srat_memory(numamem, end - 1, 1, default_node,
MEM_AFFINITY_HOTPLUGGABLE | MEM_AFFINITY_ENABLED);
break;
}
。。。
通过参考最新版的的代码以及老版本(2.x)的代码稍作修改即可。
PS:这里其实也可以通过设置device模型为pc-dimm,让info不为null,比如如下:
-numa node -object memory-backend-ram,policy=default,size=4G,id=mem-mem1 -device pc-dimm,node=0,id=dimm-mem1,memdev=mem-mem1
#)
这里最后再解释下为什么内存区域会随着slots个数而变化:
hotplugin内存区域初始化流程如下:
main->machine_run_board_init->pc_init_v3_0->pc_init1->pc_memory_init
如下可知device_mem_size = max_size - ram_size + 1G * slots
device_mem_size = machine->maxram_size - machine->ram_size
if (pcmc->enforce_aligned_dimm) {
/* size device region assuming 1G page max alignment per slot */
device_mem_size += (1 * GiB) * machine->ram_slots;
}
device_mem_size = 12G(16G-4G) + 1G * 16(slots) = 28G(30064771072)。这里的1G * slots是考虑到大页对齐的问题,支持单页最大1G。所以hotplug区域也会随着slots而变化。