linux内存管理-物理页面的分配

前面的博客中曾经提到,当需要分配若干内存页面时,用于DMA的内存页面必须是连续的。其实,为便于管理,特别是处于对物理存储空间质地一致性的考虑,即使不是用于DMA的内存页面也是连续分配的。

当一个进程需要分配若干连续的物理页面时,可以通过alloc_pages来完成。linux2.4.0内核版本的代码中有两个alloc_pages,一个是在mm/numa.c中,另一个在mm.h中,编译时根据所定义的条件编译选择CONFIG_DISCONTIGMEM决定取舍。为什么呢?这就是出于前面博客中所叙述对物理存储空间质地一致性的考虑。

我们先来看用于NUMA结构的alloc_pages代码实现:


/*
 * This can be refined. Currently, tries to do round robin, instead
 * should do concentratic circle search, starting from current node.
 */
struct page * alloc_pages(int gfp_mask, unsigned long order)
{
	struct page *ret = 0;
	pg_data_t *start, *temp;
#ifndef CONFIG_NUMA
	unsigned long flags;
	static pg_data_t *next = 0;
#endif

	if (order >= MAX_ORDER)
		return NULL;
#ifdef CONFIG_NUMA
	temp = NODE_DATA(numa_node_id());
#else
	spin_lock_irqsave(&node_lock, flags);
	if (!next) next = pgdat_list;
	temp = next;
	next = next->node_next;
	spin_unlock_irqrestore(&node_lock, flags);
#endif
	start = temp;
	while (temp) {
		if ((ret = alloc_pages_pgdat(temp, gfp_mask, order)))
			return(ret);
		temp = temp->node_next;
	}
	temp = pgdat_list;
	while (temp != start) {
		if ((ret = alloc_pages_pgdat(temp, gfp_mask, order)))
			return(ret);
		temp = temp->node_next;
	}
	return(0);
}

首先,对NUMA的支持是通过条件编译作为可选项提供的,所以这段代码仅在可选项CONFIG_DISCONTIGMEM有定义时才能得到编译。不过,这里用来作为条件的是“不连续存储空间”,而不是CONFIG_NUMA。其实,不连续的物理存储空间是一种广义的NUMA,因为那说明在最低物理地址和最高物理地址之间存在着空洞,而有空洞的空间当然是非均质的。所以在地址不连续的物理空间也要像在质地不均匀的物理空间那样划分出若干连续(而且均匀)的节点。所以,在存储空间不连续的系统中,每个模块都有若干个节点,因而都有个pg_data_t数据结构的队列。

调用时有两个参数。第一参数gfp_mask是个整数,表示采用哪一种分配策略;第二个参数order表示所需的物理块大小,可以使1、2、4、。。。、直到2^MAX_ORDER个页面。 

在NUMA结构的系统中,可以通过宏操作NUMA_DATA和numa_node_id找到CPU所在节点的pg_data_t数据结构队列。而在不连续的UMA结构中,则也有个pg_data_t数据结构的队列pgdat_list,分配时轮流从各个节点开始,以求各节点负荷的平衡。

函数中主要的操作在于两个while循环,它们分两截 (先是从temp开始到队列的末尾,然后回头从第一个节点到最初开始的地方)扫描队列中所有的节点,直至在某个节点内分配成功,或彻底失败而返回0。对于每个节点,调用alloc_pages_pgdat试图分配所需的页面,这个函数的代码如下:

static struct page * alloc_pages_pgdat(pg_data_t *pgdat, int gfp_mask,
	unsigned long order)
{
	return __alloc_pages(pgdat->node_zonelists + gfp_mask, order);
}

可见,参数gfp_mask在这里用作给定节点中数组node_zonelists的下标,决定具体的分配策略。把这段代码与下面用于连续空间UMA结构的alloc_pages对照一下,就可以看出区别:在连续空间UMA结构中只有一个节点contig_page_data,而在NUMA结构或不连续空间UMA结构中则有多个。

连续空间UMA结构的alloc_pages实现如下:

