Linux内存管理(5):分页机制和管理区初始化

    1、初始化启动内存分配器

    在内存子系统初始化以前,即boot阶段也需要进行内存管理,启动内存分配器是专为此而设计的。linux启动内存分配器是在伙伴系统、slab机制实现之前,为满足内核中内存的分配而建立的。本身的机制比较简单,使用位图来进行标志分配和释放。arch/x86/kernel/setup.c:setup_arch()在用init_memory_mapping(0, max_low_pfn<<PAGE_SHIFT)建立完内核页表之后,就会调用arch/x86/mm/init_32.c:initmem_init(0, max_pfn)启动bootmem内存分配器。如下:

#ifndef CONFIG_NEED_MULTIPLE_NODES
void __init initmem_init(unsigned long start_pfn,
				  unsigned long end_pfn)
{
#ifdef CONFIG_HIGHMEM
	highstart_pfn = highend_pfn = max_pfn;
	if (max_pfn > max_low_pfn)
		highstart_pfn = max_low_pfn;
	/* 注册内存活动区 */
	e820_register_active_regions(0, 0, highend_pfn);
	sparse_memory_present_with_active_regions(0);
	printk(KERN_NOTICE "%ldMB HIGHMEM available.\n",
		pages_to_mb(highend_pfn - highstart_pfn));
	num_physpages = highend_pfn;
	/* 计算高端内存地址 */
	high_memory = (void *) __va(highstart_pfn * PAGE_SIZE - 1) + 1;
#else
	e820_register_active_regions(0, 0, max_low_pfn);
	sparse_memory_present_with_active_regions(0);
	num_physpages = max_low_pfn;
	high_memory = (void *) __va(max_low_pfn * PAGE_SIZE - 1) + 1;
#endif
#ifdef CONFIG_FLATMEM
	max_mapnr = num_physpages;
#endif
	__vmalloc_start_set = true;

	printk(KERN_NOTICE "%ldMB LOWMEM available.\n",
			pages_to_mb(max_low_pfn));

	setup_bootmem_allocator(); /* 启动内存分配器 */
}
#endif /* !CONFIG_NEED_MULTIPLE_NODES */
    主要工作是调用e820_register_active_regions()在节点0上注册内存活动区,然后调用setup_bootmem_allocator()建立启动内存分配器。Linux的内存活动区域其实就是全局变量e820中的内存块做了相关检查和处理后的区域,它会在管理区初始化等地方被用到。注册时,要根据是否配置了高端内存来决定活动的区的终止地址。
    函数e820_register_active_regions()在arch/x86/kernel/e820.c中,它扫描e820内存图,并在一个节点nid上注册活动区。如下:

/* 扫描e820内存图,并在一个节点上注册活动区 */
void __init e820_register_active_regions(int nid, unsigned long start_pfn,
					 unsigned long last_pfn)
{
	unsigned long ei_startpfn;
	unsigned long ei_endpfn;
	int i;

	for (i = 0; i < e820.nr_map; i++)
		/* 从全局变量e820中查找活动区 */
		if (e820_find_active_region(&e820.map[i],
					    start_pfn, last_pfn,
					    &ei_startpfn, &ei_endpfn))
			/* 添加查找到的活动区 */
			add_active_range(nid, ei_startpfn, ei_endpfn);
}

/*
 * 在start_pfn到last_pfn的地址范围内查找一个活动区,并在ei_startpfn和ei_endpfn中返回
 * 这个e820内存块的范围
 */
int __init e820_find_active_region(const struct e820entry *ei,
				  unsigned long start_pfn,
				  unsigned long last_pfn,
				  unsigned long *ei_startpfn,
				  unsigned long *ei_endpfn)
{
	u64 align = PAGE_SIZE;

	*ei_startpfn = round_up(ei->addr, align) >> PAGE_SHIFT;
	*ei_endpfn = round_down(ei->addr + ei->size, align) >> PAGE_SHIFT;

	/* 跳过内存图中比一个页面还小的各个内存块 */
	if (*ei_startpfn >= *ei_endpfn)
		return 0;

	/* 如果内存图中的所有内存块都在节点范围之外,则跳过 */
	if (ei->type != E820_RAM || *ei_endpfn <= start_pfn ||
				    *ei_startpfn >= last_pfn)
		return 0;

	/* 检查是否有重叠 */
	if (*ei_startpfn < start_pfn)
		*ei_startpfn = start_pfn;
	if (*ei_endpfn > last_pfn)
		*ei_endpfn = last_pfn;

	return 1;
}
    主要的工作是在start_pfn到last_pfn的地址范围内,从e820内存图的各内存块中查找一个物理活动区,若找到,则把其物理地址范围保存到ei_startpfn和ei_endpfn中,然后调用mm/page_alloc.c中的add_active_range()函数在nid节点上注册这块活动区。如下:
/* 添加活动区域,需要对原有的进行检查 */
void __init add_active_range(unsigned int nid, unsigned long start_pfn,
						unsigned long end_pfn)
{
	int i;

	mminit_dprintk(MMINIT_TRACE, "memory_register",
			"Entering add_active_range(%d, %#lx, %#lx) "
			"%d entries of %d used\n",
			nid, start_pfn, end_pfn,
			nr_nodemap_entries, MAX_ACTIVE_REGIONS);

	mminit_validate_memmodel_limits(&start_pfn, &end_pfn);

	/* 如果可能,与存在的活动内存区合并 */
	for (i = 0; i < nr_nodemap_entries; i++) {
		if (early_node_map[i].nid != nid)
			continue;

		/* 如果一个存在的活动区包含这个要添加的新区,则跳过 */
		if (start_pfn >= early_node_map[i].start_pfn &&
				end_pfn <= early_node_map[i].end_pfn)
			return;

		/* 如果合适,则向前合并 */
		if (start_pfn <= early_node_map[i].end_pfn &&
				end_pfn > early_node_map[i].end_pfn) {
			early_node_map[i].end_pfn = end_pfn;
			return;
		}

		/* 如果合适,则向后合并 */
		if (start_pfn < early_node_map[i].end_pfn &&
				end_pfn >= early_node_map[i].start_pfn) {
			early_node_map[i].start_pfn = start_pfn;
			return;
		}
	}

	/* 检查early_node_map是否足够大 */
	if (i >= MAX_ACTIVE_REGIONS) {
		printk(KERN_CRIT "More than %d memory regions, truncating\n",
							MAX_ACTIVE_REGIONS);
		return;
	}

	early_node_map[i].nid = nid;
	early_node_map[i].start_pfn = start_pfn;
	early_node_map[i].end_pfn = end_pfn;
	nr_nodemap_entries = i + 1;
}
    该函数注册一段页框范围指定的物理内存活动区,参数nid为要注册到的节点编号,start_pfn为可用物理内存的开始PFN(物理页框号),end_pfn为可用物理内存的终止PFN。这些活动被存储在全局的early_node_map[]数组中(该数组也定义在mm/page_alloc.c中),表示内存管理的早期节点显现图。它会被以后的free_area_init_nodes()用来计算管理区大小和空洞数量。如果活动区范围跨越一个内存空洞,则需要确保内存不会被bootmem分配器释放(依赖于体系结构)。如果可能,要注册的活动区可以跟已存在的活动区合并。
    回到arch/x86/mm/init_32.c:initmem_init(),最后是调用arch/x86/mm/init_32.c:setup_bootmem_allocator()建立内核引导时的启动内存分配器。在建立启动内存分配器的时候,会涉及到保留内存。也就是说,当分配器进行内存分配时,之前保留给页表、分配器本身(用于映射的位图)、io的这些保留内存就不能再分配了。linux中对保留内存空间的部分用下列数据结构表示,在arch/x86/kernel/e820.c中:
/*
 * Early reserved memory areas.
 */
#define MAX_EARLY_RES 20  /* 保留空间最大块数 */

struct early_res {  /* 保留空间结构 */
	u64 start, end;
	char name[16];
	char overlap_ok;
};
/* 保留内存空间全局变量 */
static struct early_res early_res[MAX_EARLY_RES] __initdata = {
	{ 0, PAGE_SIZE, "BIOS data page" },	/* BIOS data page */
	{}
};
	bootmem分配器的数据结构bootmem_data_t用于管理启动内存的分配、释放等,在include/linux/bootmem.h中,如下:
/* 用于bootmem分配器的节点数据结构 */
typedef struct bootmem_data {
	unsigned long node_min_pfn;
	unsigned long node_low_pfn;
	void *node_bootmem_map;
	unsigned long last_end_off;
	unsigned long hint_idx;
	struct list_head list;
} bootmem_data_t;
    这些域分别为存放bootmem位图的第一个页面(即内核映象结束处的第一个页面)、低端内存最大页面号(物理内存的顶点,最高不超过896MB)、位图(各个位代表节点上的所有物理内存页,包括洞)、前一次分配的最后一个字节相对于last_pos的位移量、hint_idx为前一次分配的最后一个页面号、list是用于内存分配的链表。注意在内存节点pg_data_t数据结构中,用bdata指针批向了这个bootmem分配器的数据结构。
    全局链表定义可在mm/bootmeme.c中找到,如下:
static struct list_head bdata_list __initdata = LIST_HEAD_INIT(bdata_list);
    启动分配器的建立主要的流程为初始化映射位图、活动内存区的映射位置0(表示可用)、保留内存区域处理,其中保留区存放在上面介绍的全局数组中,这里只是将分配器中对应映射位图值1,表示已经分配。核心函数是arch/x86/mm/init_32.c:setup_bootmem_allocator(),以及setup_node_bootmem()。如下:
void __init setup_bootmem_allocator(void)
{
	int nodeid;
	unsigned long bootmap_size, bootmap;
	/*
	 * 初始化引导时的内存分配器(只是低端内存区):
	 */
	/* 计算所需要的映射页面大小一个字节一位,所以需要对总的页面大小除以8 */ 
	bootmap_size = bootmem_bootmap_pages(max_low_pfn)<<PAGE_SHIFT;
	/* 从e820中查找一个合适的内存块 */
	bootmap = find_e820_area(0, max_pfn_mapped<<PAGE_SHIFT, bootmap_size,
				 PAGE_SIZE);
	if (bootmap == -1L)
		panic("Cannot find bootmem map of size %ld\n", bootmap_size);
	/* 将用于位图映射的页面保留 */
	reserve_early(bootmap, bootmap + bootmap_size, "BOOTMAP");

	printk(KERN_INFO "  mapped low ram: 0 - %08lx\n",
		 max_pfn_mapped<<PAGE_SHIFT);
	printk(KERN_INFO "  low ram: 0 - %08lx\n", max_low_pfn<<PAGE_SHIFT);
	/* 扫描每个在线节点 */
	for_each_online_node(nodeid) {
		 unsigned long start_pfn, end_pfn;

#ifdef CONFIG_NEED_MULTIPLE_NODES
		/* 计算出当前节点的起始地址和终止地址 */
		start_pfn = node_start_pfn[nodeid];
		end_pfn = node_end_pfn[nodeid];
		if (start_pfn > max_low_pfn)
			continue;
		if (end_pfn > max_low_pfn)
			end_pfn = max_low_pfn;
#else
		start_pfn = 0;
		end_pfn = max_low_pfn;
#endif
		/* 对指定节点安装启动分配器 */
		bootmap = setup_node_bootmem(nodeid, start_pfn, end_pfn,
						 bootmap);
	}
	/* bootmem的分配制度到这里就已经建立完成,把after_bootmem变量置成1 */
	after_bootmem = 1;
}

static unsigned long __init setup_node_bootmem(int nodeid,
				 unsigned long start_pfn,
				 unsigned long end_pfn,
				 unsigned long bootmap)
{
	unsigned long bootmap_size;

	/* 初始化这个内存节点:将映射位图中的所有位置1。不要触及min_low_pfn */
	bootmap_size = init_bootmem_node(NODE_DATA(nodeid),
					 bootmap >> PAGE_SHIFT,
					 start_pfn, end_pfn);
	printk(KERN_INFO "  node %d low ram: %08lx - %08lx\n",
		nodeid, start_pfn<<PAGE_SHIFT, end_pfn<<PAGE_SHIFT);
	printk(KERN_INFO "  node %d bootmap %08lx - %08lx\n",
		 nodeid, bootmap, bootmap + bootmap_size);
	/* 将活动内存区对应位图相关位置0,表示可被分配的 */ 
	free_bootmem_with_active_regions(nodeid, end_pfn);
	/* 将保留内存的相关页面对应位置为1,表示已经分配
       或者不可用(不能被分配) */
	early_res_to_bootmem(start_pfn<<PAGE_SHIFT, end_pfn<<PAGE_SHIFT);
	/* 返回映射页面的最后地址,下次映射即可以从这里开始 */
	return bootmap + bootmap_size;
}
    设置分配器的主要工作是初始化引导时的内存分配器(只是低端内存区);在e820中查找引导内存块;对每个在线节点计算出其起始和终止地址,然后调用setup_node_bootmem()安装启动分配器。在这个函数中,调用init_bootmem_node()初始化这个节点的映射位图。将活动内存区对应位图相关位置0,表示可用;将保留内存的相关页面对应位置为1,表示已经分配(不可用)。其中初始化映射位图的函数init_bootmem_node()在mm/bootmem.c中,调用链为init_bootmem_node()--->init_bootmem_core()--->link_bootmem(bdata),最终将bdata添加到全局的bdata_list链表中。当所有在线内存节点设置好后,bootmem内存分配器就初始化完毕。
    mm/bootmem.c实现了完整的引导时物理内存分配器和配置器,包括内存节点初始化、内存分配、释放等各种操作。我们概述一下启动内存分配器的主要操作接口功能:
    init_bootmem_node():注册一个节点以作为启动内存。核心操作由init_bootmem_core()完成,每调用它一次来设置自己的分配器。
    link_bootmem():按顺序添加一个bdata到全局的bdata_list链表中。
    free_all_bootmem_node():释放一个节点的可用页面给伙伴系统。核心操作由free_all_bootmem_core()完成。
    free_bootmem_node():将指定节点上的一个页面范围标记为可用(即未分配)。
    reserve_bootmem_node():将指定节点上的一个页面范围标记为保留。
    __alloc_bootmem_node():为指定节点分配启动内存。核心操作由alloc_bootmem_core()完成。
    __free():bootmem分配器的释放内存操作。
    __reserve():bootmem分配器的保留内存操作。
    alloc_bootmem_core():bootmem分配器的分配内存操作。    
    2、建立永久的分页机制
    在前面的“内存映射机制“介绍中,init_memory_mapping()只是构建了内核页表,作为临时的分页映射。例如只对高端内存固定映射区创建了页表结构,并没有对高端内存区永久映射区进行初始化。setup_arch()在执行完init_memory_mapping()和initmem_init()后,就会调用arch/x86/mm/init_32.c:paging_init()建立虚拟内存管理要用到的完整页表和永久分页机制。如下:
void __init paging_init(void)
{
	pagetable_init();

	__flush_tlb_all();

	kmap_init();

	/*
	 * NOTE: 在这里bootmem分配器完全可用了
	 */
	sparse_init();
	zone_sizes_init();
}
    该函数建立完整的页表,注意起始的8MB已经被head_32.S映射了。该函数也会取消虚拟地址0处的页面映射,以便我们可以在内核中陷入并跟踪那些麻烦的NULL引用错误。它的主要工作包括页表初始化、内核永久映射区初始化、稀疏内存映射初始化、管理区初始化。下面重点讨论该函数。
    arch/x86/mm/init_32.c:pagetable_init()函数用于完成页表初始化,并初始化高端内存永久映射区。如下:
static void __init pagetable_init(void)
{
	pgd_t *pgd_base = swapper_pg_dir;

	permanent_kmaps_init(pgd_base);
}

#ifdef CONFIG_HIGHMEM
static void __init permanent_kmaps_init(pgd_t *pgd_base)
{
	unsigned long vaddr;
	pgd_t *pgd;
	pud_t *pud;
	pmd_t *pmd;
	pte_t *pte;

	vaddr = PKMAP_BASE;
	/* 该阶段,也就是永久内存映射区的页表初始化 */
	page_table_range_init(vaddr, vaddr + PAGE_SIZE*LAST_PKMAP, pgd_base);

	pgd = swapper_pg_dir + pgd_index(vaddr);
	pud = pud_offset(pgd, vaddr);
	pmd = pmd_offset(pud, vaddr);
	pte = pte_offset_kernel(pmd, vaddr);
	/* 将永久映射区间映射的第一个页表项保存到pkmap_page_table中 */
	pkmap_page_table = pte;
}
/* ...... */
#else
static inline void permanent_kmaps_init(pgd_t *pgd_base)
{
}
#endif /* CONFIG_HIGHMEM */
    根据上面代码,只有定义了使用高端内存,才会有高端永久映射区。首先用pgd_base保存页全局目录表的起始地址swapper_pg_dir。而后在函数permanent_kmaps_init()中,调用page_table_range_init()建立页表,这个函数在前面分析过,它会先根据永久映射区起始地址PKMAP_BASE,获取pgd表项索引、pmd表项索引,然后建立下一级pmd表,和最终的pte页表。第一个页表项保存到pkmap_page_table中。如果内核不划分高端内存,则permanent_kmaps_init()什么也不做。注意paging_init()初始化完页表后,要用__flush_tlb_all()刷新缓存TLB中的映射内容。
    arch/x86/mm/init_32.c:kmap_init()函数用于缓存第一个kmap页表项,如下:
static void __init kmap_init(void)
{
	unsigned long kmap_vstart;

	/*
	 * Cache the first kmap pte:
	 */
	/* 得到高端固定内存映射区域的起始内存的页表,将这个页表 
     放到kmap_pte变量中。确切的说应该是固定内存中的临时内存映射区域 */
	kmap_vstart = __fix_to_virt(FIX_KMAP_BEGIN);
	kmap_pte = kmap_get_fixmap_pte(kmap_vstart);

	kmap_prot = PAGE_KERNEL;
}
    该函数首先把高端固定映射区(即高端临时内存映射区)的起始地址FIX_KMAP_BEGIN转换成虚拟地址,然后获取它的pte页表项,并保存到全局的kmap_pte中。
    mm/sparse.c:sparse_init()函数用于初始稀疏内存的映射,这里就不展开了。这里重点介绍管理区初始化,这是内存管理的重要组成部分,在arch/x86/mm/init_32.c:zone_sizes_init()中,如下:
static void __init zone_sizes_init(void)
{
	/* 初始化各种管理区中的最大页面数,在后面用于具体的初始化工作 */
	unsigned long max_zone_pfns[MAX_NR_ZONES];
	memset(max_zone_pfns, 0, sizeof(max_zone_pfns));
	max_zone_pfns[ZONE_DMA] = /* DMA区的最大页面帧号,后面的类似 */
		virt_to_phys((char *)MAX_DMA_ADDRESS) >> PAGE_SHIFT;
	max_zone_pfns[ZONE_NORMAL] = max_low_pfn;
#ifdef CONFIG_HIGHMEM
	max_zone_pfns[ZONE_HIGHMEM] = highend_pfn;
#endif
	 /* 内存体系的MMU建立,包括伙伴系统的初步建立 */
	free_area_init_nodes(max_zone_pfns);
}
    在“内存描述”一节中对各种管理区类型做了详细介绍,这里首先用数组max_zone_pfns保存各种类型管理区的最大页面数,宏MAX_DMA_ADDRESS在arch/x86/include/asm/dma.h中定义,表示能执行DMA传输的最大地址,其中x86-32非PAE模式下MAX_DMA_ADDRESS为PAGE_OFFSET + 0x1000000,即从内核空间开始处的16MB为DMA区的地址范围,因此DMA区的地址范围为3G~3G+16M这一段空间。把这个最大地址转换成页帧号保存到max_zone_pfns数组,接着保存NORMAL区和HIGHMEM区的最大页面号。最后调用核心函数mm/page_alloc.c:free_area_init_nodes()初始化所有pg_data_t内存节点的各种管理区数据,传入参数为由各管理区最大PFN构成的数组。代码如下:
