void *virtual; /* Kernel virtual address (NULL if not kmapped, ie. highmem) */ #endif /* WANT_PAGE_VIRTUAL */ };
WANT_PAGE_VIRTUAL是由是否需要高端内存决定的,virtual用于寻址高端内存区域中的页框,存储该页的虚拟地址。有些时候高端内存并不映射到任何实际的物理地址页框上,此时它的值为NULL。
13.3. bootmem_free_node
在bootmem_init_node函数中,根据struct meminfo参数来初始化内存节点对应的pg_data_t数据结构contig_page_data,并且申请Bootmem机制使用的 bitmap。从该函数使用的参数来看,一个内存节点对应一个struct meminfo的内存块信息。所以一个内存节点有可能对应多个membank,而这些membank的物理地址可能是不连续的,这就是内存孔洞的存在的原 因。contig_page_data成员中的
bdata->node_min_pfn和bdata->node_low_pfn参数分别记 录了所有内存块中的最低物理页框地址和最高物理页框地址。 arch/arm/include/asm/setup.h struct membank {
unsigned long start; unsigned long size; int node; };
struct meminfo {
int nr_banks;
struct membank bank[NR_BANKS]; };
static unsigned long __init bootmem_init_node(int node, struct meminfo *mi); bootmem_free_node与bootmem_init_node参数类似,它用来初始化特定内存节点的管理区信息。 arch/arm/mm/init.c
static void __init bootmem_free_node(int node, struct meminfo *mi);
尽管局部zone_size和zhole_size声明为大小为MAX_NR_ZONES的数组,但是只用到了其中的第一个元素。这是由于ARM Linux采用了UMA方式的内存管理机制。
? zone_size[0]被赋值为end_pfn - start_pfn,然后根据zone_size减去meminfo中每个membank中真正的size得到内存孔洞的大小zhole_size[0]。 ? 通过arch_adjust_zones为特定架构的系统预留内存。通常用它来为特定的限制的DMA寻址预留内存,将这些DMA无法访问的内存放入zone[1],而DMA对应zone[0],通常DMA可以寻址所有内存。
? 最后调用free_area_init_node初始化节点对应的pg_data_t描述符信息,并且为每个页表分配struct page结构。
?
#define arch_adjust_zones(node,size,holes) do { } while (0) 图 72. bootmem_free_node调用流程
13.4. free_area_init_node
mm/page_alloc.c
void __paginginit free_area_init_node(int nid, unsigned long *zones_size,
unsigned long node_start_pfn, unsigned long *zholes_size) free_area_init_node的函数参数解释如下:
nid,节点ID号。
? zones_size,大小为MAX_NR_ZONES的数组,用来记录当前内存节点中的内存页框数,包含孔洞。
? node_start_pfn,当前内存节点中的起始内存页框。 ? zholes_size,大小为MAX_NR_ZONES的数组,用来记录当前内存节点中的内存孔洞页框数。
?
free_area_init_node完成以下功能:
? ? ? ?
?
根据参数nid,确定该节点对应的pgdat,并初始化成员node_id = nid。 pgdat->node_start_pfn = node_start_pfn。
通过calculate_node_totalpages函数,计算pgdat->node_spanned_pages(包含孔洞)和pgdat->node_present_pages(不含孔洞)。
每一个物理页框对应一个struct page结构,通过alloc_node_mem_map为所有的物理页面分配该结构体空间,并将起始页框地址保存在pgdat->node_mem_map中。
调用free_area_init_core,用来初始化内存管理区zone。
13.5. free_area_init_core
mm/page_alloc.c /*
* Set up the zone data structures: * - mark all pages reserved
* - mark all memory queues empty * - clear the memory bitmaps */
static void __paginginit free_area_init_core(struct pglist_data *pgdat, unsigned long *zones_size, unsigned long *zholes_size); free_area_init_core的函数参数解释如下:
pgdat,内存节点对应的pgdat_t类型描述符。
? zones_size,大小为MAX_NR_ZONES的数组,用来记录当前内存节点中的内存页框数,包含孔洞。 ? zholes_size,大小为MAX_NR_ZONES的数组,用来记录当前内存节点中的内存孔洞页框数。
?
free_area_init_core针对单个内存节点内的所有管理区进行初始化,并计算管理内存页所用的struct page数组占用的memmap_pages。
? ? ? ? ?
通过pgdat_resize_init函数初始化pgdat自旋锁成员node_size_lock,它与CONFIG_MEMORY_HOTPLUG(内存热插拔)有关。 初始化pgdat->nr_zones为0。
通过init_waitqueue_head函数初始化pgdat->kswapd_wait,它是kswapd页换出守护进程使用的等待队列。
初始化pgdat->kswapd_max_order为0。
通过pgdat_page_cgroup_init函数初始化pgdat->node_page_cgroup为
NULL,如果没有打开CONFIG_CGROUP_MEM_RES_CTLR选项,则为空函数。
接下来遍历内存节点中的所有管理区,完成以下工作:
? ? ?
? ?
计算含有孔洞的页面总数存入size,同时zone->spanned_pages记录该值。 计算不含孔洞的页面总数存入realsize。
根 据size变量计算页面数组所占用的页面数,存入memmap_pages。之所以不使用realsize,是因为在通过 alloc_node_mem_map函数来分配页面管理数组时采用的含有孔洞的页面数,这是为了管理方便,但是在有大量孔洞的内存节点中,这样会浪费大 量struct page页面管理结构,所以通常会使能内存的CONFIG_DISCONTIGMEM选项。 如果处理管理区是DMA区,那么将在realsize中再次为DMA预留内存。也即realsize再次减去dma_reserve。
将realsize减去页面映射使用的页面大小memmap_pages并存入zone->present_pages。
?
? ? ? ? ?
? ? ? ? ? ? ? ?
?
? ? ?
通过is_highmem_idx判断当前内存区是否为高端内存,如果不是,那么将realsize计入内核全局统计信息nr_kernel_pages,它描述了内核所有可以一一映射的页。
将realsize计入nr_all_pages,与nr_kernel_pages类似,它还记录了高端内存页。
如果定义了CONFIG_NUMA,则初始化管理区中的node,min_unmapped_pages和min_slab_pages成员。
为zone->name赋值,它指向zone_names数组中对应的当前管理区的值 使用spin_lock_init初始化管理区中的lock和lru_lock自旋锁。 如果配置了CONFIG_MEMORY_HOTPLUG,那么初始化自旋锁
span_seqlock。与lock和lru_lock不同,它通过seqlock_init函数完成初始化。
设置prev_priority为DEF_PRIORITY。 初始化管理区中的回调指针zone_pgdat,显然它指向该区所属的内存节点类型pgdat指针。
zone_pcp_init初始化管理区的per-CPU缓存。 初始化lru成员。
初始化recent_rotated和recent_scanned成0。
通过函数zap_zone_vm_stats初始化vm_stat成员为0。 初始化flags成员为0。
如果打开了CONFIG_HUGETLB_PAGE_SIZE_VARIABLE选项,则通过pageblock_default_order函数获取默认值并设置给全局变量
pageblock_order,否则默认值为MAX_ORDER-1。它被用在伙伴系统中。 setup_usemap设置pageblock_flags为NULL。如果该区包含的页框数满足要求,那么为pageblock_flags分配内存并初始化为0。pageblock_flags与伙伴系统的碎片迁移算法有关。
init_currently_empty_zone初始化伙伴系统的free_area列表。
最后通过memmap_init宏间接引用函数memmap_init_zone将属于该管理区的所有page数组都设置为初始默认值。
zone_start_pfn记录下一循环处理的管理区的开始页框地址。
13.6. memmap_init_zone
memmap_init_zone函数初始化每个管理区中的页帧对应的page数组。 mm/page_alloc.c
#ifndef __HAVE_ARCH_MEMMAP_INIT
#define memmap_init(size, nid, zone, start_pfn) \\ memmap_init_zone((size), (nid), (zone), (start_pfn), MEMMAP_EARLY) #endif
void __meminit memmap_init_zone(unsigned long size, int nid, unsigned long zone, unsigned long start_pfn, enum memmap_context context);
mem_init_zone通过memmap_init宏实现调用,由于一些特定的架构,系统公共的memmap初始化函数无法满足需求,比如IA64。 此时在特定架构的代码中会定义memmap_init。memmap_init_zone顾名思义,它是针对单个管理区对应的page数组来初始化的。
? ? ? ? ?
size指明了管理区的页帧数,它包含孔洞。 nid是当前管理区所属的内存节点的编号。
zone指明了当前管理区在内存节点中node_zones数组下标。 zone_start_pfn则提供了当前管理区的第一个页帧的编号。
context是为了指明当前是在系统初始化阶段,还是热插拔阶段对内存管理页的初始化。它只有两个值:MEMMAP_EARLY和MEMMAP_HOTPLUG。
memmap_init_zone依次完成了以下功能:
? ? ? ?
? ? ? ? ? ? ?
通过end_pfn = start_pfn + size得到终止页帧,然后从start_pfn到end_pfn通过循环一次处理它们对应的struct page。
如果context指定的系统状态是MEMMAP_EARLY,则需要判断当前页帧
[10]
是否存在,这是因为内存孔洞的存在。 根据公式page = pfn_to_page(pfn),由页帧得到它对应的struct page管理项。 设 置page中flags成员的Field Area,它由段区,管理区和节点区三部分组成,分别占用的位数由SECTIONS_WIDTH,ZONES_WIDTH和NODES_WIDTH分别表 示。set_page_links函数的作用就是分别通过set_page_zone,set_page_node和set_page_section函 数来设置这些字段区。以后就可以根据这些区域获取当前页帧的位置信息。
mminit_verify_page_links用来验证set_page_links设置的信息是否正确。 通过init_page_count将page->_count成员初始化为1。
通过reset_page_mapcount将page->_mapcount成员初始化为-1。
通过由宏定义展开后的函数SetPageReserved设置PG_reserved标记到page->flags中。
设置所有页面均为MIGRATE_MOVABLE的。 初始化page->lru。
如果配置了WANT_PAGE_VIRTUAL,且不为高端内存则初始化virtual成员。比如SPARC系统。
include/asm-generic/memory_model.h #if defined(CONFIG_FLATMEM) #ifndef ARCH_PFN_OFFSET
#define ARCH_PFN_OFFSET (0UL) #endif ......
#define __pfn_to_page(pfn) (mem_map + ((pfn) - ARCH_PFN_OFFSET)) #define __page_to_pfn(page) ((unsigned long)((page) - mem_map) + \\ ARCH_PFN_OFFSET) ......