12.伙伴系统(初始化内存域和结点数据结构,free_area_init_nodes详解)
直到现在,我们只在特定于体系结构的代码中看到了内核如何检测系统中的可用内存。与高层数据结构(如内存域和结点)的关联,则需要根据该信息构建。我们知道,体系结构相关代码需要在启动期间建立以下信息:
系统中各个内存域的页帧边界,保存在max_zone_pfn数组;
各结点页帧的分配情况,保存在全局变量early_node_map中。
从内核版本2.6.10开始提供了一个通用框架,用于将上述信息转换为伙伴系统预期的结点和内存域数据结构。在这以前,各个体系结构必须自行建立相关结构。现在,体系结构相关代码只需要建立前述的简单结构,将繁重的工作留给free_area_init_nodes即可。图3-27给出了该过程概述,图3-28给出了free_area_init_nodes的代码流程图。

free_area_init_nodes首先必须分析并改写特定于体系结构的代码提供的信息。其中,需要对照在zone_max_pfn和zone_min_pfn中指定的内存域的边界,计算各个内存域可使用的最低和最高的
页帧编号。使用了两个全局数组来存储这些信息:
mm/page_alloc.c static unsigned long __meminitdata arch_zone_lowest_possible_pfn[MAX_NR_ZONES]; static unsigned long __meminitdata arch_zone_highest_possible_pfn[MAX_NR_ZONES];
但free_area_init_nodes首先会根据结点的第一个页帧start_pfn,对early_node_map中的各项进行排序。
mm/page_alloc.c void __init free_area_init_nodes(unsigned long *max_zone_pfn) { unsigned long nid; enum zone_type i; /* 对early_node_map进行排序,因为初始化代码假定它已经是排序的 */ sort_node_map(); ...
通过max_zone_pfn传递给free_area_init_nodes的信息记录了各个内存域包含的最大页帧号。free_area_init_nodes将该信息转换为一种更方便的表示形式,即以[low, high]形式描述各个内
存域的页帧区间,存储在前述的全局变量中。
mm/page_alloc.c 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]); }
辅助函数find_min_pfn_with_active_regions用于找到注册的最低内存域中可用的编号最小的页帧。该内存域不必一定是ZONE_DMA,例如,在计算机不需要DMA内存的情况下也可以是ZONE_NORMAL。最低内存域的最大页帧号可以从max_zone_pfn提供的信息直接获得。接下来构建其他内存域的页帧区间,方法很直接:第n个内存域的最小页帧,即前一个(第n-1个)内存域的最大页帧。当前内存域的最大页帧由max_zone_pfn给出。
free_area_init_nodes剩余的部分遍历所有结点,分别建立其数据结构。
mm/page_alloc.c /* 输出有关内存域的信息 */ ... /* 初始化各个结点 */ for_each_online_node(nid) { pg_data_t *pgdat = NODE_DATA(nid); free_area_init_node(nid, pgdat, 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); } }
代码遍历所有活动结点,并分别对各个结点调用free_area_init_node建立数据结构。该函数需要结点第一个可用的页帧作为一个参数,而find_min_pfn_for_node则从early_node_map数组提取该信息。如果根据node_present_pages字段判断结点具有内存,则在结点位图中设置N_HIGH_MEMORY标志。我们知道该标志只表示结点上存在普通或高端内存,因此check_for_regular_memory进一步检查低于ZONE_HIGHMEM的内存域中是否有内存,并据此在结点位图中相应地设置N_NORMAL_MEMORY标志。
2. 对各个结点创建数据结构
在内存域边界已经确定之后,free_area_init_nodes分别对各个内存域调用free_area_init_node创建数据结构。为此还需要几个辅助函数。
calculate_node_totalpages首先累计各个内存域的页数,计算结点中页的总数。对连续内存模型而言,这可以通过zones_size_init完成,但calculate_zone_totalpages还考虑了内存域中的空洞。
alloc_node_mem_map负责初始化一个简单但非常重要的数据结构。如上所述,系统中的各个物理内存页,都对应着一个struct page实例。该结构的初始化由alloc_node_mem_map执行。
mm/page_alloc.c static void __init_refok alloc_node_mem_map(struct pglist_data *pgdat) { /* 跳过空结点 */ if (!pgdat->node_spanned_pages) return; if (!pgdat->node_mem_map) { unsigned long size, start, end; struct page *map; start = pgdat->node_start_pfn & ~(MAX_ORDER_NR_PAGES -1); end = pgdat->node_start_pfn + pgdat->node_spanned_pages; end = ALIGN(end, MAX_ORDER_NR_PAGES); size = (end -start) * sizeof(struct page); map = alloc_remap(pgdat->node_id, size); if (!map) map = alloc_bootmem_node(pgdat, size); pgdat->node_mem_map = map + (pgdat->node_start_pfn -start); } if (pgdat == NODE_DATA(0)) mem_map = NODE_DATA(0)->node_mem_map; }
没有页的空结点显然可以跳过。如果特定于体系结构的代码尚未建立内存映射(这是可能的,例如,在IA-64系统上),则必须分配与该结点关联的所有struct page实例所需的内存。请注意,代码将内存映射对齐到伙伴系统的最大分配阶,因为要使所有的计算都工作正常,这是必需的。
指向该空间的指针不仅保存在pglist_data实例中,还保存在全局变量mem_map中,前提是当前考察的结点是系统的第0个结点(如果系统只有一个内存结点,则总是这样)。mem_map是一个全局数组,在讲解内存管理时,我们会经常遇到。
初始化内存域数据结构涉及的繁重工作由free_area_init_core执行,它会依次遍历结点的所有内存域。
mm/page_alloc.c static void __init 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; ... for (j = 0; j < MAX_NR_ZONES; j++) { struct zone *zone = pgdat->node_zones + j; unsigned long size, realsize, memmap_pages; size = zone_spanned_pages_in_node(nid, j, zones_size); realsize = size -zone_absent_pages_in_node(nid, j,zholes_size); ...
内存域的真实长度,可通过跨越的页数减去空洞覆盖的页数而得到。这两个值是通过两个辅助函数计算的。
mm/page_alloc.c ... if (!is_highmem_idx(j)) nr_kernel_pages += realsize; nr_all_pages += realsize; zone->spanned_pages = size; zone->present_pages = realsize; ... zone->name = zone_names[j]; ... zone->zone_pgdat = pgdat; /* 初始化内存域字段为默认值,并调用辅助函数 */ ... }
内核使用两个全局变量跟踪系统中的页数。nr_kernel_pages统计所有一致映射的页,而nr_all_pages还包括高端内存页在内。
free_area_init_core剩余部分的任务是初始化zone结构中的各个表头,并将各个结构成员初始化为0。
我们比较感兴趣的是调用的两个辅助函数。
zone_pcp_init初始化该内存域的per-CPU缓存,且将在下一节广泛讨论。详见7.冷热缓存的初始化(free_area_init_nodes)
init_currently_empty_zone初始化free_area列表,并将属于该内存域的所有page实例都设置为初始默认值。正如前文的讨论,调用了memmap_init_zone来初始化内存域的页。我们还可以回想前文提到的,所有页属性起初都设置为MIGRATE_MOVABLE。此外,空闲列表是在zone_init_free_lists中初始化的:
mm/page_alloc.c static void __meminit zone_init_free_lists(struct pglist_data *pgdat,struct zone *zone, unsigned long size) { int order, t; for_each_migratetype_order(order, t) { INIT_LIST_HEAD(&zone->free_area[order].free_list[t]); zone->free_area[order].nr_free = 0; } }
空闲页的数目(nr_free)当前仍然规定为0,这显然没有反映真实情况。直至停用bootmem分配器、普通的伙伴分配器生效,才会设置正确的数值。
浙公网安备 33010602011771号