內核的bootmem內存分配器

在內核啓動期間,夥伴系統內存管理器還沒有建立之前,內核此時也要分配內存以及創建一些用於建立內存管理等機制的數據結構,此時內存分配和管理就是由bootmem內存分配器來完成的。

bootmem的建立要求就是簡單,越簡單越好,因爲一旦夥伴系統建立之後,就不需要bootmem了,因此對性能和通用性等要服從一切從簡的原則。在瞭解這個分配器之後,就會知道它真的很簡單。

該分配器使用一個位圖來管理頁,位圖比特位的數目與系統中物理內存頁的數目相同,比特位爲1時,表示這個頁已經分配,爲0時,表示當前指示的頁是空閒的。在需要分配內存時,分配器掃描整個位圖,直到找到一個能夠提供足夠連續頁的位置。

下面分析一下這個分配器。

一,前提

在這個分配器被建立之前,先了解一下內核此時是一個什麼樣的狀態,主要說內存方面的。

內存在檢測系統可用內存之後,被存入一個數組之中,其結構如下:

struct e820map{
    _u32 nr_map;
    struct e820entry map[E820MAX];
}
struct e820entry{
    _u64 addr;
    _u64 size;
    _u32 type;
} __attribute__((packed));
在用中斷檢測可用內存之後,內存被存入e820map e820變量中,然後根據這個數組,確定下面一些值:

  1. min_low_pfn :表示RAM 中在內核映像後第一個可用頁框的頁框號,因爲內存映像所佔用的頁肯定不能用於分配的。
  2. max_pfn:表示最後一個可用頁框的頁框號,包括可端內存。
  3. max_low_pfn:被內核直接映射的,低地址內存的最後一個頁框的頁框號。
在確定好了這些值之後,調用setup_bootmem_allocator函數,完成bootmem分配的建立工作,這是本博文的重點。

二,數據結構

      用於維護該分配器的數據結構如下
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 */
	struct list_head list;
} bootmem_data_t;

node_boot_start:這個字段保存了系統中第一個頁的編號,這一般都是0;

node_low_pfn:可以被直接管理的物理地址空間中最後一頁的編號;

node_bootmem_map:指向分配位圖的內存指針,前面說這種分配方式主要是維護一個大的位圖。

last_offset:上一次分配的頁內的偏移,上一次分配的頁的編號由last_pos指定,而last_offset指定個頁內已經分配的偏移。

last_success:指定位圖中上一次成功分配內存的位置,下一個分配從這裏開始分配。

list:因爲內存並不是都是連續的,對於不連續的內存,系統需要多個bootmem分配器,所有的分配器都保存一個鏈表中,表頭由一個全局變量指定。

void __init setup_bootmem_allocator(void)
{
	unsigned long bootmap_size;
	/*
	 * Initialize the boot-time allocator (with low memory only):
	 */
	bootmap_size = init_bootmem(min_low_pfn, max_low_pfn);
調用init_bootmem函數完成初始化,這個函數會調用init_bootmem_core函數完成初始化。

unsigned long __init init_bootmem(unsigned long start, unsigned long pages)
{
	max_low_pfn = pages;
	min_low_pfn = start;
	return init_bootmem_core(NODE_DATA(0), start, 0, pages);
}
這裏的兩個參數需要注意,是由min_low_pfn和max_low_pfn傳遞而來,表示低內存域的最小和最大的頁幀編號。

在下面的函數實現中,可以看到mapstart的值就是min_log_pfn,end的值是max_low_pfn,而start的值爲0,這個值要賦給bootmem_data_t結構的node_boot_start字段,參數中的NODE_DATA(0),表示當前內存結點。

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;

	bdata->node_bootmem_map = phys_to_virt(PFN_PHYS(mapstart));//內存的最開始,由分配位圖佔用一部分
	bdata->node_boot_start = PFN_PHYS(start);
	bdata->node_low_pfn = end;//注意這個賦值
	link_bootmem(bdata);

	/*
	 * Initially all pages are reserved - setup_arch() has to
	 * register free RAM areas explicitly.
	 */
	mapsize = get_mapsize(bdata);
	memset(bdata->node_bootmem_map, 0xff, mapsize);

	return mapsize;
}
PFN_PHYS宏是將頁幀的編號轉換爲對應的頁內的物理地址,該操作通過左移頁內偏移的位數來達到。

