Linux內存管理第五章 -- Boot Memory Allocator

Linux內存管理第五章 – Boot Memory Allocator

在編譯階段就初始化內核核心的memory struct是不現實的,因爲有太多的硬件配置的排列組合。但是要初始化這些基本的structures需要用到內存,甚至說physical page allocator也需要分配內存來初始化它自己。但時physical page allocator如何來分配內存來初始化自己呢 ?
爲了達到這個目標,一種叫做Boot Memory Allocator的特殊分配被使用。它是基於一種最基本的allocator:First Fit allocator的原理實現的。它使用bitmap來呈現memory的狀態而不是使用鏈表將free blocks連接起來。如果一個bit是1,那麼該page已經被分配了,如果是0,則該page還未被分配。爲了能夠滿足比一個page還小內存塊的分配,boot memory allocator會記住上次分配使用的PFN(頁框號)和上次分配完成之後在該page中的偏移。將細小的內存塊的分配儘量集中在相同的page中。
至此,很多人可能會問爲什麼改分配器沒有在系統運行起來之後再繼續使用?一種最終要的原因是儘管該分配器不會造成嚴重的內存碎片,但是每次分配過程中都需要線性掃描搜索內存來滿足當前的這次分配。因爲是檢查bitmaps,所以代價是很昂貴的,尤其是first fit算法傾向將小塊分配放在物理內存的開頭,但是這些內存區域在分配大塊內存時也需要掃描,因此該過程將非常浪費。

unsigned long init_bootmem(unsigned long start, unsigned long page)
初始化0 ~~ PFN page個page的物理內存,可用內存從PFN start開始
void reserve_bootmem(unsigned long addr, unsigned long size)
標記從地址addr開始,到地址addr + size終止的這段內存區域爲保留區域。部分保留頁面的請求將導致整個頁面被保留。
void free_bootmem(unsigned long addr, unsigned long size)
標記從地址addr開始,到地址addr + size終止的這段內存區域爲free
void * alloc_bootmem(unsigned long size)
從ZONE_NORMAL中分配size個字節的內存。通過此函數分配的地址將會與L1 硬件緩存對齊從而最大效率利用硬件緩存
void * alloc_bootmem_pages(unsigned long size)
從ZONE_NORMAL中分配滿足size的頁對齊的pages,如:不足一頁返回整頁,不足兩頁返回兩個整頁。
void * alloc_bootmem_low_pages(unsigned long size)
從ZONE_NORMAL中分配滿足size的頁對齊的pages,如:不足一頁返回整頁,不足兩頁返回兩個整頁。
unsigned long bootmem_bootmap_pages(unsigned long pages)
計算分配pages個page所需要的用於代表分配狀態的bitmap所佔用page的個數
unsigned long free_all_bootmem()
在boot allocator生命結束的時候使用。它遍歷bitmap中的所有頁面。每個page將釋放給physical page allocator。

Representing the Boot Map

系統中每個node都包含有一個bootmem_data struct。它包有boot memory allocator從一個node中分配內存所需的信息,比如:代表page狀態的bitmap和分配的page的位置。其定義如下:

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;	/* Previous allocation point.  To speed  up searching */
} bootmem_data_t;
  • node_boot_start:當前bootmem_data結構中所描述的memory塊的起始物理地址。
  • node_low_pfn:當前bootmem_data結構中所描述memory塊的結束pfn。換句話說,就是該node中ZONE_NORMAL的結束物理地址。
  • node_bootmem_map:代表page分配狀態的bitmap的地址。
  • last_offset:在某個page中上次分配完成之後的偏移。如果爲0,則代表該page全部被分配完了。
  • last_pos:上次分配所使用的page的PFN。將last_pos和last_offset聯合起來,可以測試多個分配是否可以合併到一個page中進行而不是使用一個全新的page
  • last_success:上次分配所對應的物理地址

其中node_bootmem_map數組中的每個bit代表一個page,下面來看下函數bootmem_bootmap_pages()的實現:

unsigned long __init bootmem_bootmap_pages (unsigned long pages)
{
	unsigned long mapsize;
	mapsize = (pages+7)/8;//一個page代表爲一個bit,將pages個bit轉換成字節數,也就是字節對齊
						  //比如pages = 11, (11 + 7)/8 = 2
	mapsize = (mapsize + ~PAGE_MASK) & PAGE_MASK;//將字節數進行頁對齊,比如:mapsize = 10,計算之後mapsize = 4096.
	                                             //如果mapsize = 4098,計算之後mapsize = 8192
	mapsize >>= PAGE_SHIFT;
	return mapsize;
}

該函數的作用是有pages個物理page,bitmap需要多大的空間。
下面來看下兩次分配過程中上述字段的變化:
boot

Allocating Memory

reserve_bootmem()函數可以爲調用者保留頁面,但該函數對已常規分配來說非常麻煩。因此有四個函數: alloc_bootmem(), alloc_bootmem_low(), alloc_bootmem_pages() and alloc_bootmem_low_pages()
這四個函數最終都會調用到__alloc_bootmem_core(),其參數含義如下:

static void * __init
__alloc_bootmem_core(struct bootmem_data *bdata, unsigned long size,
								unsigned long align, unsigned long goal)
  • bdata:爲每個node中對應的bootmem_data struct
  • size:當前需要分配的內存大小
  • align:需要進行字節對齊,主要是爲了與硬件cache對齊高效利用L1 CACHE
  • goal:用戶期望得到該從該地址開始的size大小的內存塊。

