linux內存管理筆記(十八)----bootmem內存分配器

前面章節我們介紹了memblock,其作用內核啓動初期,常用的內存分配器還未被初始化而不能使用,在此期間memblock是一種用於內存管理區域的方法。然後調用page_init來完成系統分頁機制的初始化工作,建立頁表,從而內核可以完成虛擬地址到物理地址的映射關係,本章主要是分析bootmem_init的流程。

1. bootm初始化

arm架構下, 在setup_arch中通過paging_init函數初始化內核分頁機制之後, 內核通過bootmem_init()開始完成內存結點和內存區域的初始化工作,其函數定義在arch/arm/mm/init.c中,如下所示

void __init bootmem_init(void)
{
	unsigned long min, max_low, max_high;

	memblock_allow_resize();                                                           -------------(1)
	max_low = max_high = 0;

	find_limits(&min, &max_low, &max_high);

	early_memtest((phys_addr_t)min << PAGE_SHIFT,
		      (phys_addr_t)max_low << PAGE_SHIFT);


	arm_memory_present();                                                              -------------(2)
	sparse_init();                                                                     -------------(3)
	zone_sizes_init(min, max_low, max_high);                                          -------------(4)
	min_low_pfn = min;
	max_low_pfn = max_low;
	max_pfn = max_high;
}
  1. 通過memblock拿到limit,DRAM的起始地址的頁面號,分別爲min = 0x80000, max_low = 0xa0000,內存的結束地址max_high = 0xa0000,early_memtest做內存的Memtest使用,最終會賦值給min_low_pfn(內存塊的起始幀號),max_low_pfn(normal結束幀號),max_pfn(內存塊的結束幀號)。
  2. arm_memory_present是通過CONFIG_SPARSEMEM來定義的,對於現在ARM32該宏沒有定義,暫不分析,以後單獨討論。其主要是linux內核已經實現了內存熱插的支持,當一個linux系統不管運行在 物理環境 或者虛擬環境 時只要宿主能提供內存熱插拔機制,linux內核就能相應的增加或者減少內存。
  3. 啓動並運行bootmem分配器,對於ARM32位系統,該功能不支持,幾乎沒有做什麼
  4. zone_sizes_init()來初始化節點和管理區的一些數據項, 其中關鍵的是初始化了系統中各個內存域的頁幀邊界,保存在max_zone_pfn數組,從min_low_pfn到max_low_pfn是ZONE_NORMAL,max_low_pfn到max_pfn是ZONE_HIGHMEM。

2.zone_sizes_init初始化

static void __init zone_sizes_init(unsigned long min, unsigned long max_low,
	unsigned long max_high)
{
	unsigned long zone_size[MAX_NR_ZONES], zhole_size[MAX_NR_ZONES];                   -------------(1)
	struct memblock_region *reg;
	memset(zone_size, 0, sizeof(zone_size));
	zone_size[0] = max_low - min;
#ifdef CONFIG_HIGHMEM
	zone_size[ZONE_HIGHMEM] = max_high - max_low;
#endif

	/*
	 * Calculate the size of the holes.
	 *  holes = node_size - sum(bank_sizes)
	 */
	memcpy(zhole_size, zone_size, sizeof(zhole_size));                              -------------(2)
	for_each_memblock(memory, reg) {
		unsigned long start = memblock_region_memory_base_pfn(reg);
		unsigned long end = memblock_region_memory_end_pfn(reg);

		if (start < max_low) {
			unsigned long low_end = min(end, max_low);
			zhole_size[0] -= low_end - start;
		}
#ifdef CONFIG_HIGHMEM
		if (end > max_low) {
			unsigned long high_start = max(start, max_low);
			zhole_size[ZONE_HIGHMEM] -= end - high_start;
		}
#endif
	}

#ifdef CONFIG_ZONE_DMA                                                              -------------(3)
	/*
	 * Adjust the sizes according to any special requirements for
	 * this machine type.
	 */
	if (arm_dma_zone_size)
		arm_adjust_dma_zone(zone_size, zhole_size,
			arm_dma_zone_size >> PAGE_SHIFT);
#endif

	free_area_init_node(0, zone_size, min, zhole_size);                             -------------(4)
}
  • 統計zone_size[0]和zone_size[ZONE_HIGHMEM]的大小,zone_size[0] = 0x20000,zone_size[ZONE_HIGHMEM] = 0

  • 最終只是配置了zole_size[0],並且其值爲0

  • 如果定義了CONFIG_ZONE_DMA,通過arm_dma_zone_size來配置DMA的內存域,該區域的長度依於處理器類型。在IA-32計算機上,一般的限制爲16MB,在我們現在使用的處理器上,CONFIG_ZONE_DMA沒有定義,所以只有ZONE_NORMAL和ZONE_HIGHMEM兩種。

  • 進入到最關鍵的地方,free_area_init_node用來針對特定的節點進行初始化

    zone_size[]數據用於保持不同ZONE類型具有的頁表,zhole_size數組用於保持不同的ZONE類型具有的空洞的頁數,如下圖所示

    在這裏插入圖片描述