#define PFN_PHYS(x)	((x) << PAGE_SHIFT)//在這裏是左移12位

init_bootmem_core完成以下操作:

  1. 給內存結點的bdata域,就是bootmem_data_t類型字段的值賦值。一個內存結點的數據結構中包含由這個分配器指針。
  2. 將bootmem_data_t添加到全局變量爲表頭的鏈表上,這個通過調用link_bootmem完成,將其添加到分局變量bdata_list上。
    static void __init link_bootmem(bootmem_data_t *bdata)
    {
    	bootmem_data_t *ent;
    
    	if (list_empty(&bdata_list)) {
    		list_add(&bdata->list, &bdata_list);
    		return;
    	}
    	/* insert in order */
    	list_for_each_entry(ent, &bdata_list, list) {
    		if (bdata->node_boot_start < ent->node_boot_start) {
    			list_add_tail(&bdata->list, &ent->list);
    			return;
    		}
    	}
    	list_add_tail(&bdata->list, &bdata_list);
    }
  3. get_mapsize函數計算分配器中可用的內存頁所需的BIT位數,就是一個頁佔用一個BIT的話,需要多少個bit位,然後按字對齊。注意這個函數會將從第0頁開始進行管理,但是第0頁至少已經被作爲內核映射了。
    static unsigned long __init get_mapsize(bootmem_data_t *bdata)
    {
    	unsigned long mapsize;
    	unsigned long start = PFN_DOWN(bdata->node_boot_start);
    	unsigned long end = bdata->node_low_pfn;
    
    	mapsize = ((end - start) + 7) / 8;
    	return ALIGN(mapsize, sizeof(long));
    }
  4. 調用memset將所有的頁標識爲已經使用。接下來肯定要完成哪些頁能夠被用來分配內存,
三,釋放可用的內存

      在前面已經初始化了一個位圖,該位圖的位置從min_low_pfn開始佔用,其實就是被內核映射之後的第一個頁。但是標記了所有的內存頁都是已經被使用了,這時系統中就不存在可用的內存了,需要從剛標識的內存位圖中釋放一些潛在的、可用的內存。這通過調用register_bootmem_low_pages函數完成。

	register_bootmem_low_pages(max_low_pfn);

	/*
	 * Reserve the bootmem bitmap itself as well. We do this in two
	 * steps (first step was init_bootmem()) because this catches
	 * the (very unlikely) case of us accidentally initializing the
	 * bootmem allocator with an invalid RAM area.
	 */
	reserve_bootmem(__pa_symbol(_text), (PFN_PHYS(min_low_pfn) +
			 bootmap_size + PAGE_SIZE-1) - __pa_symbol(_text));

	/*
	 * reserve physical page 0 - it's a special BIOS page on many boxes,
	 * enabling clean reboots, SMP operation, laptop functions.
	 */
	reserve_bootmem(0, PAGE_SIZE);

	/* reserve EBDA region, it's a 4K region */
	reserve_ebda_region();

    /* could be an AMD 768MPX chipset. Reserve a page  before VGA to prevent
       PCI prefetch into it (errata #56). Usually the page is reserved anyways,
       unless you have no PS/2 mouse plugged in. */
	if (boot_cpu_data.x86_vendor == X86_VENDOR_AMD &&
	    boot_cpu_data.x86 == 6)
	     reserve_bootmem(0xa0000 - 4096, 4096);

#ifdef CONFIG_SMP
	/*
	 * But first pinch a few for the stack/trampoline stuff
	 * FIXME: Don't need the extra page at 4K, but need to fix
	 * trampoline before removing it. (see the GDT stuff)
	 */
	reserve_bootmem(PAGE_SIZE, PAGE_SIZE);
#endif
#ifdef CONFIG_ACPI_SLEEP
	/*
	 * Reserve low memory region for sleep support.
	 */
	acpi_reserve_bootmem();
#endif
#ifdef CONFIG_X86_FIND_SMP_CONFIG
	/*
	 * Find and reserve possible boot-time SMP configuration:
	 */
	find_smp_config();
#endif
	numa_kva_reserve();