該函數最聰明的部分是處理最新一次的分配能否和上一次分配合並。如果滿足以下條件就能合併,

  • 上次分配所後的last_pos,與此次分配找到的page是連續的
  • 上次分配之後的page中海油空閒的空間last_offset != 0
  • 對齊大小小於PAGE_SIZE

無論當前的分配是否有與上次分配合並,last_pos和last_offse都將更新用來顯示當前分配所使用的是哪一個page以及該page使用了多少空間。如果該page全部被使用,則last_offset = 0。

Freeing Memory

與分配函數相比,釋放函數只有free_bootmem(),其核心原理相當簡單。該free會影響每個整體page,將page在bitmap中對應的bit清零。如果在此次清零時發現已經是0,則是由double free的錯誤上報。
對於free函數來說有個嚴格的限制:只有full pages纔可能被釋放。系統不會記錄某個page部分被分,如果部分被釋放的,該page整體仍是預留的。這並不像它所表現的問題那麼嚴重,因爲此次分配會在系統生命週期內持續。但是在系統啓動階段,它對於開發者來說仍是一箇中藥的限制。
下面來看下free函數的源代碼:(有些疑問??)

static void __init free_bootmem_core(bootmem_data_t *bdata, unsigned long addr, unsigned long size)
{
	unsigned long i;
	unsigned long start;
	/*
	 * round down end of usable mem, partially free pages are
	 * considered reserved.
	 */
	unsigned long sidx;
	unsigned long eidx = (addr + size - bdata->node_boot_start)/PAGE_SIZE;//獲取從addr開始size大小地址塊的結束相對頁框號
																//因爲node_bootmem_map中的bit 對應的是相對頁框號,從0,1,2...開始
																//bdata->node_boot_start可能在第100個page中。
	unsigned long end = (addr + size)/PAGE_SIZE;

	BUG_ON(!size);
	BUG_ON(end > bdata->node_low_pfn);

	if (addr < bdata->last_success)
		bdata->last_success = addr;

	/*
	 * Round up the beginning of the address.
	 */
	start = (addr + PAGE_SIZE-1) / PAGE_SIZE;//獲取從addr開始size大小地址塊的起始絕對頁框號
	sidx = start - (bdata->node_boot_start/PAGE_SIZE);//獲取addr的相對頁框號
    //如果sidx = eidx,則該page不會釋放.即此次要釋放的內存大小不足一個page,那這種page是時候釋放?
    //另外,如果size不足一個page,但addr 和addr + size落在兩個相鄰的page中,直接釋放掉會不會出錯 ?
	for (i = sidx; i < eidx; i++) {
		if (unlikely(!test_and_clear_bit(i, bdata->node_bootmem_map)))
			BUG();
	}
}

Retiring the Boot Memory Allocator

衆所周知,在系統啓動過程的後半部分,start_kernel()函數被調用用來安全的刪除boot allocator和以及和它相關的數據結構。每種硬件架構下都會提供一個mem_init()的函數來銷燬boot memory allocator和其相關數據結構。
mem_init()函數的目的非常簡單。它負責計算low memory 和high memory的大小並打印出信息給使用者來做最終的硬件初始化。在x86上與virtual memory相關的主要函數是set_highmem_pages_init()。
mem_init()首先通過free_all_bootmem()來講boot memory allocator退休即釋放boot memory自己所佔用和分配出去的memory。該函數會調用free_all_bootmem_core(),其核心步驟如下:

  • boot memory allocator中爲分配的pages:
    • 清除struct page中的PG_reserve的標記位
    • 將count設置爲1
    • 調用__free_pages()將memory移交給buddy allocator來構建它的free lists
  • 釋放bitmap中使用過的pages並將這些pages移交給buddy allocator。

至此,buddy allocator將完全控制所有page中的low memory的部分,留下hig memory pages。當free_all_bootmem()返回後,mem_init()將首先出於統計的目的,計算一下保留pages的個數。然後調用set_highmem_pages_init()來處理high memory pages。但是在此時,應該要清除mem_map數組是如何分配和初始化的,以及如何將mem_map數組中的struct pages給到main allocator。
在但個node系統中初始化low memory的基本流程見下圖:
retire
一旦free_all_bootmem()返回,ZONE_NORMAL中的所有pages就已經交給buddy allocator了。然後調用set_highmem_pages_init()-> one_highpage_init()來循環初始化highstart_pfn到highend_pfn之間的每一個high memory page。one_highpage_init()簡單地清除PG_reserved標記。將count設置爲1然後再調用__free_pages()來將該struct page移交給buddy allocator,其過程與free_all_bootmem_core()中的行爲一致。
此時,boot memory allocator不再需要了並且buddy allocator是系統中主要的物理page allocator。一個有趣的特點需要注意下就是不僅僅boot allocator的數據被刪除了,其代碼同時也被刪除了。只在系統初始化階段用到的初始化函數會使用__init標記:

unsigned long __init free_all_bootmem (void)

所有的這些函數都被連接器集中放置在.init段。在x86上,函數free_initmem()從__init_begin到__init_end遍歷所有page並且將這些page釋放給buddy allocator。使用這種方法,Linux可以釋放大量的memory:啓動階段使用的代碼,系統啓動後就不再需要的代碼。

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