#ifndef CONFIG_DISCONTIGMEM
static inline struct page * alloc_pages(int gfp_mask, unsigned long order)
{
	/*
	 * Gets optimized away by the compiler.
	 */
	if (order >= MAX_ORDER)
		return NULL;
	return __alloc_pages(contig_page_data.node_zonelists+(gfp_mask), order);
}

与NUMA结构的alloc_pages相反,这个函数仅在CONFIG_DISCONTIGMEM未定义时才得到编译。所以这两个同名函数只有一个会得到编译。

具体的页面分配由函数__alloc_pages完成,我们分段来看:

alloc_pages=>__alloc_pages

/*
 * This is the 'heart' of the zoned buddy allocator:
 */
struct page * __alloc_pages(zonelist_t *zonelist, unsigned long order)
{
	zone_t **zone;
	int direct_reclaim = 0;
	unsigned int gfp_mask = zonelist->gfp_mask;
	struct page * page;

	/*
	 * Allocations put pressure on the VM subsystem.
	 */
	memory_pressure++;

	/*
	 * (If anyone calls gfp from interrupts nonatomically then it
	 * will sooner or later tripped up by a schedule().)
	 *
	 * We are falling back to lower-level zones if allocation
	 * in a higher zone fails.
	 */

	/*
	 * Can we take pages directly from the inactive_clean
	 * list?
	 */
	if (order == 0 && (gfp_mask & __GFP_WAIT) &&
			!(current->flags & PF_MEMALLOC))
		direct_reclaim = 1;

	/*
	 * If we are about to get low on free pages and we also have
	 * an inactive page shortage, wake up kswapd.
	 */
	if (inactive_shortage() > inactive_target / 2 && free_shortage())
		wakeup_kswapd(0);
	/*
	 * If we are about to get low on free pages and cleaning
	 * the inactive_dirty pages would fix the situation,
	 * wake up bdflush.
	 */
	else if (free_shortage() && nr_inactive_dirty_pages > free_shortage()
			&& nr_inactive_dirty_pages >= freepages.high)
		wakeup_bdflush(0);

调用时有两个参数。第一个参数zonelist指向代表着一个具体分配策略的zonelist_t数据结构。另一个参数order则与前面alloc_pages中的相同。全局变量memory_pressure表示内存页面管理所受的压力,分配内存页面时递增,归还时则递减。这里的局部变量gfp_mask来自代表着具体分配策略的数据结构,是一些用于控制目的的标志位。如果要求分配的只是单个页面,而且要等待分配完成,又不是用于管理目的,则把一个局部变量direct_reclaim设成1,表示可以从相应页面管理区的不活跃干净页面缓冲队列中回收。这些页面的内容都已写出至页面交换设备或文件中,只是还保存着页面的内容,使得在需要这个页面的内容时无需再从设备或文件读入,但是当空闲页面短缺时,就顾不得那么多了。由于一般而言这些页面不一定能像真正的空闲页面那样连成块,所以仅在要求分配的那个页面时才能从这些页面中回收。此外,当发现可分配页面短缺时,还要唤醒kswapd和bdflush两个内核线程,让它们设法腾出一些内存页面来(后面的 linux内存管理-页面的定期换出 会讲解)。我们继续往下看:

alloc_pages=>__alloc_pages

try_again:
	/*
	 * First, see if we have any zones with lots of free memory.
	 *
	 * We allocate free memory first because it doesn't contain
	 * any data ... DUH!
	 */
	zone = zonelist->zones;
	for (;;) {
		zone_t *z = *(zone++);
		if (!z)
			break;
		if (!z->size)
			BUG();

		if (z->free_pages >= z->pages_low) {
			page = rmqueue(z, order);
			if (page)
				return page;
		} else if (z->free_pages < z->pages_min &&
					waitqueue_active(&kreclaimd_wait)) {
				wake_up_interruptible(&kreclaimd_wait);
		}
	}

这是对一个分配策略中所规定的所有页面管理区的循环。循环中依次考察各个管理区中空闲页面的总量,如果总量尚在低水位以上,就通过rmqueue试图从该管理区中分配。要是发现管理区中的空闲页面总量已经降到了最低点,而且有进程(实际上只能是内核线程kreclaimd)在一个等待队列kreclaimd_wait中睡眠,就把它会唤醒,让它帮忙回收一些页面备用。函数rmqueue试图从一个页面管理区分配若干连续的内存页面,其代码如下:

alloc_pages=>__alloc_pages=>rmqueue

static struct page * rmqueue(zone_t *zone, unsigned long order)
{
	free_area_t * area = zone->free_area + order;
	unsigned long curr_order = order;
	struct list_head *head, *curr;
	unsigned long flags;
	struct page *page;