#ifdef CONFIG_BLK_DEV_INITRD
	if (boot_params.hdr.type_of_loader && boot_params.hdr.ramdisk_image) {
		unsigned long ramdisk_image = boot_params.hdr.ramdisk_image;
		unsigned long ramdisk_size  = boot_params.hdr.ramdisk_size;
		unsigned long ramdisk_end   = ramdisk_image + ramdisk_size;
		unsigned long end_of_lowmem = max_low_pfn << PAGE_SHIFT;

		if (ramdisk_end <= end_of_lowmem) {
			reserve_bootmem(ramdisk_image, ramdisk_size);
			initrd_start = ramdisk_image + PAGE_OFFSET;
			initrd_end = initrd_start+ramdisk_size;
		} else {
			printk(KERN_ERR "initrd extends beyond end of memory "
			       "(0x%08lx > 0x%08lx)\ndisabling initrd\n",
			       ramdisk_end, end_of_lowmem);
			initrd_start = 0;
		}
	}
#endif
	reserve_crashkernel();
}
下面是register_bootmem_low_pages的代碼。

void __init register_bootmem_low_pages(unsigned long max_low_pfn)
{
	int i;

	if (efi_enabled) {
		efi_memmap_walk(free_available_memory, NULL);
		return;
	}
	for (i = 0; i < e820.nr_map; i++) {
		unsigned long curr_pfn, last_pfn, size;
		/*
		 * Reserve usable low memory
		 */
		if (e820.map[i].type != E820_RAM)
			continue;
		/*
		 * We are rounding up the start address of usable memory:
		 */
		curr_pfn = PFN_UP(e820.map[i].addr);
		if (curr_pfn >= max_low_pfn)
			continue;
		/*
		 * ... and at the end of the usable range downwards:
		 */
		last_pfn = PFN_DOWN(e820.map[i].addr + e820.map[i].size);

		if (last_pfn > max_low_pfn)
			last_pfn = max_low_pfn;

		/*
		 * .. finally, did all the rounding and playing
		 * around just make the area go away?
		 */
		if (last_pfn <= curr_pfn)
			continue;

		size = last_pfn - curr_pfn;
		free_bootmem(PFN_PHYS(curr_pfn), PFN_PHYS(size));
	}
}

前面說了由BIOS的中斷給我們提供了可用的內存區列表,並且存在變量e820中,那麼我們應該要將e820中標識的可用的內存列表都在內存分配器中標識爲可用的內存。這個函數就是完成這個功能的,它通過對列表的遍歷,找到每一個可用的內存域所在的頁,然後標識該頁可用。標記爲可用是通過調用free_bootmem函數完成的。

void __init free_bootmem(unsigned long addr, unsigned long size)
{
	free_bootmem_core(NODE_DATA(0)->bdata, addr, size);
}
static void __init free_bootmem_core(bootmem_data_t *bdata, unsigned long addr,
				     unsigned long size)
{
	unsigned long sidx, eidx;
	unsigned long i;


	/*
	 * round down end of usable mem, partially free pages are
	 * considered reserved.
	 */
	BUG_ON(!size);
	BUG_ON(PFN_DOWN(addr + size) > bdata->node_low_pfn);


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


	/*
	 * Round up the beginning of the address.
	 */
	sidx = PFN_UP(addr) - PFN_DOWN(bdata->node_boot_start);
	eidx = PFN_DOWN(addr + size - bdata->node_boot_start);


	for (i = sidx; i < eidx; i++) {
		if (unlikely(!test_and_clear_bit(i, bdata->node_bootmem_map)))
			BUG();
	}
}


這個函數其實是釋放內存的函數,就是釋放從addr開始的內存頁

  1. 先檢查所需要釋放的內存是否超過了分配器的最大內存頁。
  2. 然後要注意的是:這裏只釋放內存中的整頁。由sidx和eidx的計算可以知道,它會計算完全包含在該內存中的、將被釋放的頁,如果只有部分包含在內存區中的頁是被忽略的。這個過程肯定是有風險的,如果頁包含在兩個不同的內存區中,那麼連續釋放這些內存區,卻無法釋放這個頁的內存,在釋放所有的頁後,分配器無法知道這個頁是否還在使用,也沒辦法再去釋放它了,因爲這個頁的狀態一直都是標記爲使用。所以這個分配器是簡單的,但它是可行的,爲什麼呢?因爲它只用於系統初始化期間,並且系統初始化期間所分配的大多數內存都會一直被使用,直到這個分配器停止使用,新的分配器開始工作,就是夥伴系統了。
  3. 調用test_and_clear_bit函數將這個BIT位標記爲0。