接下來看看free_area_init_node的實現接口

void __paginginit free_area_init_node(int nid, unsigned long *zones_size,
		unsigned long node_start_pfn, unsigned long *zholes_size)
{
	pg_data_t *pgdat = NODE_DATA(nid);                                            -----------------(1)
	unsigned long start_pfn = 0;
	unsigned long end_pfn = 0;

	/* pg_data_t should be reset to zero when it's allocated */
	WARN_ON(pgdat->nr_zones || pgdat->kswapd_classzone_idx);

	pgdat->node_id = nid;
	pgdat->node_start_pfn = node_start_pfn;
	pgdat->per_cpu_nodestats = NULL;
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
	get_pfn_range_for_nid(nid, &start_pfn, &end_pfn);
	pr_info("Initmem setup node %d [mem %#018Lx-%#018Lx]\n", nid,
		(u64)start_pfn << PAGE_SHIFT,
		end_pfn ? ((u64)end_pfn << PAGE_SHIFT) - 1 : 0);
#else
	start_pfn = node_start_pfn;
#endif
	calculate_node_totalpages(pgdat, start_pfn, end_pfn,                        -----------------(2)
				  zones_size, zholes_size);

	alloc_node_mem_map(pgdat);                                                  -----------------(3)
#ifdef CONFIG_FLAT_NODE_MEM_MAP
	printk(KERN_DEBUG "free_area_init_node: node %d, pgdat %08lx, node_mem_map %08lx\n",
		nid, (unsigned long)pgdat,
		(unsigned long)pgdat->node_mem_map);
#endif

	reset_deferred_meminit(pgdat);                                              -----------------(4)
	free_area_init_core(pgdat);                                                 -----------------(5)
}
  • 在NUMA有多個節點,而每個節點內,訪問內存的時間是相同的,不同的節點,訪問內存的時間可以不同。而對於UMA,只有一個節點,取得該node的pg_data_t數據結構變量,每個node都有一個pg_data_t變量描述,進行初始化工作。node_id=0,node_start_fn = 0x80000, start_fn = 0x80000
  • 對節點長度和節點總可用頁面數進行初始化。calculate_node_totalpages函數是通過調用zone_spanned_pages_in_node和zone_absent_pages_in_node函數實現的,主要是爲pgdat的成員變量(包括空洞在內的總頁數(node_spanned_pages))和除空洞外的頁數(node_present_pages)設置值
  • alloc_node_mem_map() 初始化節點的局部映射地址,即pg_data_t->node_mem_map。在NUMA中,全局mem_map指向系統第一個節點的地址,系統中每個節點的起始地址,都對應在全局mem_map的某個位置。在UMA中,全局mem_map就是節點的node_mem_map
  • 由於CONFIG_DEFERRED_STRUCT_PAGE_INIT未定義,該函數爲空
  • 調用free_area_init_core()來真正初始化每個struct zone中的成員,填充pgdat的ZONE結構體。

2.1 calculate_node_totalpages初始化

對於該函數主要是用來計算每一個zone的總頁數和實際頁數(不包含空洞),以及內存節點的總頁數和實際頁數(不包含空洞),其代碼實現如下

static void __meminit calculate_node_totalpages(struct pglist_data *pgdat,
						unsigned long node_start_pfn,
						unsigned long node_end_pfn,
						unsigned long *zones_size,
						unsigned long *zholes_size)
{
	unsigned long realtotalpages = 0, totalpages = 0;
	enum zone_type i;

	for (i = 0; i < MAX_NR_ZONES; i++) {
		struct zone *zone = pgdat->node_zones + i;
		unsigned long zone_start_pfn, zone_end_pfn;
		unsigned long size, real_size;

		size = zone_spanned_pages_in_node(pgdat->node_id, i,
						  node_start_pfn,
						  node_end_pfn,
						  &zone_start_pfn,
						  &zone_end_pfn,
						  zones_size);
		real_size = size - zone_absent_pages_in_node(pgdat->node_id, i,
						  node_start_pfn, node_end_pfn,
						  zholes_size);
		if (size)
			zone->zone_start_pfn = zone_start_pfn;
		else
			zone->zone_start_pfn = 0;
		zone->spanned_pages = size;
		zone->present_pages = real_size;

		totalpages += size;
		realtotalpages += real_size;
	}

	pgdat->node_spanned_pages = totalpages;
	pgdat->node_present_pages = realtotalpages;
	printk(KERN_DEBUG "On node %d totalpages: %lu\n", pgdat->node_id,
							realtotalpages);
}