	spin_lock_irqsave(&zone->lock, flags);
	do {
		head = &area->free_list;
		curr = memlist_next(head);

		if (curr != head) {
			unsigned int index;

			page = memlist_entry(curr, struct page, list);
			if (BAD_RANGE(zone,page))
				BUG();
			memlist_del(curr);
			index = (page - mem_map) - zone->offset;
			MARK_USED(index, curr_order, area);
			zone->free_pages -= 1 << order;

			page = expand(zone, page, index, order, curr_order, area);
			spin_unlock_irqrestore(&zone->lock, flags);

			set_page_count(page, 1);
			if (BAD_RANGE(zone,page))
				BUG();
			DEBUG_ADD_PAGE
			return page;	
		}
		curr_order++;
		area++;
	} while (curr_order < MAX_ORDER);
	spin_unlock_irqrestore(&zone->lock, flags);

	return NULL;
}

以前讲过,代表物理页面的page数据结构,以双向链的形式链接在管理区的某个空闲队列中。分配页面时当然要把它从队列中摘链,而摘链的过程是不容许其他的进程、其他的处理器(如果有的话)来打扰的。所以要用spin_lock_irqsave将相应的分区加上锁,不容许打扰。管理区结构中的空闲区zone->free_area是个结构数组,所以zone->free_area + order就指向链接所需大小的物理内存块的队列头。主要的操作是在一个do_while循环中进行。它首先在恰好满足大小要求的队列里分配,如果不行的话就试试更大的(指物理内存块)队列中分配,成功的话,就把分配的大块中剩余的部分分解成小块而链入相应的队列(通过196行的expand)。

第188行中的memlist_entry从一个非空的队列里取第一个结构page元素,然后通过memlist_del将其从队列中摘除。

alloc_pages=>__alloc_pages=>rmqueue=>expand


static inline struct page * expand (zone_t *zone, struct page *page,
	 unsigned long index, int low, int high, free_area_t * area)
{
	unsigned long size = 1 << high;

	while (high > low) {
		if (BAD_RANGE(zone,page))
			BUG();
		area--;
		high--;
		size >>= 1;
		memlist_add_head(&(page)->list, &(area)->free_list);
		MARK_USED(index, high, area);
		index += size;
		page += size;
	}
	if (BAD_RANGE(zone,page))
		BUG();
	return page;
}

调用参数中的low对应于表示所需物理块大小的order,而high则对应于表示当前空闲区队列(也就是从中得到能满足要求的物理块的队列)的curr_order。当两者相符时,从155行开始的while循环就被跳过了。若是分配到的物理块大于所需的大小(不可能小于所需的大小),那就将该物理块链入低一档也就是物理块大小减半的空闲块队列中去,并相应设置该空闲区队列的位图,这是在第158行至162行中完成的。然后从该物理块中切去一半,而以其后半部作为一个新的物理块(第163和163行),而后开始下一轮循环也就是处理更低一档的空闲块队列。这样,最后必有high和low两者相等,也就是实际剩下的物理块与要求恰好相符的时候,循环就结束了。

就这样,rmqueue一直往下扫描,直到成功或者最终失败。如果rmqueue失败,则__alloc_pages通过其for循环降格以求,接着试分配策略中规定的下一个管理区,直到成功,或者碰到了空指针而最终失败(见327行)。如果分配成功了,则__alloc_pages返回一个page结构指针,指向页面块中第一个页面的page结构,并且该page结构中的使用计数count为1。如果每次分配的都是单个页面(order为0),则自然每个页面的使用计数都是1。

要是给定分配策略中所有的页面管理区都失败了,那就只好加大力度再试,一是降低对页面管理区中保持水位的要求,二是把缓冲在管理区中的不活跃干净页面也考虑进去,我们在往下看__alloc_pages的代码:

alloc_pages=>__alloc_pages

