為什么要使用bootmem分配器,內存管理不是有buddy系統和slab分配器嗎?由於在系統初始化的時候需要執行一些內存管理,內存分配的任務,這個時候buddy系統,slab分配器等並沒有被初始化好,此時就引入了一種內存管理器bootmem分配器在系統初始化的時候進行內存管理與分配,當buddy系統和slab分配器初始化好后,在mem_init()中對bootmem分配器進行釋放,內存管理與分配由buddy系統,slab分配器等進行接管。
bootmem分配器使用一個bitmap來標記物理頁是否被占用,分配的時候按照第一適應的原則,從bitmap中進行查找,如果這位為1,表示已經被占用,否則表示未被占用。為什么系統運行的時候不使用bootmem分配器呢?bootmem分配器每次在bitmap中進行線性搜索,效率非常低,而且在內存的起始端留下許多小的空閑碎片,在需要非常大的內存塊的時候,檢查位圖這一過程就顯得代價很高。bootmem分配器是用於在啟動階段分配內存的,對該分配器的需求集中於簡單性方面,而不是性能和通用性。
一、bootmem分配器核心數據結構
bootmem核心數據結構bootmem_data:
typedef struct bootmem_data { unsigned long node_boot_start; unsigned long node_low_pfn; void *node_bootmem_map; unsigned long last_offset; unsigned long last_pos; unsigned long last_success; } bootmem_data_t;
系統內存的中每一個結點(如果是內存是UMA,就只有一個這樣的結構)都有一個bootmem_data_t結構,它含有bootmem分配器給結點分配內存時所需的信息。
1、node_boot_start是這個結點內存的起始地址(如果內存是UMA的話,為0);
2、node_low_pfn是低端內存最后一個page的頁幀號;
3、node_bootmem_map指向內存中bitmap所在的位置;
4、last_offset是分配的最后一個頁內的偏移,如果該頁完全使用,則offset為0;
5、last_pos是分配最后一個頁幀號;
6、last_success是最后一次成功分配的位置。
二、bootmem分配器初始化
是在init_bootmem_core函數對bootmem分配器進行初始化的。
static unsigned long __init init_bootmem_core (pg_data_t *pgdat, unsigned long mapstart, unsigned long start, unsigned long end) { bootmem_data_t *bdata = pgdat->bdata; unsigned long mapsize = ((end - start)+7)/8; /*映射的字節數*/ pgdat->pgdat_next = pgdat_list; pgdat_list = pgdat; mapsize = (mapsize + (sizeof(long) - 1UL)) & ~(sizeof(long) - 1UL); bdata->node_bootmem_map = phys_to_virt(mapstart << PAGE_SHIFT); bdata->node_boot_start = (start << PAGE_SHIFT); bdata->node_low_pfn = end; memset(bdata->node_bootmem_map, 0xff, mapsize); //初始化所有內存都保留 return mapsize; }
linux系統初始化過程中通過init_bootmem_core(NODE_DATA(0), min_low_pfn, 0, max_low_pfn)來調用這個函數初始化bootmem分配器。其中min_low_pfn是臨時頁表后的第一個可用頁框,max_low_pfn是低端內存結束的頁框。
前面說的bootmem分配器核心數據結構bootmem_data_t是內嵌在pg_data_t數據結構中(也就是節點的描述符中)。內存中的節點描述符是通過鏈表pgdat_list鏈起來的,如果內存為UMA的話,這個鏈表就只有一個節點。
mapsize按字節來計算系統低端內存對應的bitmap的大小,並且保證它是4的倍數。bitmap存放在min_low_pfn開始的頁框中,並將bitmap的每一位置1,表示所有的內存頁保留(為1是表示bootmem分配器是不能分配該頁框的,為0表示可以分配)。最后函數返回bitmap的大小。
三、bootmem分配器可使用內存注冊
在bootmem分配器初始化中把所有的頁都標記為保留,即不可分配的,所以必須要給bootmem分配器注冊可使用的內存。Linux在初始化中通過調用函數register_bootmem_low_pages(max_low_pfn)來向bootmem分配器注冊低端內存中可使用的內存,該函數根據e820檢測到的內存信息(e820.map[]),將類型為E820_RAM的page頁框對應的位圖中的位置為0,表示對應的page頁框為空閑狀態。然后調用reserve_bootmem(1024*1024, (PFN_PHYS(start_pfn) + bootmap_size + PAGE_SIZE-1) – 1024*1024)來保留bootmem分配器中內存從1MB到bitmap所用的內存頁框,這些保留就包括了kernel內核,臨時頁表,bitmap。
四、bootmem分配器分配內存
內核在初始化階段使用的分配內存函數alloc_bootmem,alloc_bootmem_low,alloc_bootmem_pages,alloc_bootmem_low_pages都會調用__alloc_bootmem,只是一層封裝,實際上是傳遞不同的參數調用__alloc_bootmem。
static void * __init __alloc_bootmem_core(struct bootmem_data *bdata, unsigned long size, unsigned long align, unsigned long goal) { unsigned long offset, remaining_size, areasize, preferred; unsigned long i, start = 0, incr, eidx; void *ret; eidx = bdata->node_low_pfn - (bdata->node_boot_start >> PAGE_SHIFT); offset = 0; if (align && (bdata->node_boot_start & (align - 1UL)) != 0) offset = (align - (bdata->node_boot_start & (align - 1UL))); offset >>= PAGE_SHIFT; if (goal && (goal >= bdata->node_boot_start) && ((goal >> PAGE_SHIFT) < bdata->node_low_pfn)) { preferred = goal - bdata->node_boot_start; if (bdata->last_success >= preferred) preferred = bdata->last_success; } else preferred = 0; preferred = ((preferred + align - 1) & ~(align - 1)) >> PAGE_SHIFT; preferred += offset; areasize = (size+PAGE_SIZE-1)/PAGE_SIZE; incr = align >> PAGE_SHIFT ? : 1;
參數size表示需要分配的內存大小,align表示對齊的大小,goal指定了希望分配內存的起始地址,會從這個位置開始查找。eidx表示在bootmem中頁框的最大索引號。如果align不為0,則計算出還需要偏移的頁框數offset來滿足align的要求。如果goal不為0,且goal在bootmem分配的內存范圍內,則將preferred設置為相對於內存起始地址的偏移量,如果最近一次分配成功的地址偏移量大於preferred,則修改preferred為這個地址偏移量,其它情況都將preferred設置為0。然后preferred轉化為相對於起始地址的頁索引號,並加上offset來滿足align的對齊要求。
restart_scan: for (i = preferred; i < eidx; i += incr) { unsigned long j; i = find_next_zero_bit(bdata->node_bootmem_map, eidx, i); i = ALIGN(i, incr); if (test_bit(i, bdata->node_bootmem_map)) continue; for (j = i + 1; j < i + areasize; ++j) { if (j >= eidx) goto fail_block; if (test_bit (j, bdata->node_bootmem_map)) goto fail_block; } start = i; goto found; fail_block: i = ALIGN(j, incr); } if (preferred > offset) { preferred = offset; goto restart_scan; } return NULL; }
find_next_zero_bit函數從第i個頁框開始搜索空的頁框,並返回空的頁框i,如果后面有連續areasize個空的頁,則找到了並記下起始位置start,如果沒有連續的areasize個頁框,就從新查找連續areasize個空的頁。
found: bdata->last_success = start << PAGE_SHIFT; if (align < PAGE_SIZE && bdata->last_offset && bdata->last_pos+1 == start) { offset = (bdata->last_offset+align-1) & ~(align-1); BUG_ON(offset > PAGE_SIZE); remaining_size = PAGE_SIZE-offset; if (size < remaining_size) { areasize = 0; /* last_pos unchanged */ bdata->last_offset = offset+size; ret = phys_to_virt(bdata->last_pos*PAGE_SIZE + offset + bdata->node_boot_start); } else { remaining_size = size - remaining_size; areasize = (remaining_size+PAGE_SIZE-1)/PAGE_SIZE; ret = phys_to_virt(bdata->last_pos*PAGE_SIZE + offset + bdata->node_boot_start); bdata->last_pos = start+areasize-1; bdata->last_offset = remaining_size; } bdata->last_offset &= ~PAGE_MASK; } else { bdata->last_pos = start + areasize - 1; bdata->last_offset = size & ~PAGE_MASK; ret = phys_to_virt(start * PAGE_SIZE + bdata->node_boot_start); } for (i = start; i < start+areasize; i++) if (unlikely(test_and_set_bit(i, bdata->node_bootmem_map))) BUG(); memset(ret, 0, size); return ret;
找到連續areasize個空的頁后,重新設置last_success。如果align小於PAGE_SIZE並且上一次分配的頁框就是這次分配頁框的上一個頁框並且上一次分配的頁框還沒用完,則合並前面分配的未用的頁框。合並有兩種情況,如果前一個頁框剩下未用的大小remaining_size大於這次需要請求分配的空間大小size,則直接從上一個頁框未使用的空間分配給這次需要的size空間;如果前一個頁框剩下未用的大小remaining_size不大於這次需要請求分配的空間大小size,則將請求的一部分分在前一個頁框中,另一部分從新計算需要分配的頁框數。然后更新相應的last_pos,last_offset字段。如果不能合並,就從start開始分配,更新last_pos,last_offset字段。
五、bootmem分配器釋放內存
釋放bootmem分配器分配的內存,很簡單,直接把對應的bitmap位清0,並更新last_success,代碼如下:
static void __init free_bootmem_core(bootmem_data_t *bdata, unsigned long addr, unsigned long size) { unsigned long i; unsigned long start; unsigned long sidx; unsigned long eidx = (addr + size - bdata->node_boot_start)/PAGE_SIZE; unsigned long end = (addr + size)/PAGE_SIZE; if (addr < bdata->last_success) bdata->last_success = addr; start = (addr + PAGE_SIZE-1) / PAGE_SIZE; sidx = start - (bdata->node_boot_start/PAGE_SIZE); for (i = sidx; i < eidx; i++) { if (unlikely(!test_and_clear_bit(i, bdata->node_bootmem_map))) BUG(); } }
六、bootmem分配器的銷毀
在buddy系統和slab分配器初始化之后,就可以把bootmem分配器銷毀,把內存的管理交給buddy系統和slab分配器。bootmem分配器銷毀是在mem_init函數中調用free_all_bootmem_core函數實現。
static unsigned long __init free_all_bootmem_core(pg_data_t *pgdat) { struct page *page; bootmem_data_t *bdata = pgdat->bdata; unsigned long i, count, total = 0; unsigned long idx; unsigned long *map; int gofast = 0; count = 0; page = virt_to_page(phys_to_virt(bdata->node_boot_start)); //bootmem管理的第一個頁框 idx = bdata->node_low_pfn - (bdata->node_boot_start >> PAGE_SHIFT); //最大bootmem索引號 map = bdata->node_bootmem_map; /*如果起始物理地址頁框號是32對齊的*/ if (bdata->node_boot_start == 0 || ffs(bdata->node_boot_start) - PAGE_SHIFT > ffs(BITS_PER_LONG)) gofast = 1; for (i = 0; i < idx; ) { unsigned long v = ~map[i / BITS_PER_LONG]; if (gofast && v == ~0UL) { //從i開始的32個頁都未使用 int j, order; count += BITS_PER_LONG; __ClearPageReserved(page); //清除頁框描述符的保留位 order = ffs(BITS_PER_LONG) - 1; set_page_refs(page, order); //設置從page開始的32個頁的引用為1 for (j = 1; j < BITS_PER_LONG; j++) { if (j + 16 < BITS_PER_LONG) prefetchw(page + j + 16); __ClearPageReserved(page + j); } __free_pages(page, order); //將這些頁回收到buddy系統中 i += BITS_PER_LONG; page += BITS_PER_LONG; } else if (v) { //從i開始的32頁存在未使用的 unsigned long m; for (m = 1; m && i < idx; m<<=1, page++, i++) { //每次移動一位 if (v & m) { count++; __ClearPageReserved(page); set_page_refs(page, 0); __free_page(page); } } } else { //從i開始的32頁都使用 i+=BITS_PER_LONG; page += BITS_PER_LONG; } } total += count;
如果起始物理地址頁框號是32對齊的,設置gofast為1。如果gofast等於1,並且從page開始的32個頁都未使用,則將page開始的32個頁框回收到buddy系統中,這個通過調用函數__free_pages(page, order),一次回收32個頁框。如果從page開始的32個頁框中存在未使用的,查找這32個頁框中空的頁框,並回收到buddy系統中,一次回收一個頁框。如果32個頁框都使用了,則什么都不做。然后查找下一組32個頁框回收,直到查找完bootmem分配器管理的所有頁框。
total += count; /* 釋放bootmem的bitmap所在的頁 */ page = virt_to_page(bdata->node_bootmem_map); count = 0; for (i = 0; i < ((bdata->node_low_pfn-(bdata->node_boot_start >> PAGE_SHIFT))/8 + PAGE_SIZE-1)/PAGE_SIZE; i++,page++) { count++; __ClearPageReserved(page); set_page_count(page, 1); __free_page(page); } total += count; bdata->node_bootmem_map = NULL; return total; }
既然bootmem分配器要釋放了,那么bootmem分配器所使用的bitmap所在的頁框也應該回收到buddy系統中去。((bdata->node_low_pfn-(bdata->node_boot_start >> PAGE_SHIFT))/8 + PAGE_SIZE-1)/PAGE_SIZE計算bitmap所占的頁框數,然后從bitmap所在的起始頁框開始回收bitmap占用的所有頁框到buddy系統中,一次回收一個頁框。