void __init free_area_init_nodes(unsigned long *max_zone_pfn)
{
	unsigned long nid;
	int i;

	/* Sort early_node_map as initialisation assumes it is sorted */
	sort_node_map(); /* 将活动区域进行排序 */

	/* 记录管理区的界限 */
	memset(arch_zone_lowest_possible_pfn, 0,
				sizeof(arch_zone_lowest_possible_pfn));
	memset(arch_zone_highest_possible_pfn, 0,
				sizeof(arch_zone_highest_possible_pfn));
	/* 找出活动内存中最小的页面 */
	arch_zone_lowest_possible_pfn[0] = find_min_pfn_with_active_regions();
	arch_zone_highest_possible_pfn[0] = max_zone_pfn[0];
	for (i = 1; i < MAX_NR_ZONES; i++) {
		if (i == ZONE_MOVABLE)
			continue;
		/* 假定区域连续,下一个区域的最小页面为上一个区的最大页面 */
		arch_zone_lowest_possible_pfn[i] =
			arch_zone_highest_possible_pfn[i-1];
		arch_zone_highest_possible_pfn[i] =
			max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]);
	}
	/* 对ZONE_MOVABLE区域设置为0 */
	arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0;
	arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0;

	/* 找出每个节点上ZONE_MOVABLE区的开始页面号 */
	memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn));
	find_zone_movable_pfns_for_nodes(zone_movable_pfn);

	/* 打印管理区的范围 */
	printk("Zone PFN ranges:\n");
	for (i = 0; i < MAX_NR_ZONES; i++) {
		if (i == ZONE_MOVABLE)
			continue;
		printk("  %-8s %0#10lx -> %0#10lx\n",
				zone_names[i],
				arch_zone_lowest_possible_pfn[i],
				arch_zone_highest_possible_pfn[i]);
	}

	/* 打印每个节点上ZONE_MOVABLE区开始的页面号 */
	printk("Movable zone start PFN for each node\n");
	for (i = 0; i < MAX_NUMNODES; i++) {
		if (zone_movable_pfn[i])
			printk("  Node %d: %lu\n", i, zone_movable_pfn[i]);
	}

	/* 打印early_node_map[] */
	printk("early_node_map[%d] active PFN ranges\n", nr_nodemap_entries);
	for (i = 0; i < nr_nodemap_entries; i++)
		printk("  %3d: %0#10lx -> %0#10lx\n", early_node_map[i].nid,
						early_node_map[i].start_pfn,
						early_node_map[i].end_pfn);

	/* 初始化每个节点 */
	mminit_verify_pageflags_layout(); /* 调试用 */
	setup_nr_node_ids();
	for_each_online_node(nid) {
		pg_data_t *pgdat = NODE_DATA(nid);
		/* zone中数据的初始化,伙伴系统建立,但是没有页面 
           和数据,页面在后面的mem_init中得到 */
		free_area_init_node(nid, NULL,
				find_min_pfn_for_node(nid), NULL);

		/* 对该节点上的任何内存区 */
		if (pgdat->node_present_pages)
			node_set_state(nid, N_HIGH_MEMORY);
		/* 内存的相关检查 */
		check_for_regular_memory(pgdat);
	}
}

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);

	pgdat->node_id = nid;
	/* 这个已在前面调用一个函数得到 */
	pgdat->node_start_pfn = node_start_pfn;
	/* 计算系统中节点nid的所有物理页面并保存在数据结构中 */
	calculate_node_totalpages(pgdat, zones_size, zholes_size);
	/* 当节点只有一个时,将节点的map保存到全局变量中 */
	alloc_node_mem_map(pgdat);
#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
	/* zone中相关数据的初始化,包括伙伴系统,等待队列,相关变量, 
        数据结构、链表等 */
	free_area_init_core(pgdat, zones_size, zholes_size);
}

