解決qemu虛擬機中內存偏小的問題

問題描述:
最近測試部報了一個問題,雲平臺中設置大於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而變化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章