該函數主要計算各個ZONE區的page數目,對於ZONE區,其主要有以下3個

  • ZONE_DMA,該管理區是一些設備無法使用DMA訪問所有地址的範圍,因此特意劃分出來的一塊內存,專門用於特殊DMA訪問分配使用的區域。比如x86架構此區域爲0-16M。本處理器該區域不存在

  • ZONE_NORMAL:直接映射區,含有的頁面數爲0x20000

  • ZONE_HIGHMEM:高端內存管理區,申請的內存,需要內核進行map後才能訪問

  • ZONE_MOVABLE:這個區域是一個特殊的存在,主要是爲了支持memory hotplug功能,所以MOVABLE表示可移除,其實它也表示可遷移。本架構CPU不支持該功能。

    簡單來說,可遷移的頁面不一定都在ZONE_MOVABLE中,但是ZONE_MOVABLE中的也頁面必須都是可遷移的,我們通過查看/proc/pagetypeinfo來看下實例:

在這裏插入圖片描述

ZONE_MOVABLE這個管理區,主要是和memory hotplug功能有關,爲什麼要設計內存熱插拔功能,主要是爲了如下兩點考慮:
1.邏輯內存熱插拔,對於虛擬機的支持,對於虛擬機按照需求來分配可用內存
2.物理內存熱插拔,對於NUMA服務器的支持,不需要的內存就設置爲offline,以降低功耗
3.優化內存碎片問題

2.2 alloc_node_mem_map

在linux內核中,所有的物理內存都用struct page結構來描述,這些對象以數組形式存放,而這個數組的地址就是mem_map。內核以節點node爲單位,每個node下的物理內存統一管理,也就是說在表示內存node的描述類型struct pglist_data中,有node_mem_map這個成員,其針對平坦型內存進行描述(CONFIG_FLAT_NODE_MEM_MAP)。如果系統只有一個pglist_data對象,那麼此對象下的node_mem_map即爲全局對象mem_map。函數alloc_remap()就是針對節點node的node_mem_map處理

static void __ref alloc_node_mem_map(struct pglist_data *pgdat)
{
	unsigned long __maybe_unused start = 0;
	unsigned long __maybe_unused offset = 0;

	/* Skip empty nodes */
	if (!pgdat->node_spanned_pages)                                                ------------------(1)
		return;

#ifdef CONFIG_FLAT_NODE_MEM_MAP
	start = pgdat->node_start_pfn & ~(MAX_ORDER_NR_PAGES - 1);                     ------------------(2)
	offset = pgdat->node_start_pfn - start;
	/* ia64 gets its own node_mem_map, before this, without bootmem */
	if (!pgdat->node_mem_map) {
		unsigned long size, end;
		struct page *map;
		/*
		 * The zone's endpoints aren't required to be MAX_ORDER
		 * aligned but the node_mem_map endpoints must be in order
		 * for the buddy allocator to function correctly.
		 */
		end = pgdat_end_pfn(pgdat);                                                ------------------(3)
		end = ALIGN(end, MAX_ORDER_NR_PAGES);
		size =  (end - start) * sizeof(struct page);                               ------------------(4)
		map = alloc_remap(pgdat->node_id, size);
		if (!map)
			map = memblock_virt_alloc_node_nopanic(size,                          ------------------(5)
							       pgdat->node_id);
		pgdat->node_mem_map = map + offset;
	}
#endif /* CONFIG_FLAT_NODE_MEM_MAP */
}
  • pgdat->node_spanned_pages此內存節點內無有效的內存,直接略過
  • 起始地址必須對其,這個一般按照MB級別對齊即可,對齊後地址與真正開始地址之間的偏移大小,start = 0x80000,offset = 0
  • 獲取節點內結束頁幀號pfn,然後對齊,end = 0xa0000,
  • 計算需要的數組大小,需要注意end-start是頁幀個數(0xa0000 - 0x80000 = 0x40000),每個頁需要一個struct page對象,所以,這裏是乘關係,這樣得到整個node內所有以page爲單位描述需要佔據的內存
  • 如果這裏分配失敗,則通過memblock管理算法分配內存。

2.3 free_area_init_core