	/*
	 * Try to allocate a page from a zone with a HIGH
	 * amount of free + inactive_clean pages.
	 *
	 * If there is a lot of activity, inactive_target
	 * will be high and we'll have a good chance of
	 * finding a page using the HIGH limit.
	 */
	page = __alloc_pages_limit(zonelist, order, PAGES_HIGH, direct_reclaim);
	if (page)
		return page;

	/*
	 * Then try to allocate a page from a zone with more
	 * than zone->pages_low free + inactive_clean pages.
	 *
	 * When the working set is very large and VM activity
	 * is low, we're most likely to have our allocation
	 * succeed here.
	 */
	page = __alloc_pages_limit(zonelist, order, PAGES_LOW, direct_reclaim);
	if (page)
		return page;

这里先以参数PAGES_HIGH调用__alloc_pages_limit;如果还不行就再加大力度,改以PAGES_LOW再调用一次。函数__alloc_pages_limit代码如下:

alloc_pages=>__alloc_pages=>__alloc_pages_limit


/*
 * This function does the dirty work for __alloc_pages
 * and is separated out to keep the code size smaller.
 * (suggested by Davem at 1:30 AM, typed by Rik at 6 AM)
 */
static struct page * __alloc_pages_limit(zonelist_t *zonelist,
			unsigned long order, int limit, int direct_reclaim)
{
	zone_t **zone = zonelist->zones;

	for (;;) {
		zone_t *z = *(zone++);
		unsigned long water_mark;

		if (!z)
			break;
		if (!z->size)
			BUG();

		/*
		 * We allocate if the number of free + inactive_clean
		 * pages is above the watermark.
		 */
		switch (limit) {
			default:
			case PAGES_MIN:
				water_mark = z->pages_min;
				break;
			case PAGES_LOW:
				water_mark = z->pages_low;
				break;
			case PAGES_HIGH:
				water_mark = z->pages_high;
		}

		if (z->free_pages + z->inactive_clean_pages > water_mark) {
			struct page *page = NULL;
			/* If possible, reclaim a page directly. */
			if (direct_reclaim && z->free_pages < z->pages_min + 8)
				page = reclaim_page(z);
			/* If that fails, fall back to rmqueue. */
			if (!page)
				page = rmqueue(z, order);
			if (page)
				return page;
		}
	}

	/* Found nothing. */
	return NULL;
}

这个函数的代码与前面__alloc_pages中的for循环在逻辑上只是稍有不同,我们把它留给读者。其中reclaim_page从页面管理区的inactive_clean_pages队列中回收页面,其代码我们会在后面的博客中列出,我们可以再学习了页面的换入和换出以后自己阅读。注意调用这个函数的条件是参数direct_reclaim非0,所以要求分配的一定是单个页面。

还是不行的话,那就说明这些管理区中的页面已经严重短缺了,让我们看看__alloc_pages是如何应对的:

alloc_pages=>__alloc_pages

	/*
	 * OK, none of the zones on our zonelist has lots
	 * of pages free.
	 *
	 * We wake up kswapd, in the hope that kswapd will
	 * resolve this situation before memory gets tight.
	 *
	 * We also yield the CPU, because that:
	 * - gives kswapd a chance to do something
	 * - slows down allocations, in particular the
	 *   allocations from the fast allocator that's
	 *   causing the problems ...
	 * - ... which minimises the impact the "bad guys"
	 *   have on the rest of the system
	 * - if we don't have __GFP_IO set, kswapd may be
	 *   able to free some memory we can't free ourselves
	 */
	wakeup_kswapd(0);
	if (gfp_mask & __GFP_WAIT) {
		__set_current_state(TASK_RUNNING);
		current->policy |= SCHED_YIELD;
		schedule();
	}