四,預留內存

      在上面把系統的可用內存都標記爲可用,但此時系統正在使用一些內存,需要把這些內存相應的標記出來,這通過調用reserve_bootmem函數完成。

#define reserve_bootmem(addr, size) \
	reserve_bootmem_node(NODE_DATA(0), (addr), (size))
void __init reserve_bootmem_node(pg_data_t *pgdat, unsigned long physaddr,
				 unsigned long size)
{
	reserve_bootmem_core(pgdat->bdata, physaddr, size);
}
static void __init reserve_bootmem_core(bootmem_data_t *bdata, unsigned long addr,
					unsigned long size)
{
	unsigned long sidx, eidx;
	unsigned long i;


	/*
	 * round up, partially reserved pages are considered
	 * fully reserved.
	 */
	BUG_ON(!size);
	BUG_ON(PFN_DOWN(addr) >= bdata->node_low_pfn);
	BUG_ON(PFN_UP(addr + size) > bdata->node_low_pfn);


	sidx = PFN_DOWN(addr - bdata->node_boot_start);
	eidx = PFN_UP(addr + size - bdata->node_boot_start);


	for (i = sidx; i < eidx; i++)
		if (test_and_set_bit(i, bdata->node_bootmem_map)) {
#ifdef CONFIG_DEBUG_BOOTMEM
			printk("hm, page %08lx reserved twice.\n", i*PAGE_SIZE);
#endif
		}
}


這個函數和前面的有些類似,就是將對應的頁標記爲已經使用。注意這個內存區域的計算,和前面的計算是相反的,前面是計算完全包含在內存中的內存頁,這個計算是被完全包含在內存頁中那些內存頁。


五,內核分配內存

        內核提供一些函數,用於向bootmem分配器索要內存,提供了很多系列的接口,如alloc_bootmem(size)、alloc_bootmem_pages(size)等等,這些接口最終調用__alloc_bootmem函數完成分配。

void * __init __alloc_bootmem(unsigned long size, unsigned long align,
			      unsigned long goal)
{
	void *mem = __alloc_bootmem_nopanic(size,align,goal);

	if (mem)
		return mem;
	/*
	 * Whoops, we cannot satisfy the allocation request.
	 */
	printk(KERN_ALERT "bootmem alloc of %lu bytes failed!\n", size);
	panic("Out of memory");
	return NULL;
}
這個函數也是__alloc_bootmem_nopanic函數的一個前端。

void * __init __alloc_bootmem_nopanic(unsigned long size, unsigned long align,
				      unsigned long goal)
{
	bootmem_data_t *bdata;
	void *ptr;

	list_for_each_entry(bdata, &bdata_list, list) {
		ptr = __alloc_bootmem_core(bdata, size, align, goal, 0);
		if (ptr)
			return ptr;
	}
	return NULL;
}
這個函數會實際的分配,之前我們知道多個分配器被一個全局變量鏈接到一個鏈表上,這裏會遍歷整個鏈表,調用__alloc_bootmem_core來分配內存,這個函數比較長,函數所需要參數有

  1. 分配器指針,表示從這個分配器上分配。
  2. 分配的大小。
  3. 對齊方式,如果需要分配頁,此時分配的對齊方式就是頁對齊了。
  4. goal,表示從哪開始掃描這個分配器的位圖。
  5. 內存分配的限制,表示不能分配limit指定的後面的頁。