static void __paginginit free_area_init_core(struct pglist_data *pgdat)
{
	enum zone_type j;
	int nid = pgdat->node_id;
	int ret;

	pgdat_resize_init(pgdat);                                                      ------------------(1)
#ifdef CONFIG_NUMA_BALANCING
	spin_lock_init(&pgdat->numabalancing_migrate_lock);
	pgdat->numabalancing_migrate_nr_pages = 0;
	pgdat->numabalancing_migrate_next_window = jiffies;
#endif
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
	spin_lock_init(&pgdat->split_queue_lock);
	INIT_LIST_HEAD(&pgdat->split_queue);
	pgdat->split_queue_len = 0;
#endif
	init_waitqueue_head(&pgdat->kswapd_wait);
	init_waitqueue_head(&pgdat->pfmemalloc_wait);
#ifdef CONFIG_COMPACTION
	init_waitqueue_head(&pgdat->kcompactd_wait);
#endif
	pgdat_page_ext_init(pgdat);
	spin_lock_init(&pgdat->lru_lock);
	lruvec_init(node_lruvec(pgdat));

	for (j = 0; j < MAX_NR_ZONES; j++) {                                        ------------------(2)
		struct zone *zone = pgdat->node_zones + j;
		unsigned long size, realsize, freesize, memmap_pages;
		unsigned long zone_start_pfn = zone->zone_start_pfn;

		size = zone->spanned_pages;
		realsize = freesize = zone->present_pages;

		/*
		 * Adjust freesize so that it accounts for how much memory
		 * is used by this zone for memmap. This affects the watermark
		 * and per-cpu initialisations
		 */
		memmap_pages = calc_memmap_size(size, realsize);
		if (!is_highmem_idx(j)) {
			if (freesize >= memmap_pages) {
				freesize -= memmap_pages;
				if (memmap_pages)
					printk(KERN_DEBUG
					       "  %s zone: %lu pages used for memmap\n",
					       zone_names[j], memmap_pages);
			} else
				pr_warn("  %s zone: %lu pages exceeds freesize %lu\n",
					zone_names[j], memmap_pages, freesize);
		}

		/* Account for reserved pages */
		if (j == 0 && freesize > dma_reserve) {
			freesize -= dma_reserve;
			printk(KERN_DEBUG "  %s zone: %lu pages reserved\n",
					zone_names[0], dma_reserve);
		}

		if (!is_highmem_idx(j))
			nr_kernel_pages += freesize;
		/* Charge for highmem memmap if there are enough kernel pages */
		else if (nr_kernel_pages > memmap_pages * 2)
			nr_kernel_pages -= memmap_pages;
		nr_all_pages += freesize;

		/*
		 * Set an approximate value for lowmem here, it will be adjusted
		 * when the bootmem allocator frees pages into the buddy system.
		 * And all highmem pages will be managed by the buddy system.
		 */
		zone->managed_pages = is_highmem_idx(j) ? realsize : freesize;
#ifdef CONFIG_NUMA
		zone->node = nid;
#endif
		zone->name = zone_names[j];
		zone->zone_pgdat = pgdat;
		spin_lock_init(&zone->lock);
		zone_seqlock_init(zone);
		zone_pcp_init(zone);                                                    ------------------(3)

		if (!size)
			continue;

		set_pageblock_order();
		setup_usemap(pgdat, zone, zone_start_pfn, size);
		ret = init_currently_empty_zone(zone, zone_start_pfn, size);            ------------------(4)
		BUG_ON(ret);
		memmap_init(size, nid, j, zone_start_pfn);                              ------------------(5)
	}
}
  • 主要是初始化struct pglist_data,首先初始化pgdat->node_size_lock自旋鎖初始化,初始化pgdat->kswapd_wait等待隊列,初始化頁換出守護進程創建空閒塊的大小

  • 遍歷各個zone區域,進行如下初始化:

    • 根據spanned_pages和present_pages,調用calc_memmap_size計算管理該zone所需的struct page結構所佔的頁面數memmap_pages

    • zone中的freesize表示可用的區域,需要減去memmap_pages和dma_reserve的區域,如下開發板的Log打印所示:memmap使用1024頁,DMA 保留0頁

在這裏插入圖片描述

  • 計算nr_kernel_pagesnr_all_pages的數量

  • 初始化zone的其他變量和各種鎖

  • 初始化zone結構體的per_cpu_pageset結構體變量pageset,per_cpu_pageset按CPU進行管理。它不直接返回夥伴系統,爲快速分配而按不同CPU持有頁。

  • 初始化zone結構體的free_area結構體。

  • memmap_init函數在與頁幀具有1:1映射關係的頁數組中,向相應的頁幀的page結構體的flags成員設置PG_reserved位。

最後,當我們回顧bootmem_init函數時,發現它基本上完成了linux物理內存框架的初始化,包括Node, Zone, Page Frame,以及對應的數據結構等。

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