static void __paginginit free_area_init_core(struct pglist_data *pgdat,
		unsigned long *zones_size, unsigned long *zholes_size)
{
	enum zone_type j;
	int nid = pgdat->node_id;
	unsigned long zone_start_pfn = pgdat->node_start_pfn;
	int ret;

	pgdat_resize_init(pgdat);
	pgdat->nr_zones = 0;
	init_waitqueue_head(&pgdat->kswapd_wait);
	pgdat->kswapd_max_order = 0;
	pgdat_page_cgroup_init(pgdat);
	
	for (j = 0; j < MAX_NR_ZONES; j++) {
		struct zone *zone = pgdat->node_zones + j;
		unsigned long size, realsize, memmap_pages;
		enum lru_list l;
		/* 下面的两个函数会获得指定节点的真实内存大小 */
		size = zone_spanned_pages_in_node(nid, j, zones_size);
		realsize = size - zone_absent_pages_in_node(nid, j,
								zholes_size);

		/*
		 * Adjust realsize 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 = /* 存放页面所需要的内存大小 */
			PAGE_ALIGN(size * sizeof(struct page)) >> PAGE_SHIFT;
		if (realsize >= memmap_pages) {
			realsize -= memmap_pages;
			if (memmap_pages)
				printk(KERN_DEBUG
				       "  %s zone: %lu pages used for memmap\n",
				       zone_names[j], memmap_pages);
		} else
			printk(KERN_WARNING
				"  %s zone: %lu pages exceeds realsize %lu\n",
				zone_names[j], memmap_pages, realsize);

		/* Account for reserved pages */
		if (j == 0 && realsize > dma_reserve) {
			realsize -= dma_reserve; /* 减去为DMA保留的页面 */
			printk(KERN_DEBUG "  %s zone: %lu pages reserved\n",
					zone_names[0], dma_reserve);
		}
		/* 如果不是高端内存区 */
		if (!is_highmem_idx(j))
			nr_kernel_pages += realsize;
		nr_all_pages += realsize;
		
		/* 下面为初始化zone结构的相关变量 */
		zone->spanned_pages = size;
		zone->present_pages = realsize;
#ifdef CONFIG_NUMA
		zone->node = nid;
		zone->min_unmapped_pages = (realsize*sysctl_min_unmapped_ratio)
						/ 100;
		zone->min_slab_pages = (realsize * sysctl_min_slab_ratio) / 100;
#endif
		zone->name = zone_names[j];
		spin_lock_init(&zone->lock);
		spin_lock_init(&zone->lru_lock);
		zone_seqlock_init(zone);
		zone->zone_pgdat = pgdat;

		zone->prev_priority = DEF_PRIORITY;

		zone_pcp_init(zone);
		for_each_lru(l) { /* 初始化链表 */
			INIT_LIST_HEAD(&zone->lru[l].list);
			zone->reclaim_stat.nr_saved_scan[l] = 0;
		}
		zone->reclaim_stat.recent_rotated[0] = 0;
		zone->reclaim_stat.recent_rotated[1] = 0;
		zone->reclaim_stat.recent_scanned[0] = 0;
		zone->reclaim_stat.recent_scanned[1] = 0;
		zap_zone_vm_stats(zone);
		zone->flags = 0;
		if (!size)
			continue;
		/* 需要定义相关宏 */
		set_pageblock_order(pageblock_default_order());
		/* zone中变量pageblock_flags,表示从启动分配器中进行内存申请 */
		setup_usemap(pgdat, zone, size);
		/* zone中的任务等待队列和zone的伙伴系统(MAX_ORDER个链表)的初始化 */
		ret = init_currently_empty_zone(zone, zone_start_pfn,
						size, MEMMAP_EARLY);
		BUG_ON(ret);
		/* zone中page相关属性的初始化工作 */
		memmap_init(size, nid, j, zone_start_pfn);
		zone_start_pfn += size;
	}
}
    分析:
    (1)free_area_init_nodes()函数用于初始化所有的节点和它们的管理区数据。它会对系统中每个活动节点(即内存簇)调用free_area_init_node(),使用add_active_range()提供的页面范围来计算各节点上每种管理区和洞的大小。如果两个相邻管理区的最大PFN相同,则表明后面这个管理区是空的。例如,如果arch_max_dma_pfn == arch_max_dma32_pfn,则表明arch_max_dma32_pfn没有页面。我们假定管理区是连续的,即后一种管理区的开始位置紧接着前一种管理区的结束位置。例如ZONE_DMA32开始于at arch_max_dma_pfn。函数先计算各种管理区的下限页面号和上限页面号,保存在两个数组中,对于连续的相邻管理区(只有ZONE_MOVABLE管理区的内存是不连续的),后一个管理区的下限页面号为前一个管理区的上限页面号。而ZONE_MOVABLE的上下限页面号均设为0。然后调用find_zone_movable_pfns_for_nodes()找出每个节点上ZONE_MOVABLE的开始PFN。
    (2)对每个节点,调用free_area_init_node(),传入参数为节点ID,各个管理区的大小,节点的开始页面号,各洞的大小。该函数先调用calculate_node_totalpages()计算节点上的所有物理页面,并保存在节点的pgdat数据结构中,从“内存描述”一节中可知,节点pg_data_t结构中保存了该节点的所有管理区数据。然后调用free_area_init_core()初始化各个zone中相关数据,包括伙伴系统、等待队列、相关变量、数据结构、链表等。
    (3)free_area_init_core()用于设置管理区的各个数据结构,包括标记管理区的所有页面,标记所有内存空队列,清除内存位图。该函数对节点上的每个管理区,计算它需要映射的真实页面数realsize(即真实内存大小),注意对DMA区这需要减去为DMA保留的页面。然后初始化该管理区的zone数据结构中的相关变量,包括总页面数、真实页面数即realsize、未映射页面数的下限(低于此值时将进行页面回收)、用于slab分配器的页面数下限、保护伙伴系统和页面回收的LRU链表的自旋锁、LRU队列初始化、页面回收状态域、用于管理区使用情况统计的vm_stats置0,等等。最后调用init_currently_empty_zone()初始化zone中的任务等待队列和伙伴系统,调用memmap_init()初始化zone中所有page的相关属性。
     3、初始化管理区分配机制
    从以上分析可以看出,setup_arch()中的内存管理初始化工作是与体系结构相关的,这里介绍的是x86 32位的情况。start_kernel()在执行完setup_arch()后即建立起永久分页机制,然后就会调用mm/page_alloc.c:build_all_zonelists()来初始化管理区分配机制,它通过对每种管理区维护一个管理区队列来实现分配和回收,因此整个初始化工作的核心就是构建所有的管理区队列。一个分配请求在zonelist数据结构上进行操作,该结构在include/linux/mmzone.h中,如下:
