【Linux内存源码分析】构建内存管理框架(1)

传统的计算机结构中,整个物理内存都是一条线上的,CPU访问整个内存空间所需要的时间都是相同的。这种内存结构被称之为UMAUniform Memory Architecture,一致存储结构)。但是随着计算机的发展,一些新型的服务器结构中,尤其是多CPU的情况下,物理内存空间的访问就难以控制所需的时间相同了。在多CPU的环境下,系统只有一条总线,有多个CPU都链接到上面,而且每个CPU都有自己本地的物理内存空间,但是也可以通过总线去访问别的CPU物理内存空间,同时也存在着一些多CPU都可以共同访问的公共物理内存空间。于是乎这就出现了一个新的情况,由于各种物理内存空间所处的位置不同,于是访问它们的时间长短也就各异,没法保证一致。对于这种情况的内存结构,被称之为NUMANon-Uniform Memory Architecture,非一致存储结构)。事实上也没有完全的UMA,比如常见的单CPU电脑,RAMROM等物理存储空间的访问时间并非一致的,只是纯粹对RAM而言,是UMA的。此外还有一种称之为MPP的结构(Massive Parallel Processing,大规模并行处理系统),是由多个SMP服务器通过一定的节点互联网络进行连接,协同工作,完成相同的任务。从外界使用者看来,它是一个服务器系统。

回归正题,着重看一下NUMA。由于NUMA存储结构的引入,这就需要相应的管理机制来支持, linux 2.4版本就已经开始对其支持了。随着新增管理机制的支持,也随之引入了Node的概念(存储节点),把访问时间相同的存储空间归结为一个存储节点。于是当前分析的3.14.12版本,linux的物理内存管理机制将物理内存划分为三个层次来管理,依次是:Node(存储节点)、Zone(管理区)和Page(页面)。

存储节点的数据结构为pg_data_t,每个NUMA节点都有一个pg_data_t负责记载该节点的内存布局信息。其中pg_data_t结构体中存储管理区信息的为node_zones成员,其数据结构为zone,每个pg_data_t都有多个node_zones,通常是三个:ZONE_DMAZONE_NORMALZONE_HIGHMEM

ZONE_DMA区通常是由于计算机中部分设备无法直接访问全部内存空间而特地划分出来给该部分设备使用的区,x86环境中,该区通常小于16M

ZONE_NORMAL区位于ZONE_DMA后面,这个区域被内核直接映射到线性地址的高端部分,x86环境中,该区通常为16M-896M

ZONE_HIGHMEM区则是系统除了ZONE_DMAZONE_NORMAL区后剩下的物理内存,这个区不能直接被内核映射,x86环境中,该区通常为896M以后的内存。

为什么要有高端内存的存在?通常都知道内核空间的大小为1G(线性空间为:3-4G)。那么映射这1G内存需要多少页全局目录项?很容易可以算出来是256项,内核有这么多线程在其中,1G够吗?很明显不够,如果要使用超出1G的内存空间怎么办?如果要使用内存,很明显必须要做映射,那么腾出几个页全局目录项出来做映射?Bingo,就是这样,那么腾出多少来呢?linux内核的设计就是腾出32个页全局目录项,2561/8。那么32个页全局目录项对应多大的内存空间?算一下可以知道是128M,也就是说直接映射的内存空间是896M。使用超过896M的内存空间视为高端内存,一旦使用的时候,就需要做映射转换,这是一件很耗资源的事情。所以不要常使用高端内存,就是这么一个由来。

接着看一下内存管理框架的初始化实现,initmem_init()

【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的实现:

【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_rangememblock_set_region_nodememblock_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节点,则根据入参basesize标记节点内存范围将内存划分开来。如果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()将原regionnode节点号保留在被拆分出来的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_get_region_node()则是获取node节点号:

【file:/mm/memblock.h】
static inline int memblock_get_region_node(const struct memblock_region *r)
{
	return r->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 memoryDiscontiguous memorySparse memory。其分别表示:

Flat
memory:顾名思义,物理内存是平坦连续的,整个系统只有一个node节点。

Discontiguous
memory:物理内存不连续,内存中存在空洞,也因而系统将物理内存分为多个节点,但是每个节点的内部内存是平坦连续的。值得注意的是,该模型不仅是对于NUMA环境而言,UMA环境上同样可能存在多个节点的情况。

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管理算法,这里仅作保留打印部分信息。

这部分代码主要是对内存进行node节点进行设置。

发表评论

电子邮件地址不会被公开。 必填项已用*标注