void * __init
__alloc_bootmem_core(struct bootmem_data *bdata, unsigned long size,
	      unsigned long align, unsigned long goal, unsigned long limit)
{
	unsigned long offset, remaining_size, areasize, preferred;
	unsigned long i, start = 0, incr, eidx, end_pfn;
	void *ret;

	if (!size) {
		printk("__alloc_bootmem_core(): zero-sized request\n");
		BUG();
	}
	BUG_ON(align & (align-1));

	if (limit && bdata->node_boot_start >= limit)
		return NULL;

	/* on nodes without memory - bootmem_map is NULL */
	if (!bdata->node_bootmem_map)
		return NULL;

	end_pfn = bdata->node_low_pfn;
	limit = PFN_DOWN(limit);
	if (limit && end_pfn > limit)
		end_pfn = limit;

	eidx = end_pfn - PFN_DOWN(bdata->node_boot_start);
	offset = 0;
	if (align && (bdata->node_boot_start & (align - 1UL)) != 0)
		offset = align - (bdata->node_boot_start & (align - 1UL));
	offset = PFN_DOWN(offset);

	/*
	 * We try to allocate bootmem pages above 'goal'
	 * first, then we try to allocate lower pages.
	 */
	if (goal && goal >= bdata->node_boot_start && PFN_DOWN(goal) < end_pfn) {
		preferred = goal - bdata->node_boot_start;

		if (bdata->last_success >= preferred)
			if (!limit || (limit && limit > bdata->last_success))
				preferred = bdata->last_success;
	} else
		preferred = 0;

	preferred = PFN_DOWN(ALIGN(preferred, align)) + offset;
	areasize = (size + PAGE_SIZE-1) / PAGE_SIZE;
	incr = align >> PAGE_SHIFT ? : 1;

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 (i >= eidx)
			break;
		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;

found:
	bdata->last_success = PFN_PHYS(start);
	BUG_ON(start >= eidx);

	/*
	 * Is the next page of the previous allocation-end the start
	 * of this allocation's buffer? If yes then we can 'merge'
	 * the previous partial page with this allocation.
	 */
	if (align < PAGE_SIZE &&
	    bdata->last_offset && bdata->last_pos+1 == start) {
		offset = ALIGN(bdata->last_offset, align);
		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);
	}

	/*
	 * Reserve the area now:
	 */
	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;
}
這個函數的中心思想是從指定的位置開始掃描位圖,如果找到了滿足分配要求的,就馬上分配。執行以下一些操作:

  1. 從goal所指定的位置開始掃描位圖,找到滿足條件的空閒內存區域。
  2. 如果目標頁緊接着上一次分配的頁,即bootmem_data->last_pos所指定的頁,那麼內核會檢查bootmem_data->last_offset,判斷這次請求的內存能不能從上一個頁開始分配。
  3. 新分配的頁在位圖對應的比特位設置爲1,最後一頁的數目也保存在bootmem_data->lastpos中,如果這個頁沒有完全分配,那麼保存分配的偏移至last_offset中,否則將這個值表示爲0。注意,如果分配的時候,要求是頁對齊的,此時不能從偏移開始分配,頁要從新頁開始分配。
  4. 分配成功之後,返回分配的內存域的起始地址。
六,內存的釋放
      內存的釋放在前面已經介紹過了,通過調用free_bootmem函數完成,前面也說了,這個函數一般很少使用。原因也是上面的原因。

七,停用bootmem分配器
      這個分配器因爲簡單,所以用於系統的初期,在夥伴系統開始接手內存管理工作之後,這個分配器的歷史使命就完成了,此時需要停止這個分配器。可以通過函數free_all_bootmem等來完成。基本的步驟是:
  1. 掃描分配器中的頁位圖,釋放沒有使用的頁,此時釋放頁是通過調用_free_pages_bootmem函數,爲什麼呢?因爲這時釋放的內存需要釋放到夥伴系統中,而不是這個分配器中,_free_pages_bootmem函數會調用__free_pages函數,而後者是夥伴系統的標準內核接口函數,在這釋放之後,夥伴系統就可以使用剛剛被從這個分配器中釋放的頁了。
  2. 然後釋放分配器本身的內存佔用,其實主要是位圖所佔用的內存。
    	page = virt_to_page(bdata->node_bootmem_map);
    	count = 0;
    	idx = (get_mapsize(bdata) + PAGE_SIZE-1) >> PAGE_SHIFT;
    	for (i = 0; i < idx; i++, page++) {
    		__free_pages_bootmem(page, 0);
    		count++;
    	}
    	total += count;
    	bdata->node_bootmem_map = NULL;

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