传统的计算机结构中,整个物理内存都是一条线上的,CPU访问整个内存空间所须要的时间都是相同的。这种内存结构被称之为UMA(Uniform Memory Architecture,一致存储结构)。可是随着计算机的发展,一些新型的服务器结构中,尤为是多CPU的状况下,物理内存空间的访问就难以控制所需的时间相同了。在多CPU的环境下,系统只有一条总线,有多个CPU都连接到上面,并且每一个CPU都有本身本地的物理内存空间,可是也能够经过总线去访问别的CPU物理内存空间,同时也存在着一些多CPU均可以共同访问的公共物理内存空间。因而乎这就出现了一个新的状况,因为各类物理内存空间所处的位置不一样,因而访问它们的时间长短也就各异,无法保证一致。对于这种状况的内存结构,被称之为NUMA(Non-Uniform Memory Architecture,非一致存储结构)。事实上也没有彻底的UMA,好比常见的单CPU电脑,RAM、ROM等物理存储空间的访问时间并不是一致的,只是纯粹对RAM而言,是UMA的。此外还有一种称之为MPP的结构(Massive Parallel Processing,大规模并行处理系统),是由多个SMP服务器经过必定的节点互联网络进行链接,协同工做,完成相同的任务。从外界使用者看来,它是一个服务器系统。node
回归正题,着重看一下NUMA。因为NUMA存储结构的引入,这就须要相应的管理机制来支持, linux 2.4版本就已经开始对其支持了。随着新增管理机制的支持,也随之引入了Node的概念(存储节点),把访问时间相同的存储空间归结为一个存储节点。因而当前分析的3.14.12版本,linux的物理内存管理机制将物理内存划分为三个层次来管理,依次是:Node(存储节点)、Zone(管理区)和Page(页面)。linux
存储节点的数据结构为pg_data_t,每一个NUMA节点都有一个pg_data_t负责记载该节点的内存布局信息。其中pg_data_t结构体中存储管理区信息的为node_zones成员,其数据结构为zone,每一个pg_data_t都有多个node_zones,一般是三个:ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM。算法
ZONE_DMA区一般是因为计算机中部分设备没法直接访问所有内存空间而特意划分出来给该部分设备使用的区,x86环境中,该区一般小于16M。服务器
ZONE_NORMAL区位于ZONE_DMA后面,这个区域被内核直接映射到线性地址的高端部分,x86环境中,该区一般为16M-896M。网络
ZONE_HIGHMEM区则是系统除了ZONE_DMA和ZONE_NORMAL区后剩下的物理内存,这个区不能直接被内核映射,x86环境中,该区一般为896M之后的内存。数据结构
为何要有高端内存的存在?一般都知道内核空间的大小为1G(线性空间为:3-4G)。那么映射这1G内存须要多少页全局目录项?很容易能够算出来是256项,内核有这么多线程在其中,1G够吗?很明显不够,若是要使用超出1G的内存空间怎么办?若是要使用内存,很明显必需要作映射,那么腾出几个页全局目录项出来作映射?Bingo,就是这样,那么腾出多少来呢?linux内核的设计就是腾出32个页全局目录项,256的1/8。那么32个页全局目录项对应多大的内存空间?算一下能够知道是128M,也就是说直接映射的内存空间是896M。使用超过896M的内存空间视为高端内存,一旦使用的时候,就须要作映射转换,这是一件很耗资源的事情。因此不要常使用高端内存,就是这么一个由来。多线程
接着看一下内存管理框架的初始化实现,initmem_init():app
【file:/arch/x86/mm/init_32.c】 #ifndef CONFIG_NEED_MULTIPLE_NODES void __init initmem_init(void) { #ifdef CONFIG_HIGHMEM highstart_pfn = highend_pfn = max_pfn; if (max_pfn > max_low_pfn) highstart_pfn = max_low_pfn; printk(KERN_NOTICE "%ldMB HIGHMEM available.\n", pages_to_mb(highend_pfn - highstart_pfn)); high_memory = (void *) __va(highstart_pfn * PAGE_SIZE - 1) + 1; #else high_memory = (void *) __va(max_low_pfn * PAGE_SIZE - 1) + 1; #endif memblock_set_node(0, (phys_addr_t)ULLONG_MAX, &memblock.memory, 0); sparse_memory_present_with_active_regions(0); #ifdef CONFIG_FLATMEM max_mapnr = IS_ENABLED(CONFIG_HIGHMEM) ? highend_pfn : max_low_pfn; #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 */
将high_memory初始化为低端内存页框max_low_pfn对应的地址大小,接着调用memblock_set_node,根据函数命名,能够推断出该函数用于给早前创建的memblock算法设置node节点信息。框架
memblock_set_node的实现:ide
【file:/mm/memblock.c】 /** * memblock_set_node - set node ID on memblock regions * @base: base of area to set node ID for * @size: size of area to set node ID for * @type: memblock type to set node ID for * @nid: node ID to set * * Set the nid of memblock @type regions in [@base,@base+@size) to @nid. * Regions which cross the area boundaries are split as necessary. * * RETURNS: * 0 on success, -errno on failure. */ int __init_memblock memblock_set_node(phys_addr_t base, phys_addr_t size, struct memblock_type *type, int nid) { int start_rgn, end_rgn; int i, ret; ret = memblock_isolate_range(type, base, size, &start_rgn, &end_rgn); if (ret) return ret; for (i = start_rgn; i < end_rgn; i++) memblock_set_region_node(&type->regions[i], nid); memblock_merge_regions(type); return 0; }
memblock_set_node主要调用了三个函数作相关操做:memblock_isolate_range、memblock_set_region_node和memblock_merge_regions。
其中memblock_isolate_range:
【file:/mm/memblock.c】 /** * memblock_isolate_range - isolate given range into disjoint memblocks * @type: memblock type to isolate range for * @base: base of range to isolate * @size: size of range to isolate * @start_rgn: out parameter for the start of isolated region * @end_rgn: out parameter for the end of isolated region * * Walk @type and ensure that regions don't cross the boundaries defined by * [@base,@base+@size). Crossing regions are split at the boundaries, * which may create at most two more regions. The index of the first * region inside the range is returned in *@start_rgn and end in *@end_rgn. * * RETURNS: * 0 on success, -errno on failure. */ static int __init_memblock memblock_isolate_range(struct memblock_type *type, phys_addr_t base, phys_addr_t size, int *start_rgn, int *end_rgn) { phys_addr_t end = base + memblock_cap_size(base, &size); int i; *start_rgn = *end_rgn = 0; if (!size) return 0; /* we'll create at most two more regions */ while (type->cnt + 2 > type->max) if (memblock_double_array(type, base, size) < 0) return -ENOMEM; for (i = 0; i < type->cnt; i++) { struct memblock_region *rgn = &type->regions[i]; phys_addr_t rbase = rgn->base; phys_addr_t rend = rbase + rgn->size; if (rbase >= end) break; if (rend <= base) continue; if (rbase < base) { /* * @rgn intersects from below. Split and continue * to process the next region - the new top half. */ rgn->base = base; rgn->size -= base - rbase; type->total_size -= base - rbase; memblock_insert_region(type, i, rbase, base - rbase, memblock_get_region_node(rgn), rgn->flags); } else if (rend > end) { /* * @rgn intersects from above. Split and redo the * current region - the new bottom half. */ rgn->base = end; rgn->size -= end - rbase; type->total_size -= end - rbase; memblock_insert_region(type, i--, rbase, end - rbase, memblock_get_region_node(rgn), rgn->flags); } else { /* @rgn is fully contained, record it */ if (!*end_rgn) *start_rgn = i; *end_rgn = i + 1; } } return 0; }
该函数主要作分割操做,在memblock算法创建时,只是判断了flags是否相同,而后将连续内存作合并操做,而此时创建node节点,则根据入参base和size标记节点内存范围将内存划分开来。若是memblock中的region刚好以该节点内存范围末尾划分开来的话,那么则将region的索引记录至start_rgn,索引加1记录至end_rgn返回回去;若是memblock中的region跨越了该节点内存末尾分界,那么将会把当前的region边界调整为node节点内存范围边界,另外一部分经过memblock_insert_region()函数插入到memblock管理regions当中,以完成拆分。
顺便看一下memblock_insert_region()函数:
【file:/mm/memblock.c】 /** * memblock_insert_region - insert new memblock region * @type: memblock type to insert into * @idx: index for the insertion point * @base: base address of the new region * @size: size of the new region * @nid: node id of the new region * @flags: flags of the new region * * Insert new memblock region [@base,@base+@size) into @type at @idx. * @type must already have extra room to accomodate the new region. */ static void __init_memblock memblock_insert_region(struct memblock_type *type, int idx, phys_addr_t base, phys_addr_t size, int nid, unsigned long flags) { struct memblock_region *rgn = &type->regions[idx]; BUG_ON(type->cnt >= type->max); memmove(rgn + 1, rgn, (type->cnt - idx) * sizeof(*rgn)); rgn->base = base; rgn->size = size; rgn->flags = flags; memblock_set_region_node(rgn, nid); type->cnt++; type->total_size += size; }
这里一个memmove()将后面的region信息日后移,另外调用memblock_set_region_node()将原region的node节点号保留在被拆分出来的region当中。
而memblock_set_region_node()函数实现仅是赋值而已:
【file:/mm/memblock.h】 static inline void memblock_set_region_node(struct memblock_region *r, int nid) { r->nid = nid; }
至此,回到memblock_set_node()函数,里面接着memblock_isolate_range()被调用的memblock_set_region_node()已知是获取node节点号,而memblock_merge_regions()则前面已经分析过了,是用于将region合并的。
最后回到initmem_init()函数中,memblock_set_node()返回后,接着调用的函数为sparse_memory_present_with_active_regions()。
这里sparse memory涉及到linux的一个内存模型概念。linux内核有三种内存模型:Flat memory、Discontiguous memory和Sparse memory。其分别表示:
看一下sparse_memory_present_with_active_regions()的实现:
【file:/mm/page_alloc.c】 /** * sparse_memory_present_with_active_regions - Call memory_present for each active range * @nid: The node to call memory_present for. If MAX_NUMNODES, all nodes will be used. * * If an architecture guarantees that all ranges registered with * add_active_ranges() contain no holes and may be freed, this * function may be used instead of calling memory_present() manually. */ void __init sparse_memory_present_with_active_regions(int nid) { unsigned long start_pfn, end_pfn; int i, this_nid; for_each_mem_pfn_range(i, nid, &start_pfn, &end_pfn, &this_nid) memory_present(this_nid, start_pfn, end_pfn); }
里面的for_each_mem_pfn_range()是一个旨在循环的宏定义,而memory_present()因为实验环境中没有定义CONFIG_HAVE_MEMORY_PRESENT,因此是个空函数。暂且搁置不作深刻研究。
最后看一下initmem_init()退出前调用的函数setup_bootmem_allocator():
【file:/arch/x86/mm/init_32.c】 void __init setup_bootmem_allocator(void) { 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); }
原来该函数是用来初始化bootmem管理算法的,但如今x86的环境已经使用了memblock管理算法,这里仅做保留打印部分信息。