	/*
	 * After waking up kswapd, we try to allocate a page
	 * from any zone which isn't critical yet.
	 *
	 * Kswapd should, in most situations, bring the situation
	 * back to normal in no time.
	 */
	page = __alloc_pages_limit(zonelist, order, PAGES_MIN, direct_reclaim);
	if (page)
		return page;

首先是唤醒内核线程kswapd,让它设法换出一些页面。如果分配策略表明对于要求分配的页面是志在必得,分配不到时宁可等待,就让系统来一次调度,并且让当前进程为其他进程让一下路。这样,一来让kswapd有可能立即被调度运行,二来其他进程有可能会释放出一些页面,再说也可以减缓了要求分配页面的速度,减轻了压力。当请求分配页面的进程再次被调度运行时,或者分配策略表明不允许等待时,就以参数PAGES_MIN调用一次__alloc_pages_limit。可是,要是再失败呢?这时候就要看是谁在要求分配内存页面了。如果要求分配页面的进程(或线程)是kswapd或kreclaimd,本身就是内存分配工作者,要求分配内存页面的目的是执行公务,是要更好地分配内存页面,这当然比一般的进程重要。这些进程的task_struct结构中flags字段的PF_MEMALLOC标志位为1。我们先看对于一般进程,即PF_MEMALLOC标志位为0的进程的对策。

alloc_pages=>__alloc_pages

	/*
	 * Damn, we didn't succeed.
	 *
	 * This can be due to 2 reasons:
	 * - we're doing a higher-order allocation
	 * 	--> move pages to the free list until we succeed
	 * - we're /really/ tight on memory
	 * 	--> wait on the kswapd waitqueue until memory is freed
	 */
	if (!(current->flags & PF_MEMALLOC)) {
		/*
		 * Are we dealing with a higher order allocation?
		 *
		 * Move pages from the inactive_clean to the free list
		 * in the hope of creating a large, physically contiguous
		 * piece of free memory.
		 */
		if (order > 0 && (gfp_mask & __GFP_WAIT)) {
			zone = zonelist->zones;
			/* First, clean some dirty pages. */
			current->flags |= PF_MEMALLOC;
			page_launder(gfp_mask, 1);
			current->flags &= ~PF_MEMALLOC;
			for (;;) {
				zone_t *z = *(zone++);
				if (!z)
					break;
				if (!z->size)
					continue;
				while (z->inactive_clean_pages) {
					struct page * page;
					/* Move one page to the free list. */
					page = reclaim_page(z);
					if (!page)
						break;
					__free_page(page);
					/* Try if the allocation succeeds. */
					page = rmqueue(z, order);
					if (page)
						return page;
				}
			}
		}
		/*
		 * When we arrive here, we are really tight on memory.
		 *
		 * We wake up kswapd and sleep until kswapd wakes us
		 * up again. After that we loop back to the start.
		 *
		 * We have to do this because something else might eat
		 * the memory kswapd frees for us and we need to be
		 * reliable. Note that we don't loop back for higher
		 * order allocations since it is possible that kswapd
		 * simply cannot free a large enough contiguous area
		 * of memory *ever*.
		 */
		if ((gfp_mask & (__GFP_WAIT|__GFP_IO)) == (__GFP_WAIT|__GFP_IO)) {
			wakeup_kswapd(1);
			memory_pressure++;
			if (!order)
				goto try_again;
		/*
		 * If __GFP_IO isn't set, we can't wait on kswapd because
		 * kswapd just might need some IO locks /we/ are holding ...
		 *
		 * SUBTLE: The scheduling point above makes sure that
		 * kswapd does get the chance to free memory we can't
		 * free ourselves...
		 */
		} else if (gfp_mask & __GFP_WAIT) {
			try_to_free_pages(gfp_mask);
			memory_pressure++;
			if (!order)
				goto try_again;
		}

	}

分配内存页面失败的原因可能是两方面的,一种可能是可分配页面的总量实在已经太少了;另一种是总量其实还不少,但是所要求的页面块大小却不能满足,此时往往有不少单个的页面在管理区的inactive_clean_pages队列中,如果加以回收就有可能拼装起较大的页面块。同时,可能还有些脏页面在全局的inactive_dirty_list队列中,把脏页面的内容写出到交换设备上或文件中,就可以使它们变成干净页面而加以回收。所以,针对第二种可能,代码中通过page_launder把脏页面洗干净,然后通过一个for循环在各个页面管理区中回收和释放干净页面。具体的回收和释放是通过一个while循环完成的。在通过__free_page释放页面时会把空闲页面拼装其尽可能大的页面块,所以在每回收了一个页面以后都要调用rmqueue试一下,看看是否已经能满足要求。值得注意的是,这里在调用page_launder期间把当前进程的PF_MEMALLOC标志位设成1,使其有了执行公务时的特权。为什么要这样做呢?这是因为在page_launder中也会要求分配一些临时性的工作页面,不把PF_MEMALLOC标志位设成1就可能递归地进入这里的409-476行。

如果回收了这样的页面以后还是不行,那就是可分配页面的总量不够了。这时候一种办法是唤醒kswapd,而要求分配页面的进程则睡眠等待,由kswapd在完成了一轮运行之后再反过来唤醒要求分配页面的进程。然后,如果要求分配的是单个页面,就通过goto语句转回__alloc_pages开头处的标号try_again处。另一种办法是直接调用try_to_free_pages,这个函数本来是由kswapd调用的。

那么,如果是执行公务呢?或者,虽然不是执行公务,但已想尽办法,采取了一切措施,只不过因为要求分配的是成块的页面才没有转回前面的标号try_again处。

前面我们看到,一次次加大力度调用__alloc_pages_limit时,实际上还是有所保留的。例如,最后一次以PAGES_MIN为参数,此时判断是否可以分配的准则是管理区中可分配页面的水位高于z->pages_min。之所以还留着一点老本,是为应付紧急状况,而现在已到了不惜血本的时候了。

我们继续往下看__alloc_pages的代码。

alloc_pages=>__alloc_pages