#ifdef CONFIG_NUMA
#define MAX_ZONELISTS 2

struct zonelist_cache {
	unsigned short z_to_n[MAX_ZONES_PER_ZONELIST];		/* zone->nid */
	DECLARE_BITMAP(fullzones, MAX_ZONES_PER_ZONELIST);	/* zone full? */
	unsigned long last_full_zap;		/* when last zap'd (jiffies) */
};
#else
#define MAX_ZONELISTS 1
struct zonelist_cache;
#endif

struct zoneref {
	struct zone *zone;	/* Pointer to actual zone */
	int zone_idx;		/* zone_idx(zoneref->zone) */
};

struct zonelist {
	struct zonelist_cache *zlcache_ptr;		     // NULL or &zlcache
	struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
#ifdef CONFIG_NUMA
	struct zonelist_cache zlcache;			     // optional ...
#endif
};
    从前面“内存描述”介绍中可知,zonelist在节点的pg_data_t结构中维护,以作为节点的备用内存区,当节点没有可用内存时,就从队列中分配内存。一个zonelist表示一个管理区的一个队列,队列中的第一个管理区是分配的目标,其他则为备用管理区,以优先级递减的方式存放在队列中。zonelist_cache结构缓存了每个zonelist中的一些关键信息,以便在get_page_from_freelist()中扫描可用页面时,有更小的开销。其中位图fullzones用来跟踪当前zonelist中哪些管理区开始内存不足了;数组z_to_n[]把zonelist中的每个管理区映射到它的节点id,以便我们能估计在当前进程允许的内存范围内节点是否被设置。zoneref则包含了zonelist中实际的zone信息,封装成一个结构是为了避免解引用时进入一个大的结构体内并且搜索表格。
    在zonelist中,zlcache_ptr指针用来标识是否有zlcache。如果非空,则就是zlcache的地址;如果为空,则表示没有zlcache。为了加快zonelist的读取速度,zoneref保存了要读取条目的管理区索引。include/linux/mmzone.h中定义了一些访问zoneref的函数。zonelist_zone()函数返回zoneref中的zone,zonelist_zone_idx()为一个条目返回管理区索引,zonelist_node_idx()为一个条目返回zone中的节点索引。
    mm/page_alloc.c:build_all_zonelists()函数如下,这里介绍非NUMA的情况:
static void zoneref_set_zone(struct zone *zone, struct zoneref *zoneref)
{
	zoneref->zone = zone;
	zoneref->zone_idx = zone_idx(zone);
}

/*
 * 构建管理区环形分配队列,把节点上的所有管理区添加到队列中
 */
static int build_zonelists_node(pg_data_t *pgdat, struct zonelist *zonelist,
				int nr_zones, enum zone_type zone_type)
{
	struct zone *zone;

	BUG_ON(zone_type >= MAX_NR_ZONES);
	zone_type++;

	do {
		zone_type--;
		zone = pgdat->node_zones + zone_type;
		if (populated_zone(zone)) { /* 如果以页面为单位的管理区的总大小不为0 */
			zoneref_set_zone(zone,  /* 将管理区添加到链表中 */
				&zonelist->_zonerefs[nr_zones++]);
			check_highest_zone(zone_type);
		}

	} while (zone_type);
	return nr_zones;
}

#ifdef CONFIG_NUMA
/* ...... */
static void build_zonelists(pg_data_t *pgdat)
{
	/* ...... */
}

static void build_zonelist_cache(pg_data_t *pgdat)
{
	/* ...... */
}

#else	/* non CONFIG_NUMA */
/* ...... */
static void build_zonelists(pg_data_t *pgdat)
{
	int node, local_node;
	enum zone_type j;
	struct zonelist *zonelist;

	local_node = pgdat->node_id;

	zonelist = &pgdat->node_zonelists[0];
	/* 将zone添加到zone链表中,这样,zone中page的 
       分配等操作将依靠这个环形的链表 */  
	j = build_zonelists_node(pgdat, zonelist, 0, MAX_NR_ZONES - 1);

	/*
	 * Now we build the zonelist so that it contains the zones
	 * of all the other nodes.
	 * We don't want to pressure a particular node, so when
	 * building the zones for node N, we make sure that the
	 * zones coming right after the local ones are those from
	 * node N+1 (modulo N)
	 */
	/* 对其他在线的节点创建zonelist */
	for (node = local_node + 1; node < MAX_NUMNODES; node++) {
		if (!node_online(node))
			continue;
		j = build_zonelists_node(NODE_DATA(node), zonelist, j,
							MAX_NR_ZONES - 1);
	}
	for (node = 0; node < local_node; node++) {
		if (!node_online(node))
			continue;
		j = build_zonelists_node(NODE_DATA(node), zonelist, j,
							MAX_NR_ZONES - 1);
	}

	zonelist->_zonerefs[j].zone = NULL;
	zonelist->_zonerefs[j].zone_idx = 0;
}

/* 构建zonelist缓存:对非NUMA的zonelist信息,只是把zlcache_ptr设成NULL */
static void build_zonelist_cache(pg_data_t *pgdat)
{
	pgdat->node_zonelists[0].zlcache_ptr = NULL;
}

#endif	/* CONFIG_NUMA */

/* 返回int值,因为可能通过stop_machine()调用本函数 */
static int __build_all_zonelists(void *dummy)
{
	int nid;

#ifdef CONFIG_NUMA
	memset(node_load, 0, sizeof(node_load));
#endif
	for_each_online_node(nid) {
		pg_data_t *pgdat = NODE_DATA(nid);
		/* 创建zonelists,这个队列用来在分配内存时回绕,循环访问 */
		build_zonelists(pgdat);
		/* 创建zonelist缓存信息:在非NUMA中,仅仅是把相关缓存变量设成NULL */
		build_zonelist_cache(pgdat);
	}
	return 0;
}

void build_all_zonelists(void)
{
	/* 设置全局变量current_zonelist_order */
	set_zonelist_order();

	/* 对所有节点创建zonelists */
	if (system_state == SYSTEM_BOOTING) { /* 系统正在引导时 */
		__build_all_zonelists(NULL);  
		mminit_verify_zonelist();  /* 调试用 */
		cpuset_init_current_mems_allowed();
	} else {
		/* 非引导时要停止所有cpu以确保没有使用zonelist */
		stop_machine(__build_all_zonelists, NULL, NULL);
		/* cpuset refresh routine should be here */
	}
	/* 计算所有zone中可分配的页面数之和 */
	vm_total_pages = nr_free_pagecache_pages();
	/*
	 * Disable grouping by mobility if the number of pages in the
	 * system is too low to allow the mechanism to work. It would be
	 * more accurate, but expensive to check per-zone. This check is
	 * made on memory-hotadd so a system can start with mobility
	 * disabled and enable it later
	 */
	if (vm_total_pages < (pageblock_nr_pages * MIGRATE_TYPES))
		page_group_by_mobility_disabled = 1;
	else
		page_group_by_mobility_disabled = 0;

	printk("Built %i zonelists in %s order, mobility grouping %s.  "
		"Total pages: %ld\n",
			nr_online_nodes,
			zonelist_order_name[current_zonelist_order],
			page_group_by_mobility_disabled ? "off" : "on",
			vm_total_pages);
#ifdef CONFIG_NUMA
	printk("Policy zone: %s\n", zone_names[policy_zone]);
#endif
}
    分析:
    (1)build_all_zonelists()调用__build_all_zonelists()来构建所有管理区队列。如果是系统引导时,则直接调用__build_all_zonelists()对所有节点创建zonelist;如果不是引导时,则要通过stop_machine()来调用__build_all_zonelists(),先停止所有CPU以确保没有使用zonelist。然后用nr_free_pagecache_pages()计算所有zone中可分配的页面总数,如果页面总数太小,则禁用页面分组移动功能(因为这个性能开销比较大)。
    (2)在__build_all_zonelists()中,对每个在线节点,调用build_zonelists()创建管理区分配的环形队列,调用build_zonelist_cache()创建队列的缓存信息。这两个函数有NUMA版本和非NUMA版本,这里略去NUMA版本,只介绍非NUMA版本。在build_zonelists()中,对每个在线节点,调用build_zonelists_node()构建环形分配队列,把节点上的所有管理区添加到队列中。在build_zonelist_cache()中,对非NUMA的zonelist信息,只是把zlcache_ptr设成NULL。
    (3)在build_zonelists_node()中,通过zoneref_set_zone()将每个产生的管理区添加到队列中。
    从以上分析可知,内存管理区初始化主要是借助于引导分配器和已初始化的e820全局变量。内存管理区初始化后相应的伙伴系统、slab机制等等就可以在此基础上建立了。

你可能感兴趣的:(数据结构,linux,struct,cache,活动,Build)