	/*
	 * Final phase: allocate anything we can!
	 *
	 * Higher order allocations, GFP_ATOMIC allocations and
	 * recursive allocations (PF_MEMALLOC) end up here.
	 *
	 * Only recursive allocations can use the very last pages
	 * in the system, otherwise it would be just too easy to
	 * deadlock the system...
	 */
	zone = zonelist->zones;
	for (;;) {
		zone_t *z = *(zone++);
		struct page * page = NULL;
		if (!z)
			break;
		if (!z->size)
			BUG();

		/*
		 * SUBTLE: direct_reclaim is only possible if the task
		 * becomes PF_MEMALLOC while looping above. This will
		 * happen when the OOM killer selects this task for
		 * instant execution...
		 */
		if (direct_reclaim) {
			page = reclaim_page(z);
			if (page)
				return page;
		}

		/* XXX: is pages_min/4 a good amount to reserve for this? */
		if (z->free_pages < z->pages_min / 4 &&
				!(current->flags & PF_MEMALLOC))
			continue;
		page = rmqueue(z, order);
		if (page)
			return page;
	}

	/* No luck.. */
	printk(KERN_ERR "__alloc_pages: %lu-order allocation failed.\n", order);
	return NULL;
}

如果连这也失败,那一定是系统有问题了。

读者也许会说:好家伙,分配一个(或几个)内存页面有这么麻烦,那CPU还有多少个时间能用于实质性的计算呢?要知道我们这里是假定分配页面的努力屡战屡败,而又屡败屡战,这才有这么多艰苦卓绝的努力。实际上,绝大多数的分配页面操作都是在分配策略所规定的第一个页面管理区中就成功了。不过,从这里我们可以看到设计一个系统需要何等周密的考虑。

おすすめ

転載: blog.csdn.net/guoguangwu/article/details/120687885