linux内存管理-页面的定期换出

这个情景比较长,我们得有点耐心。

为了避免总是在CPU忙碌的时候,也就是在缺页异常发生的时候,临时再来搜索可供换出的内存页面并加以换出,linux内核定期地检查并预先将若干页面换出,腾出空间,以减轻系统在缺页异常发生时的负担。当然,由于无法确切地预测页面的使用,即使这样做了也还是不能完全杜绝在缺页异常发生时内存没有空闲页面,而只好临时寻找可换出页面的可能。但是,这样毕竟可以减少其发生的概率。并且,通过选择适当的参数,例如每隔多久换出一次,每次换出多少页面,可以使得在缺页异常发生时必须临时寻找页面换出的情况实际上很少发生。为此,在linux内核中设置了一个专司定期将页面换出的守护神kswapd。

从原理上说,kswapd相当于一个进程,有其自身的进程控制块task_struct结构,跟其他进程一样受到内核的调度。而正因为内核将它按进程调度,就可以让它在系统相对空闲的时候来运行。不过,与普通的进程相比,kswapd还是有其特殊性。首先,它没有自己独立的地址空间,所以在近代操作系统理论中称为线程(thread)以示区别。那么,kswapd使用谁的地址空间呢?它使用的是内核的空间。在这一点上,它与中断服务程序相似。其次,它的代码是静态地链接在内核中的,可以直接调用内核中的各种子程序,而不像普通的进程那样只能通过系统调用,使用预先定义好的一组功能。

本博客讲述kswapd受内核调度而运行并走完一条例行路线的全过程。

线程kswapd的源代码基本上都在mm/vmscan.c中。先来看它的建立:

static int __init kswapd_init(void)
{
	printk("Starting kswapd v1.8\n");
	swap_setup();
	kernel_thread(kswapd, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL);
	kernel_thread(kreclaimd, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL);
	return 0;
}

函数kswapd_init是在系统初始化期间受到调用的,它主要做两件事。第一件是在swap_setup中根据物理内存的大小设定一个全局变量page_cluster:

kswapd_init=>swap_setup


/*
 * Perform any setup for the swap system
 */
void __init swap_setup(void)
{
	/* Use a smaller cluster for memory <16MB or <32MB */
	if (num_physpages < ((16 * 1024 * 1024) >> PAGE_SHIFT))
		page_cluster = 2;
	else if (num_physpages < ((32 * 1024 * 1024) >> PAGE_SHIFT))
		page_cluster = 3;
	else
		page_cluster = 4;
}

这是一个跟磁盘设备驱动有关的参数。由于读磁盘时先要经过寻道,并且寻道是个比较费时间的操作,所以如果每次只读一个页面是不经济的。比较好的办法是既然读了就干脆多读几个页面,称为预读。但是预读意味着每次需要暂存更多的内存页面,所以需要决定一个适当的数量,而根据物理内存本身的大小来确定这个参数显然是合理的。第二件事情就是创建线程kswapd,这是由kernel_thread完成的。这里还创建了另一个线程kreclaimd,也是跟存储管理有关,不过不像kswapd那么复杂和重要,所以我们暂且把它放在一边。关于建立线程的详情后面到的进程管理博客会讲,这里暂且假定线程kswapd就此建立了,并且从函数kswapd开始执行。其代码在mm/vmscan.c中:


/*
 * The background pageout daemon, started as a kernel thread
 * from the init process. 
 *
 * This basically trickles out pages so that we have _some_
 * free memory available even if there is no other activity
 * that frees anything up. This is needed for things like routing
 * etc, where we otherwise might have all activity going on in
 * asynchronous contexts that cannot page things out.
 *
 * If there are applications that are active memory-allocators
 * (most normal use), this basically shouldn't matter.
 */
int kswapd(void *unused)
{
	struct task_struct *tsk = current;

	tsk->session = 1;
	tsk->pgrp = 1;
	strcpy(tsk->comm, "kswapd");
	sigfillset(&tsk->blocked);
	kswapd_task = tsk;
	
	/*
	 * Tell the memory management that we're a "memory allocator",
	 * and that if we need more memory we should get access to it
	 * regardless (see "__alloc_pages()"). "kswapd" should
	 * never get caught in the normal page freeing logic.
	 *
	 * (Kswapd normally doesn't need memory anyway, but sometimes
	 * you need a small amount of memory in order to be able to
	 * page out something else, and this flag essentially protects
	 * us from recursively trying to free more memory as we're
	 * trying to free the first piece of memory in the first place).
	 */
	tsk->flags |= PF_MEMALLOC;

	/*
	 * Kswapd main loop.
	 */
	for (;;) {
		static int recalc = 0;

		/* If needed, try to free some memory. */
		if (inactive_shortage() || free_shortage()) {
			int wait = 0;
			/* Do we need to do some synchronous flushing? */
			if (waitqueue_active(&kswapd_done))
				wait = 1;
			do_try_to_free_pages(GFP_KSWAPD, wait);
		}

		/*
		 * Do some (very minimal) background scanning. This
		 * will scan all pages on the active list once
		 * every minute. This clears old referenced bits
		 * and moves unused pages to the inactive list.
		 */
		refill_inactive_scan(6, 0);

		/* Once a second, recalculate some VM stats. */
		if (time_after(jiffies, recalc + HZ)) {
			recalc = jiffies;
			recalculate_vm_stats();
		}

		/*
		 * Wake up everybody waiting for free memory
		 * and unplug the disk queue.
		 */
		wake_up_all(&kswapd_done);
		run_task_queue(&tq_disk);

		/* 
		 * We go to sleep if either the free page shortage
		 * or the inactive page shortage is gone. We do this
		 * because:
		 * 1) we need no more free pages   or
		 * 2) the inactive pages need to be flushed to disk,
		 *    it wouldn't help to eat CPU time now ...
		 *
		 * We go to sleep for one second, but if it's needed
		 * we'll be woken up earlier...
		 */
		if (!free_shortage() || !inactive_shortage()) {
			interruptible_sleep_on_timeout(&kswapd_wait, HZ);
		/*
		 * If we couldn't free enough memory, we see if it was
		 * due to the system just not having enough memory.
		 * If that is the case, the only solution is to kill
		 * a process (the alternative is enternal deadlock).
		 *
		 * If there still is enough memory around, we just loop
		 * and try free some more memory...
		 */
		} else if (out_of_memory()) {
			oom_kill();
		}
	}
}

在一些简单的初始化操作以后,程序便进入一个无限循环。在每次循环的末尾一般都会调用interruptible_sleep_on_timeout进入睡眠,让内核自由地调度别的进程运行。但是内核在一定时间以后又会唤醒并调度kswapd继续运行,这时候kswapd就又回到这无限循环开始的地方。那么,这一定时间是多长呢?这就是常数HZ。HZ决定了内核中每秒钟有多少次时钟中断。用户可以在编译内核前的系统配置阶段改变其数值,但是一经编译就定下来了。所以,在调用interruptible_sleep_on_timeout时的参数为HZ,表示1秒钟以后又要调度kswapd继续运行。换言之,对interruptible_sleep_on_timeout的调用一进去就得1秒钟以后才回来。但是,在有些情况下内核也会在不到1秒钟就把它唤醒,那样kswapd就会提前返回而开始新的一轮循环。所以,这个循环至少每隔1秒钟执行一遍,这就是kswapd的例行路线。

那么,kswapd在这至少每秒一次的例行路线中做些什么呢?可以把它分成两部分。第一部分是在发现物理页面已经短缺的情况下才进行的,目的在于预先找出若干页面,且将这些页面的映射断开,使这些物理页面从活跃状态转入不活跃状态,为页面的换出做好准备。第二部分是每次都要执行的,目的在于把已经处于不活跃状态的脏页面写入交换设备,使它们成为不活跃干净页面继续缓冲,或进一步回收一些这样的页面成为空闲页面。

先看第一部分,首先检查内存中可供分配或周转的物理页面是否短缺:

kswapd=>inactive_shortage


/*
 * How many inactive pages are we short?
 */
int inactive_shortage(void)
{
	int shortage = 0;

	shortage += freepages.high;
	shortage += inactive_target;
	shortage -= nr_free_pages();
	shortage -= nr_inactive_clean_pages();
	shortage -= nr_inactive_dirty_pages;

	if (shortage > 0)
		return shortage;

	return 0;
}

系统中应该维持的物理页面供应量由两个全局变量确定,那就是freepages.high和inactive_target,分别为空闲页面的数量和不活跃页面的数量,二者之和为正常情况下潜在的供应量。而这些内存页面的来源则有三个方面。一方面是当前尚存的空闲页面,这是立即就可以分配的页面。这些页面分散在各个页面管理区中,并且合并成地址连续、大小为2、4、8、...、2^N个页面的页面块,其数量由nr_free_pages加以统计。另一方面是现有的不活跃干净页面,这些页面本质上也是马上就可以分配的页面,但是页面中的内容可能还会用到,所以多保留一些这样的页面有助于减少从交换设备的读入。这些页面也分散在各个页面管理区中,但并不合并成块,其数量由nr_inactive_clean_pages加以统计。最后是现有的不活跃脏页面,内核中的全局变量nr_inactive_dirty_pages记录着当前此类页面的数量。上述两个函数的代码都比较简单,读者可以自己阅读。

不过,光维持潜在的物理页面供应总量还不够,还要通过free_shortage检查是否有某个具体管理区中有严重的短缺,即直接可供分配的页面数量(除不活跃脏页面以外)是否小于一个最低限度。这个函数的代码在mm/vmscan.c中,我们也把它留给读者。

如果发现可供分配的内存页面短缺,那就要设法释放和换出若干页面,这是通过do_try_to_free_pages完成的。不过在此之前还要通过waitqueue_active,看看kswapd_done队列中是否有函数在等待执行,并把查看的结果作为参数传递给do_try_to_free_pages。在后面的博客中,读者将看到内核中有几个特殊的队列,内核中各个部分(主要是设备启动)可以把一些低层函数挂入这样的队列,使得这些函数在某种事件发生时就能得到执行。而kswapd_done,就正在这样的一个队列。凡是挂入这个队列的函数,在kswapd每完成一趟例行的操作时就能得到执行。这里的inline函数waitqueue_active就是查看是否有函数在这个队列中等待执行。其定义如下:

kswapd=>waitqueue_active


static inline int waitqueue_active(wait_queue_head_t *q)
{
#if WAITQUEUE_DEBUG
	if (!q)
		WQ_BUG();
	CHECK_MAGIC_WQHEAD(q);
#endif

	return !list_empty(&q->task_list);
}

下面就是调用do_try_to_free_pages,试图腾出一些内存页面,其代码如下:

kswapd=>do_try_to_free_pages


static int do_try_to_free_pages(unsigned int gfp_mask, int user)
{
	int ret = 0;

	/*
	 * If we're low on free pages, move pages from the
	 * inactive_dirty list to the inactive_clean list.
	 *
	 * Usually bdflush will have pre-cleaned the pages
	 * before we get around to moving them to the other
	 * list, so this is a relatively cheap operation.
	 */
	if (free_shortage() || nr_inactive_dirty_pages > nr_free_pages() +
			nr_inactive_clean_pages())
		ret += page_launder(gfp_mask, user);

	/*
	 * If needed, we move pages from the active list
	 * to the inactive list. We also "eat" pages from
	 * the inode and dentry cache whenever we do this.
	 */
	if (free_shortage() || inactive_shortage()) {
		shrink_dcache_memory(6, gfp_mask);
		shrink_icache_memory(6, gfp_mask);
		ret += refill_inactive(gfp_mask, user);
	} else {
		/*
		 * Reclaim unused slab cache memory.
		 */
		kmem_cache_reap(gfp_mask);
		ret = 1;
	}

	return ret;
}

将活跃页面的映射断开,使之转入不活跃状态,甚至进而换出到交换设备上,是不得已而为之。因为谁也不能精确地预测到底哪一些页面是合适的换出对象。虽然一般而言“最近最少用到”是个有效的准则,但也并不是放诸四海而皆准。所以,能够不动现役页面是最理想的。基于这样的考虑,这里所作的是先易后难,逐步加强力度。首先是调用page_launder,试图把已经转入不活跃状态的脏页面洗净,使它们变成立即可以分配的页面。函数名中的launder,就是洗衣工的意思。这个函数一方面(基本上)定期地受到kswapd的调用,一方面在每当需要分配内存页面,而又无页面可供分配时,临时地受到调用。其代码如下:

kswapd=>do_try_to_free_pages=>page_launder

/**
 * page_launder - clean dirty inactive pages, move to inactive_clean list
 * @gfp_mask: what operations we are allowed to do
 * @sync: should we wait synchronously for the cleaning of pages
 *
 * When this function is called, we are most likely low on free +
 * inactive_clean pages. Since we want to refill those pages as
 * soon as possible, we'll make two loops over the inactive list,
 * one to move the already cleaned pages to the inactive_clean lists
 * and one to (often asynchronously) clean the dirty inactive pages.
 *
 * In situations where kswapd cannot keep up, user processes will
 * end up calling this function. Since the user process needs to
 * have a page before it can continue with its allocation, we'll
 * do synchronous page flushing in that case.
 *
 * This code is heavily inspired by the FreeBSD source code. Thanks
 * go out to Matthew Dillon.
 */
#define MAX_LAUNDER 		(4 * (1 << page_cluster))
int page_launder(int gfp_mask, int sync)
{
	int launder_loop, maxscan, cleaned_pages, maxlaunder;
	int can_get_io_locks;
	struct list_head * page_lru;
	struct page * page;

	/*
	 * We can only grab the IO locks (eg. for flushing dirty
	 * buffers to disk) if __GFP_IO is set.
	 */
	can_get_io_locks = gfp_mask & __GFP_IO;

	launder_loop = 0;
	maxlaunder = 0;
	cleaned_pages = 0;

dirty_page_rescan:
	spin_lock(&pagemap_lru_lock);
	maxscan = nr_inactive_dirty_pages;
	while ((page_lru = inactive_dirty_list.prev) != &inactive_dirty_list &&
				maxscan-- > 0) {
		page = list_entry(page_lru, struct page, lru);

		/* Wrong page on list?! (list corruption, should not happen) */
		if (!PageInactiveDirty(page)) {
			printk("VM: page_launder, wrong page on list.\n");
			list_del(page_lru);
			nr_inactive_dirty_pages--;
			page->zone->inactive_dirty_pages--;
			continue;
		}

		/* Page is or was in use?  Move it to the active list. */
		if (PageTestandClearReferenced(page) || page->age > 0 ||
				(!page->buffers && page_count(page) > 1) ||
				page_ramdisk(page)) {
			del_page_from_inactive_dirty_list(page);
			add_page_to_active_list(page);
			continue;
		}

		/*
		 * The page is locked. IO in progress?
		 * Move it to the back of the list.
		 */
		if (TryLockPage(page)) {
			list_del(page_lru);
			list_add(page_lru, &inactive_dirty_list);
			continue;
		}

		/*
		 * Dirty swap-cache page? Write it out if
		 * last copy..
		 */
		if (PageDirty(page)) {
			int (*writepage)(struct page *) = page->mapping->a_ops->writepage;
			int result;

			if (!writepage)
				goto page_active;

			/* First time through? Move it to the back of the list */
			if (!launder_loop) {
				list_del(page_lru);
				list_add(page_lru, &inactive_dirty_list);
				UnlockPage(page);
				continue;
			}

			/* OK, do a physical asynchronous write to swap.  */
			ClearPageDirty(page);
			page_cache_get(page);
			spin_unlock(&pagemap_lru_lock);

			result = writepage(page);
			page_cache_release(page);

			/* And re-start the thing.. */
			spin_lock(&pagemap_lru_lock);
			if (result != 1)
				continue;
			/* writepage refused to do anything */
			set_page_dirty(page);
			goto page_active;
		}

		/*
		 * If the page has buffers, try to free the buffer mappings
		 * associated with this page. If we succeed we either free
		 * the page (in case it was a buffercache only page) or we
		 * move the page to the inactive_clean list.
		 *
		 * On the first round, we should free all previously cleaned
		 * buffer pages
		 */
		if (page->buffers) {
			int wait, clearedbuf;
			int freed_page = 0;
			/*
			 * Since we might be doing disk IO, we have to
			 * drop the spinlock and take an extra reference
			 * on the page so it doesn't go away from under us.
			 */
			del_page_from_inactive_dirty_list(page);
			page_cache_get(page);
			spin_unlock(&pagemap_lru_lock);

			/* Will we do (asynchronous) IO? */
			if (launder_loop && maxlaunder == 0 && sync)
				wait = 2;	/* Synchrounous IO */
			else if (launder_loop && maxlaunder-- > 0)
				wait = 1;	/* Async IO */
			else
				wait = 0;	/* No IO */

			/* Try to free the page buffers. */
			clearedbuf = try_to_free_buffers(page, wait);

			/*
			 * Re-take the spinlock. Note that we cannot
			 * unlock the page yet since we're still
			 * accessing the page_struct here...
			 */
			spin_lock(&pagemap_lru_lock);

			/* The buffers were not freed. */
			if (!clearedbuf) {
				add_page_to_inactive_dirty_list(page);

			/* The page was only in the buffer cache. */
			} else if (!page->mapping) {
				atomic_dec(&buffermem_pages);
				freed_page = 1;
				cleaned_pages++;

			/* The page has more users besides the cache and us. */
			} else if (page_count(page) > 2) {
				add_page_to_active_list(page);

			/* OK, we "created" a freeable page. */
			} else /* page->mapping && page_count(page) == 2 */ {
				add_page_to_inactive_clean_list(page);
				cleaned_pages++;
			}

			/*
			 * Unlock the page and drop the extra reference.
			 * We can only do it here because we ar accessing
			 * the page struct above.
			 */
			UnlockPage(page);
			page_cache_release(page);

			/* 
			 * If we're freeing buffer cache pages, stop when
			 * we've got enough free memory.
			 */
			if (freed_page && !free_shortage())
				break;
			continue;
		} else if (page->mapping && !PageDirty(page)) {
			/*
			 * If a page had an extra reference in
			 * deactivate_page(), we will find it here.
			 * Now the page is really freeable, so we
			 * move it to the inactive_clean list.
			 */
			del_page_from_inactive_dirty_list(page);
			add_page_to_inactive_clean_list(page);
			UnlockPage(page);
			cleaned_pages++;
		} else {
page_active:
			/*
			 * OK, we don't know what to do with the page.
			 * It's no use keeping it here, so we move it to
			 * the active list.
			 */
			del_page_from_inactive_dirty_list(page);
			add_page_to_active_list(page);
			UnlockPage(page);
		}
	}
	spin_unlock(&pagemap_lru_lock);

代码中的局部变量cleaned_pages用来统计被洗净的页面数量。另一个局部变量launder_loop用来控制扫描不活跃脏页面队列的次数。在第一趟扫描时launder_loop为0,如果有必要进行第二趟扫描,则将其设成1并转回标号dirty_page_rescan处(502行),开始又一次扫描。

对不活跃脏页面队列的扫描是通过一个while循环(505行)进行的。由于在循环中会把有些页面从当前位置转移到队列的尾部,所以除沿着链接指针扫描外还要对数量加以控制,才能避免重复处理同一页面,甚至陷入死循环,这就是局部变量maxscan的作用。

对于队列中的每一个页面,首先要检查它的PG_inactive_dirty标志位为1,否则就根本不应该出现在这个队列中,这一定是出了什么毛病,所以把它从队列中删除(见512行)。除此以外,对于正常的不活跃脏页面,则要一次作下述的检查并作出相应的处理。

  1. 有些页面虽然已经进入不活跃脏页面队列,但是由于情况已经变化,或者当初进入这个队列本来就是冤假错案,因而需要回到活跃页面队列中(519-525行)。这样的页面有:页面在进入不活跃脏页面队列之后又受到了访问,即发生了以此页面为目标的缺页异常,从而恢复了该页面的映射。页面的寿命还未耗尽。页面的page结构中有个字段age,其数值与页面受访问频繁程度有关。后面我们还要回到这个话题。页面并不用作读写文件的缓冲,而页面的使用计数却又大于1。这说明页面在至少一个进程的映射表中有映射。如前所述,一个页面的使用计数在分配时设成1,以后对该页面的每一次使用都使这个计数加1,包括将页面用作读写文件的缓冲。如果一个页面没有用作读写文件的缓冲,那么只要计数大于1就必定还有进程在使用这个页面。页面在受到进程用户空间映射的同时又用于ramdisk,即用内存空间来模拟磁盘,这种页面当然不应该换出到磁盘上。
  2. 页面已被锁住(531行),所以TryLockPage返回1,这表明正在对此页面进行操作,如输入输出,这样的页面应该留在不活跃脏页面队列中,但是把它移动到队列的末尾。注意,对于未被锁住的页面,现在已经锁住了。
  3. 如果页面仍是脏的(541行),即page结构中的PG_dirty标志位为1,则原则上要将其写出到交换设备上,但还有些特殊情况要考虑(541-571行)。首先,所属的address_space数据结构必须提供页面写出操作的函数,否则就只好转到page_active处,将页面送回活跃页面队列中。对于一般的页面交换,所属的address_space数据结构为swapper_space,其address_space_operations结构为swap_aops,所提供的页面写操作为swap_writepage,过这一关是没有问题的。在第一趟扫描中,只是把页面移到同一队列的尾部,而并不写出页面(531-535行)。如果进行第二趟扫描中,那就真的要把页面写出去了。写之前先通过ClearPageDirty把页面的PG_dirty标志位清成0,然后通过由所属address_space数据结构所提供的函数把页面写出去。根据页面的不同使用目的,例如普通的用户空间页面,或者通过mmap建立的文件映射以及文件系统的读写缓冲,具体的操作也不一样。这个写操作可能是同步的(当前进程睡眠,等待写出完成),也可能是异步的,但总是需要一定的时间才能完成,在此期间内核有可能再次进入page_launder,所以需要防止把这个页面再写出一次。这就是把页面的PG_dirty标志位清成0的目的。这样,就不会把同一个页面写出两次了(见541行)。此外,还要考虑页面写出失败的可能,具体的函数在写出失败时应该返回1,使page_launder可以恢复页面的PG_dirty并将其退还给活跃页面队列中(569-570行)。顺便提一下,这里在调用具体的writepage函数时先通过page_cache_get递增页面的使用计数,从这个函数返回后再通过page_cache_release递减这个计数,表示在把页面写出的期间多了一个用户。注意这里并没有立即把写出的页面转移到不活跃干净页面队列中,而只是把它的PG_dirty标志位清成0,这个页面一定是在以前的扫描中写出而变干净的。
  4. 如果页面不再是脏的,并且又是用作文件读写缓冲的页面(582-647行),则先使它脱离不活跃脏页面队列,再通过try_to_free_buffers试图将页面释放。如果不能释放则根据返回值将其退回不活跃脏页面队列,或者链入活跃页面队列,或者不活跃干净页面队列。如果释放成功,则页面的使用计数已经在try_to_free_buffers中减1,638行的page_cache_release再使其减1就达到了0,从而最终将页面释放回到空闲页面队列中。如果成功地释放了一个页面,并且发现系统中的空闲页面已经不再短缺,那么扫描就可以结束了(见644-645行)。否则继续扫描。函数try_to_free_buffers可以自行阅读。
  5. 如果页面不再是脏的,并且在某个address_space数据结构的队列中,这就是已经洗清了的页面,所以把它转移到所属区的不活跃干净页面队列中。
  6. 最后,如果不属于上述的任何一种情况(658行),那就是无法处理的页面,所以把它退回活跃页面队列中。

完成了一趟扫描以后,还要根据系统中空闲页面是否短缺、以及调用参数gfp_mask中的__GFP_IO标志位是否为1,来决定是否进行第二趟扫描。

kswapd=>do_try_to_free_pages=>page_launder


	/*
	 * If we don't have enough free pages, we loop back once
	 * to queue the dirty pages for writeout. When we were called
	 * by a user process (that /needs/ a free page) and we didn't
	 * free anything yet, we wait synchronously on the writeout of
	 * MAX_SYNC_LAUNDER pages.
	 *
	 * We also wake up bdflush, since bdflush should, under most
	 * loads, flush out the dirty pages before we have to wait on
	 * IO.
	 */
	if (can_get_io_locks && !launder_loop && free_shortage()) {
		launder_loop = 1;
		/* If we cleaned pages, never do synchronous IO. */
		if (cleaned_pages)
			sync = 0;
		/* We only do a few "out of order" flushes. */
		maxlaunder = MAX_LAUNDER;
		/* Kflushd takes care of the rest. */
		wakeup_bdflush(0);
		goto dirty_page_rescan;
	}

	/* Return the number of pages moved to the inactive_clean list. */
	return cleaned_pages;
}

如果决定进行第二趟扫描,就转回到502行标号dirty_page_rescan处。注意这里把launder_loop设成1,以后就不可能再回过去又扫描一次了。所以每次调用page_launder最多是作两趟扫描。

回到do_try_to_free_pages的代码中,经过page_launder以后,如果可分配的物理页面数量仍然不足,那就要进一步设法回收页面了。不过,也并不是单纯地从各个进程的用户空间所映射的物理页面中回收,而是从四个方面回收,这就是这里所调用三个函数(shrink_dcache_memory、shrink_icache_memory、refill_inactive),以及等一下将会看到的kmem_cache_reap的意图。在文件系统中,我们看到,在打开文件的过程中要分配和使用代表着目录项的dentry数据结构,还有代表着文件索引节点的inode数据结构。这些数据结构在文件关闭以后并不立即释放,而是放在LRU队列中作为后备,以防在不久将来的文件操作中又要用到。这样,经过一段时间以后,就有可能积累起大量的dentry数据结构和inode数据结构,占用数量可观的物理页面。这时,就要通过shrink_dcache_memory和shrink_icache_memory适当加以回收,以维持这些数据结构与物理页面间的生态平衡。另一方面,除此以外,内核在运行中与需要动态地分配使用很多数据结构,内核中对此采用了一种称为slab的管理机制。后面我们会看到,这种机制就好像是向存储管理批发物理页面,然后切割成小块零售。随着系统的运行,对这种物理页面的实际需求也在动态变化。但是slab管理机制也是倾向于分配和保持更多的空闲物理页面,而不热衷于退还这些页面,所以过一段时间就要通过kmem_cache_reap来收割。我们在文件系统中看到了这个操作,我们在这里则集中关注refill_inactive,其代码如下:

kswapd=>do_try_to_free_pages=>refill_inactive


/*
 * We need to make the locks finer granularity, but right
 * now we need this so that we can do page allocations
 * without holding the kernel lock etc.
 *
 * We want to try to free "count" pages, and we want to 
 * cluster them so that we get good swap-out behaviour.
 *
 * OTOH, if we're a user process (and not kswapd), we
 * really care about latency. In that case we don't try
 * to free too many pages.
 */
static int refill_inactive(unsigned int gfp_mask, int user)
{
	int priority, count, start_count, made_progress;

	count = inactive_shortage() + free_shortage();
	if (user)
		count = (1 << page_cluster);
	start_count = count;

	/* Always trim SLAB caches when memory gets low. */
	kmem_cache_reap(gfp_mask);

	priority = 6;
	do {
		made_progress = 0;

		if (current->need_resched) {
			__set_current_state(TASK_RUNNING);
			schedule();
		}

		while (refill_inactive_scan(priority, 1)) {
			made_progress = 1;
			if (--count <= 0)
				goto done;
		}

		/*
		 * don't be too light against the d/i cache since
	   	 * refill_inactive() almost never fail when there's
	   	 * really plenty of memory free. 
		 */
		shrink_dcache_memory(priority, gfp_mask);
		shrink_icache_memory(priority, gfp_mask);

		/*
		 * Then, try to page stuff out..
		 */
		while (swap_out(priority, gfp_mask)) {
			made_progress = 1;
			if (--count <= 0)
				goto done;
		}

		/*
		 * If we either have enough free memory, or if
		 * page_launder() will be able to make enough
		 * free memory, then stop.
		 */
		if (!inactive_shortage() || !free_shortage())
			goto done;

		/*
		 * Only switch to a lower "priority" if we
		 * didn't make any useful progress in the
		 * last loop.
		 */
		if (!made_progress)
			priority--;
	} while (priority >= 0);

	/* Always end on a refill_inactive.., may sleep... */
	while (refill_inactive_scan(0, 1)) {
		if (--count <= 0)
			goto done;
	}

done:
	return (count < start_count);
}

参数user是从kswapd传下来的,表示是否有函数在kswapd_done队列中等待执行,这个因素决定回收物理页面的过程是否可以慢慢来,所以对本次要回收的页面数量有影响。

首先通过kmem_cache_reap收割由slab机制管理的空闲物理页面,相对而言这是动作最小的,我们在学习了“内核工作缓冲区的管理”博客以后自己阅读这个函数的代码。

然后,就是一个do-while循环。循环从优先级最低的6级开始,逐步加大力度直到0级,结果或者达到了目标,回收的数量够了;或者在最高优先级时还是达不到目标,那也只好算了(到缺页异常真的发生时的情况也许有了改变)。

在循环中,每次开头都要检查一下当前进程的task_struct结构中的need_resched是否为1。如果是,就说明某个中断服务程序要求调度,所以调用schedule让内核进行一次调度,但是在此之前本进程的状态设置成TASK_RUNNING,表达要继续运行的意思。我们在后面的进程相关的博客中会看到,task_struct结构中的need_resched是强制调度而设置的,每当CPU结束了一次系统调用或中断服务、从系统空间回到用户空间时就会检查这个标志。可是,kswapd是个内核线程,永远不会返回用户空间,这样就可能绕过这个机制而占住CPU不放,所以只能靠它自律,自己在可能需要较长时间的操作之前检查这个标志并调用schedule。

那么,在循环中做些什么呢?主要是两件事,一件是通过refill_inactive_scan扫描活跃页面队列,试图从中找到可以转入不活跃状态的页面;另一件是通过swap_out找出一个进程,然后扫描其映射表,从中找出可以转入不活跃状态的页面。此外,还要再试试用于dentry结构和inode结构的页面。先看refill_inactive_scan的代码:

kswapd=>do_try_to_free_pages=>refill_inactive=>refill_inactive_scan


/**
 * refill_inactive_scan - scan the active list and find pages to deactivate
 * @priority: the priority at which to scan
 * @oneshot: exit after deactivating one page
 *
 * This function will scan a portion of the active list to find
 * unused pages, those pages will then be moved to the inactive list.
 */
int refill_inactive_scan(unsigned int priority, int oneshot)
{
	struct list_head * page_lru;
	struct page * page;
	int maxscan, page_active = 0;
	int ret = 0;

	/* Take the lock while messing with the list... */
	spin_lock(&pagemap_lru_lock);
	maxscan = nr_active_pages >> priority;
	while (maxscan-- > 0 && (page_lru = active_list.prev) != &active_list) {
		page = list_entry(page_lru, struct page, lru);

		/* Wrong page on list?! (list corruption, should not happen) */
		if (!PageActive(page)) {
			printk("VM: refill_inactive, wrong page on list.\n");
			list_del(page_lru);
			nr_active_pages--;
			continue;
		}

		/* Do aging on the pages. */
		if (PageTestandClearReferenced(page)) {
			age_page_up_nolock(page);
			page_active = 1;
		} else {
			age_page_down_ageonly(page);
			/*
			 * Since we don't hold a reference on the page
			 * ourselves, we have to do our test a bit more
			 * strict then deactivate_page(). This is needed
			 * since otherwise the system could hang shuffling
			 * unfreeable pages from the active list to the
			 * inactive_dirty list and back again...
			 *
			 * SUBTLE: we can have buffer pages with count 1.
			 */
			if (page->age == 0 && page_count(page) <=
						(page->buffers ? 2 : 1)) {
				deactivate_page_nolock(page);
				page_active = 0;
			} else {
				page_active = 1;
			}
		}
		/*
		 * If the page is still on the active list, move it
		 * to the other end of the list. Otherwise it was
		 * deactivated by age_page_down and we exit successfully.
		 */
		if (page_active || PageActive(page)) {
			list_del(page_lru);
			list_add(page_lru, &active_list);
		} else {
			ret = 1;
			if (oneshot)
				break;
		}
	}
	spin_unlock(&pagemap_lru_lock);

	return ret;
}

就像对脏页面队列的扫描一样,这里也通过一个局部变量maxscan来控制扫描的页面数量。不过这里扫描的不一定是整个活跃页面队列,而是根据调用参数priority的值扫描其中一部分,只有在priority为0时才扫描整个队列(见716行)。对于所扫描的页面,首先也要验证确实属于活跃页面(见721行)。然后,根据页面是否受到了访问(见729行),决定增加或减少页面的寿命。如果减少页面寿命以后到达了0,那就说明这个页面已经很长时间没有受到访问,因而已经耗尽了寿命。不过,光是耗尽了寿命还不足以把页面从活跃状态转入不活跃状态,还得看是否还有用户空间映射。如果页面并不用作文件系统的读写缓冲,那么只要页面的使用计数大于1就说明还有用户空间映射,还不能转入不活跃状态(744行),这样的页面在通过swap_out扫描相应进程的映射表时才能转入不活跃状态。对于还不能转入不活跃状态的页面,要将其从队列中的当前位置转移到队列尾部。反之,如果成功地将一个页面转入不活跃状态,则根据参数oneshot的值决定是否继续扫描。一般来说,在活跃页面队列中的页面使用计数都大于1。而当swap_out断开一个页面的映射而使其转入不活跃状态时,则已经将页面转入不活跃页面队列,因而不在这个队列中了。可是,就如代码中的注释所言,确实存在着特殊的情况,在页面的换入博客中就可以看到。

再看swap_out:

kswapd=>do_try_to_free_pages=>refill_inactive=>swap_out


static int swap_out(unsigned int priority, int gfp_mask)
{
	int counter;
	int __ret = 0;

	/* 
	 * We make one or two passes through the task list, indexed by 
	 * assign = {0, 1}:
	 *   Pass 1: select the swappable task with maximal RSS that has
	 *         not yet been swapped out. 
	 *   Pass 2: re-assign rss swap_cnt values, then select as above.
	 *
	 * With this approach, there's no need to remember the last task
	 * swapped out.  If the swap-out fails, we clear swap_cnt so the 
	 * task won't be selected again until all others have been tried.
	 *
	 * Think of swap_cnt as a "shadow rss" - it tells us which process
	 * we want to page out (always try largest first).
	 */
	counter = (nr_threads << SWAP_SHIFT) >> priority;
	if (counter < 1)
		counter = 1;

	for (; counter >= 0; counter--) {
		struct list_head *p;
		unsigned long max_cnt = 0;
		struct mm_struct *best = NULL;
		int assign = 0;
		int found_task = 0;
	select:
		spin_lock(&mmlist_lock);
		p = init_mm.mmlist.next;
		for (; p != &init_mm.mmlist; p = p->next) {
			struct mm_struct *mm = list_entry(p, struct mm_struct, mmlist);
	 		if (mm->rss <= 0)
				continue;
			found_task++;
			/* Refresh swap_cnt? */
			if (assign == 1) {
				mm->swap_cnt = (mm->rss >> SWAP_SHIFT);
				if (mm->swap_cnt < SWAP_MIN)
					mm->swap_cnt = SWAP_MIN;
			}
			if (mm->swap_cnt > max_cnt) {
				max_cnt = mm->swap_cnt;
				best = mm;
			}
		}

		/* Make sure it doesn't disappear */
		if (best)
			atomic_inc(&best->mm_users);
		spin_unlock(&mmlist_lock);

		/*
		 * We have dropped the tasklist_lock, but we
		 * know that "mm" still exists: we are running
		 * with the big kernel lock, and exit_mm()
		 * cannot race with us.
		 */
		if (!best) {
			if (!assign && found_task > 0) {
				assign = 1;
				goto select;
			}
			break;
		} else {
			__ret = swap_out_mm(best, gfp_mask);
			mmput(best);
			break;
		}
	}
	return __ret;
}

这个函数的主体是一个for循环,循环的次数取决于counter,而counter又是根据内核中进程(包括线程)的个数和调用swap_out时优先级(最初为6级,逐次上升为0级)计算而得的。当优先级为0时,counter就等于(nr_threads << SWAP_SHIFT),即32*nr_threads,这里nr_threads为当前系统中进程的数量。这个数值决定了把页面换出去的决心有多大,即代码中外层循环的次数。参数gfp_mask中是一些控制信息。

在每次循环中,程序试图从所有的进程中找到一个最合适的进程best。找到了就扫描这个进程的页面映射表,将符合条件的页面暂时断开对内存页面的映射,或进一步将页面转入不活跃状态,为把这些页面换出到交换设备上做好准备。

这里还应指出,这个函数虽然叫swap_out,但实际上只是为把一些页面换出到交换设备上做好准备,而并不一定是物理意义上的页面换出,所以在下面的叙述中所谓换出是广义的。那么,根据什么准则来找最合适的进程呢?可以说是劫富济贫,与轮流坐庄相结合。每个进程都有其自身的虚存空间,空间中已经分配并建立了映射的页面构成一个集合。但是在任何一个给定的时刻,该集合中的每一个页面所对应的物理页面不一定都在内存中,在内存中的往往只是一个子集。这个子集称为驻内页面集合(resident set),其大小称为rss。在存储管理结构mm_struct中有一个成分就是rss。以前在讲到这个结构时把rss跳过了,因为说来话长,而现在到了结合情景和源代码加以说明的时候了。

代码中的内层for循环表示从第二个进程开始扫描所有的进程。内核中所有的task_struct结构都以双向链表链接成一个队列。而进程init_task时内核中的第一个进程,是所有其他进程的祖宗。只要内核还在运行,这个进程就永远不落。所以,从init_task.next_task始至init_task止,就是扫描除第一个进程外的所有进程。扫描的目的是从中找出mm->swap_cnt为最大的进程。每个mm_struct结构中的这个数值,是在把所有进程的页面资源时都处理了一遍,从而每个mm_struct结构中的这个数值都变成了0的时候设置好了的,反映了当时该进程占用内存页面的数量mm->rss。这就好像一次人口普查。mm->rss反映了一个进程占用的内存页面数量,而mm->swap_cnt减1,直至最后变成0。所以,mm->rss反映了一个进程占用的内存页面数量,而mm->swap_cnt反映了该进程在一轮换出内存页面的努力中尚未受到考察的页面数量。只要在这一轮中至少还有一个进程的页面尚未受到考察,就一定能找到一个最佳对象。一直到所有进程的mm->swap_cnt都变成了0,从而扫描下来竟找不到一个best时(439-444行),再把这里的局部变量assign置成1,再扫描一遍。这一次将每个进程当前的mm->rss拷贝到mm->swap_cnt中,然后再从最富有的进程开始。但是,所谓尚未受到考察的页面的数量要到下一次人口普查以后才会反映出来。就每个进程的角度而言,对内存页面的占用存在着两个方向上的运动:一个方向是因页面异常而有更多的页面建立或恢复起映射;另一个方面则是周期性地受到swap_out的考察而被切断若干页面的映射。这两个运动的结合决定了一个进程在特定时间内对内存页面的占用。

找到一个最佳对象best以后,就要依次考察该进程的映射表,将符合条件的页面换出去。

页面的换出是由swap_out_mm来完成的。当swap_out_mm成功地换出一个页面时返回1,否则返回0,返回负数则为异常。在操作之前先通过356行的atomic_inc递增mm_struct结构中的使用计数mm_users,待完成以后再由373行的mmput将其还原,使这个数据结构在操作的期间多了一个用户,从而不会在中途被释放。

函数swap_out_mm的代码如下:

kswapd=>do_try_to_free_pages=>refill_inactive=>swap_out=>swap_out_mm


static int swap_out_mm(struct mm_struct * mm, int gfp_mask)
{
	int result = 0;
	unsigned long address;
	struct vm_area_struct* vma;

	/*
	 * Go through process' page directory.
	 */

	/*
	 * Find the proper vm-area after freezing the vma chain 
	 * and ptes.
	 */
	spin_lock(&mm->page_table_lock);
	address = mm->swap_address;
	vma = find_vma(mm, address);
	if (vma) {
		if (address < vma->vm_start)
			address = vma->vm_start;

		for (;;) {
			result = swap_out_vma(mm, vma, address, gfp_mask);
			if (result)
				goto out_unlock;
			vma = vma->vm_next;
			if (!vma)
				break;
			address = vma->vm_start;
		}
	}
	/* Reset to 0 when we reach the end of address space */
	mm->swap_address = 0;
	mm->swap_cnt = 0;

out_unlock:
	spin_unlock(&mm->page_table_lock);
	return result;
}

首先,mm->swap_address表示在执行的过程中要接着考察的页面地址。最初时该地址为0,到所有的页面都已经考察了一遍的时候就又清成0(见289行)。程序在一个for循环中根据当前的这个地址找到其所在的虚存区间vma,然后就调用swap_out_mm试图换出一个页面。如果成功(返回1),这一次任务就完成了。否则就试下一个虚存区间,就这样一层一层地往下调用,经过swap_out_vma、swap_out_pgd、swap_out_pmd,一直到try_to_swap_out,试图换出一个页面表项pte所指向的内存页面。中间这几个函数都在同一个文件中,读者可以自行阅读。这里我们直接来看try_to_swap_out,因为这是关键所在。下面,我们一步一步来看它的各个片段:

kswapd=>do_try_to_free_pages=>refill_inactive=>swap_out=>swap_out_mm=>swap_out_vma=>swap_out_pgd=> swap_out_pmd=>try_to_swap_out


/*
 * The swap-out functions return 1 if they successfully
 * threw something out, and we got a free page. It returns
 * zero if it couldn't do anything, and any other value
 * indicates it decreased rss, but the page was shared.
 *
 * NOTE! If it sleeps, it *must* return 1 to make sure we
 * don't continue with the swap-out. Otherwise we may be
 * using a process that no longer actually exists (it might
 * have died while we slept).
 */
static int try_to_swap_out(struct mm_struct * mm, struct vm_area_struct* vma, unsigned long address, pte_t * page_table, int gfp_mask)
{
	pte_t pte;
	swp_entry_t entry;
	struct page * page;
	int onlist;

	pte = *page_table;
	if (!pte_present(pte))
		goto out_failed;
	page = pte_page(pte);
	if ((!VALID_PAGE(page)) || PageReserved(page))
		goto out_failed;

	if (!mm->swap_cnt)
		return 1;

	mm->swap_cnt--;

首先要说明,参数page_table实际上指向一个页面表项,而不是页面表,参数名page_table有些误导。把这个表项的内容赋给变量pte以后,就通过pte_page来测试该表项所指的物理页面是否在内存中,如果不在内存中就转向out_failed,本次操作就失败了:

out_failed:
		return 0;
	}

当try_to_swap_out返回0时,其上一层的程序就会跳过这个页面,而试着换出同一个页面表中映射的下一个页面。如果一个页面表已经穷尽,就再往上退一层试下一个页面表。

反之,如果物理页面确在内存中,就通过pte_page将页面表项的内容换算成指向物理内存页面的page结构的指针。由于所有的page结构都在mem_map数组中,所以(page-mem_map)就是该页面的序号(数组中的下标)。要是这个序号大于最大的物理内存页面序号max_mapnr,那就不是一个有效的物理页面,这种情况是因为物理页面在外部设备(例如网络接口卡)上,所以也跳过这一项。

#define VALID_PAGE(page)	((page - mem_map) < max_mapnr)

此外,对于保留在内存中不允许换出的物理页面也要跳过。

跳过了这两种情况,就要具体地考察一个页面了,所以将mm->swap_cnt减1。继续往下看try_to_swap_out的代码:

kswapd=>do_try_to_free_pages=>refill_inactive=>swap_out=>swap_out_mm=>swap_out_vma=>swap_out_pgd=> swap_out_pmd=>try_to_swap_out

	onlist = PageActive(page);
	/* Don't look at this pte if it's been accessed recently. */
	if (ptep_test_and_clear_young(page_table)) {
		age_page_up(page);
		goto out_failed;
	}
	if (!onlist)
		/* The page is still mapped, so it can't be freeable... */
		age_page_down_ageonly(page);

	/*
	 * If the page is in active use by us, or if the page
	 * is in active use by others, don't unmap it or
	 * (worse) start unneeded IO.
	 */
	if (page->age > 0)
		goto out_failed;

内存页面的page结构中,字段flags中的各种标志位反映着页面的当前状态,其中的PG_active标志位表示当前这个页面是否活跃,即是否仍在active_list队列中:

#define PageActive(page)	test_bit(PG_active, &(page)->flags)

一个可交换的页面一定在某个LRU队列中,不在active_list队列中就说明一定在inactive_clean_list中,等一下就要使用测试的结果。

一个映射中的物理页面是否应该换出,取决于这个页面最近是否受到了访问。这是通过inline函数ptep_test_and_clear_young测试(并请0)的,其定义如下:
 

static inline  int ptep_test_and_clear_young(pte_t *ptep)	{ return test_and_clear_bit(_PAGE_BIT_ACCESSED, ptep); }

如前所述,页面表项中有个_PAGE_BIT_ACCESSED标志位。当i386 CPU的内存映射机制在通过一个页面表项将一个虚存映射一个物理地址,进而访问这个物理地址时,就会自动将该表项的_PAGE_BIT_ACCESSED标志位设成1。所以,如果ptep_test_and_clear_young返回1,就表示从上一次对同一个页面表项调用try_to_swap_out至今,该页面至少已经被访问过一次,所以说页面还年轻。一般而言,最近受到访问就预示着在不久的将来也会受到访问,所以不宜将其换出。取得了此项信息以后,就将页面表项中的_PAGE_BIT_ACCESSED标志位清成0,再把它写回页面表项,为下一次再来测试这个标志位做好准备。

如果页面还年轻,那就肯定不是要加以换出的对象,所以也要转到out_failed。要是页面不在活跃页面队列中,则通过age_page_up增加同页面可以留下来以观后效的时间,因为毕竟这个页面最近已受到过访问。

kswapd=>do_try_to_free_pages=>refill_inactive=>swap_out=>swap_out_mm=>swap_out_vma=>swap_out_pgd=> swap_out_pmd=>try_to_swap_out=>age_page_up


void age_page_up(struct page * page)
{
	/*
	 * We're dealing with an inactive page, move the page
	 * to the active list.
	 */
	if (!page->age)
		activate_page(page);

	/* The actual page aging bit */
	page->age += PAGE_AGE_ADV;
	if (page->age > PAGE_AGE_MAX)
		page->age = PAGE_AGE_MAX;
}

转到out_failed以后,就在那里返回0,让更高层的程序跳过这个页面。这样,到一下轮又轮到这个进程和这个页面时,如果同一个页面表项pte中的_PAGE_BIT_ACCESSED标志位仍然为0,那就表示不再年轻了。读者也许会问,既然这个页面是有映射的(否则不会出现在目标进程的映射表中并且在内存中),怎么又会不在活跃页面队列中呢?以后读者就会在do_swap_page中看到,当因页面异常而恢复一个不活跃页面的映射时,并不立即把它转入活跃页面队列,而把这项工作留给当前看到的page_launder,让其在系统比较空闲时再来处理,所以这样的页面有可能不在活跃队列中。

如果页面已不年轻,那就要进一步考察了。当然,也不能因为这个页面在过去一个周期中未受到访问就马上把它换出去,还要给它一个留职察看的机会。察看多久呢?那就是page->age的值,即页面的寿命。如果页面不在活跃队列中则还要先通过age_page_down_ageonly减少其寿命:

/*
 * We use this (minimal) function in the case where we
 * know we can't deactivate the page (yet).
 */
void age_page_down_ageonly(struct page * page)
{
	page->age /= 2;
}

只要page->age尚未达到0,就还不能将此页面换出,所以也要转到out_failed。

经过上面这些筛选,这个页面原则上已经是可以换出的对象了。我们继续往下看代码:

kswapd=>do_try_to_free_pages=>refill_inactive=>swap_out=>swap_out_mm=>swap_out_vma=>swap_out_pgd=> swap_out_pmd=>try_to_swap_out

	if (TryLockPage(page))
		goto out_failed;

	/* From this point on, the odds are that we're going to
	 * nuke this pte, so read and clear the pte.  This hook
	 * is needed on CPUs which update the accessed and dirty
	 * bits in hardware.
	 */
	pte = ptep_get_and_clear(page_table);
	flush_tlb_page(vma, address);

	/*
	 * Is the page already in the swap cache? If so, then
	 * we can just drop our reference to it without doing
	 * any IO - it's already up-to-date on disk.
	 *
	 * Return 0, as we didn't actually free any real
	 * memory, and we should just continue our scan.
	 */
	if (PageSwapCache(page)) {
		entry.val = page->index;
		if (pte_dirty(pte))
			set_page_dirty(page);
set_swap_pte:
		swap_duplicate(entry);
		set_pte(page_table, swp_entry_to_pte(entry));
drop_pte:
		UnlockPage(page);
		mm->rss--;
		deactivate_page(page);
		page_cache_release(page);
out_failed:
		return 0;
	}

下面对page数据结构的操作涉及需要互斥,或者说独占的条件下进行的操作,所以这里通过TryLockPage将page数据锁住:

#define TryLockPage(page)	test_and_set_bit(PG_locked, &(page)->flags)

如果返回为1,即表示PG_locked标志位原来已经是1,已经被别的进程先锁住了,此时就不能继续处理这个page数据结构,而又只好失败返回。

加锁成功以后,就可以根据页面的不同情况作换出的准备了。

首先通过ptep_get_and_clear再读一次页面表项的内容,并把表项的内容请成0,暂时撤销该页面的映射。前面在45行已经读了一次页面表项的内容,为什么现在还要再读一次,而不仅仅是把表项清0呢?在多处理器系统中,目标进程有可能正在另一个CPU上运行,所以其映射表项的内容有可能已经改变。

如果页面的page数据结构已经在为页面换入换出而设置的队列中,即数据结构swapper_space内的队列中,那么页面的内容已经在交换设备上,只要把映射暂时断开,表示目标进程已经同意释放这个页面,就可以了。不过,为页面换入换出而设置的队列也分成干净和脏两个,所以如果页面已经受过写访问就要通过set_page_dirty将其转入脏页面队列。宏操作PageSwapCache的定义如下:

#define PageSwapCache(page)	test_bit(PG_swap_cache, &(page)->flags)

标志位PG_swap_cache为1表示page结构在swapper_space队列中,也说明相应的页面是个普通的换入换出页面。此时page结构中的index字段是一个32位的索引项swp_entry_t,实际上是指向页面在交换设备上的映像的指针。函数swap_duplicate的作用,一者是要对索引项的内容做一些检查,二者要递增相应盘上页面的共享计数,其代码如下:

kswapd=>do_try_to_free_pages=>refill_inactive=>swap_out=>swap_out_mm=>swap_out_vma=>swap_out_pgd=> swap_out_pmd=>try_to_swap_out=>swap_duplicate


/*
 * Verify that a swap entry is valid and increment its swap map count.
 * Kernel_lock is held, which guarantees existance of swap device.
 *
 * Note: if swap_map[] reaches SWAP_MAP_MAX the entries are treated as
 * "permanent", but will be reclaimed by the next swapoff.
 */
int swap_duplicate(swp_entry_t entry)
{
	struct swap_info_struct * p;
	unsigned long offset, type;
	int result = 0;

	/* Swap entry 0 is illegal */
	if (!entry.val)
		goto out;
	type = SWP_TYPE(entry);
	if (type >= nr_swapfiles)
		goto bad_file;
	p = type + swap_info;
	offset = SWP_OFFSET(entry);
	if (offset >= p->max)
		goto bad_offset;
	if (!p->swap_map[offset])
		goto bad_unused;
	/*
	 * Entry is valid, so increment the map count.
	 */
	swap_device_lock(p);
	if (p->swap_map[offset] < SWAP_MAP_MAX)
		p->swap_map[offset]++;
	else {
		static int overflow = 0;
		if (overflow++ < 5)
			printk("VM: swap entry overflow\n");
		p->swap_map[offset] = SWAP_MAP_MAX;
	}
	swap_device_unlock(p);
	result = 1;
out:
	return result;

bad_file:
	printk("Bad swap file entry %08lx\n", entry.val);
	goto out;
bad_offset:
	printk("Bad swap offset entry %08lx\n", entry.val);
	goto out;
bad_unused:
	printk("Unused swap offset entry in swap_dup %08lx\n", entry.val);
	goto out;
}

以前讲过,数据结构swp_entry_t实际上是32无符号整数,其内容不可能全是0,但是最低位却一定是0,最高位(24位)位段offset为设备上的页面序号,其余(7位)位段type则其实交换设备本身的序号。以前还讲过,其中的位段type实际上与类型毫无关系,而是代表着交换设备的序号。以此为下标,就可在内核中的数组swap_info中找到相应交换设备的swap_info_struct数据结构。这个数据结构中的数组swap_map,则记录着交换设备上各个页面的共享计数。由于正在处理中的页面原来就已经在交换设备上,其计数显然不应为0,否则就错了;另一方面,递增以后不应达到SWAP_MAP_MAX。递增盘上页面的共享计数表示这个页面现在多了一个用户。

回到try_to_swap_out的代码中,100行调用set_pte,把这个指向盘上页面的索引项置入相应的页面表项,原先对内存页面的映射就变成了对盘上页面的映射。这样,当执行标号drop_pte的地方,目标进程的驻内存页面集合rss中就减少了一个页面。由于我们这个物理页面断开了一个映射,很可能已经满足了变成不活跃页面的条件,所以在调用deactivate_page时有条件地将其设置成不活跃状态,并将页面的page结构从活跃页面队列转移到某个不活跃页面队列:

kswapd=>do_try_to_free_pages=>refill_inactive=>swap_out=>swap_out_mm=>swap_out_vma=>swap_out_pgd=> swap_out_pmd=>try_to_swap_out=>deactivate_page

void deactivate_page(struct page * page)
{
	spin_lock(&pagemap_lru_lock);
	deactivate_page_nolock(page);
	spin_unlock(&pagemap_lru_lock);
}

kswapd=>do_try_to_free_pages=>refill_inactive=>swap_out=>swap_out_mm=>swap_out_vma=>swap_out_pgd=> swap_out_pmd=>try_to_swap_out=>deactivate_page=>deactivate_page_nolock



/**
 * (de)activate_page - move pages from/to active and inactive lists
 * @page: the page we want to move
 * @nolock - are we already holding the pagemap_lru_lock?
 *
 * Deactivate_page will move an active page to the right
 * inactive list, while activate_page will move a page back
 * from one of the inactive lists to the active list. If
 * called on a page which is not on any of the lists, the
 * page is left alone.
 */
void deactivate_page_nolock(struct page * page)
{
	/*
	 * One for the cache, one for the extra reference the
	 * caller has and (maybe) one for the buffers.
	 *
	 * This isn't perfect, but works for just about everything.
	 * Besides, as long as we don't move unfreeable pages to the
	 * inactive_clean list it doesn't need to be perfect...
	 */
	int maxcount = (page->buffers ? 3 : 2);
	page->age = 0;
	ClearPageReferenced(page);

	/*
	 * Don't touch it if it's not on the active list.
	 * (some pages aren't on any list at all)
	 */
	if (PageActive(page) && page_count(page) <= maxcount && !page_ramdisk(page)) {
		del_page_from_active_list(page);
		add_page_to_inactive_dirty_list(page);
	}
}	

在物理页面的page结构中有个计数器count,空闲页面的这个计数为0,在分配页面时将其设为1,(见__alloc_pages和rmqueue的代码),此后每当页面增加一个用户,如建立或恢复一个映射时,就使count加1。这样,如果这个计数器的值为2,就说明刚断开的映射已经是该物理页面的最后一个映射。既然最后的映射已经断开,这页面当然是不活跃的了。所以把小于等于2作为一个判断的准则,就是这里的maxcount。但是,这里还要考虑一种特殊情况,就是当整个页面是通过mmap映射到普通文件,而这个文件又已经被打开,按常规的文件操作访问,因此这个页面又同时用作读写文件的缓冲。此时页面划分成若干缓冲区,其page结构中的指针buffers指向一个buffer_head数据结构队列,而这个队列则成了该页面的另一个用户。所以,当page->buffers非0时,maxcount为3说明刚断开的映射是页面的最后一个映射。此外,内存页面也有可能用作ramdisk,即以一部分内存物理空间来模拟硬盘,这样的页面永远不会变成不活跃。这样,判断的准则一共有三条,只有在满足了这三条准则时才真的可以将页面转入不活跃队列。多数由用户空间的内存页面都只有一个映射,此时就转入了不活跃状态。同时,从代码中也可看到,对不活跃队列中的页面再调用一次deactivate_page_nolock并无害处。

将一个活跃的页面变成不活跃时,要把该页面的page结构从活跃页面的LRU队列active_list中转移到一个不活跃队列中去。可是,系统中有两种不活跃页面队列。一种是dirty,即可能最近已被写过,因而跟交换设备上的页面不一致的脏页面队列,这样的页面不能马上就拿来分配,因为还需要把 它写出去才能把它洗净。另一种clean,即肯定跟交换设备上的页面一致的干净页面队列,这样的页面原则上已可作为空闲页面分配,只是因为页面中的内容还可能有用,因而再予保存一段时间。不活跃脏页面队列只有一个,那就是inactive_dirty_list;而不活跃干净页面队列则有很多,每个页面管理区中都有个inactive_clean_list队列。那么,当一个原来活跃的页面变成不活跃时,应该把它转移到哪一个队列中去呢?第一步总是把它转入脏页面队列。将一个page结构从活跃队列脱链是由宏操作del_page_from_active_list完成的,其定义如下:


#define del_page_from_active_list(page) { \
	list_del(&(page)->lru); \
	ClearPageActive(page); \
	nr_active_pages--; \
	DEBUG_ADD_PAGE \
	ZERO_PAGE_BUG \
}

将一个page结构转入不活跃队列,则由add_page_to_inactive_dirty_list完成:


#define add_page_to_inactive_dirty_list(page) { \
	DEBUG_ADD_PAGE \
	ZERO_PAGE_BUG \
	SetPageInactiveDirty(page); \
	list_add(&(page)->lru, &inactive_dirty_list); \
	nr_inactive_dirty_pages++; \
	page->zone->inactive_dirty_pages++; \
}

这里的ClearPageActive和SetPageInactiveDirty分别将page结构中的PG_active标志位清成0和将PG_inactive_dirty标志位设成1。注意这个过程中page结构中的使用计数并未改变。

又回到try_to_swap_out的代码中,既然断开了对一个内存页面的映射,就要递减对这个页面的使用计数,这是由宏操作page_cache_release、实际上是由__free_page完成的:

#define page_cache_release(x)	__free_page(x)

#define __free_page(page) __free_pages((page), 0)

void __free_pages(struct page *page, unsigned long order)
{
	if (!PageReserved(page) && put_page_testzero(page))
		__free_pages_ok(page, order);
}


#define put_page_testzero(p) 	atomic_dec_and_test(&(p)->count)

这个函数通过put_page_testzero,将page结构中count的值减1,然后测试是否达到了0,如果达到了0就通过__free_pages_ok将该页面释放。在这里,由于页面还在不活跃页面队列中尚未释放,至少还有这么一个引用,所以不会达到0。

至此,对这个页面的处理就完成了,于是又到了标号out_failed处而返回0。为什么又是到达out_failed处呢?其实,try_to_swap_out仅在一种情况下才返回1,那就是当mm->swap_cnt达到了0的时候(见52行)。正是这样,才使swap_out_mm能够依次考察和处理一个进程的所在页面。

要是页面的page结构不在swapper_space的队列中呢?这说明尚未为该页面在交换设备上建立起映像,或者页面来自一个文件。读者可以回顾一下,在因页面无映射而发生缺页异常时,具体的处理取决于页面所在的区间是否提供了一个vm_operations_struct数据结构,并且通过这个数据结构中的函数指针nopage提供了特定的操作。如果提供了nopage操作,就说明该区间的页面来自一个文件(而不是交换设备),此时根据虚存地址可以计算出在文件中的页面位置。否则就是普通的页面,但尚未建立相应的盘上页面(因为页面表项为0),此时先把它映射到空白页面,以后需要写的时候才为之另行分配一个页面。我们继续往下看try_to_swap_out的代码。下面一段就是对此种页面的处理:

kswapd=>do_try_to_free_pages=>refill_inactive=>swap_out=>swap_out_mm=>swap_out_vma=>swap_out_pgd=> swap_out_pmd=>try_to_swap_out

	/*
	 * Is it a clean page? Then it must be recoverable
	 * by just paging it in again, and we can just drop
	 * it..
	 *
	 * However, this won't actually free any real
	 * memory, as the page will just be in the page cache
	 * somewhere, and as such we should just continue
	 * our scan.
	 *
	 * Basically, this just makes it possible for us to do
	 * some real work in the future in "refill_inactive()".
	 */
	flush_cache_page(vma, address);
	if (!pte_dirty(pte))
		goto drop_pte;

	/*
	 * Ok, it's really dirty. That means that
	 * we should either create a new swap cache
	 * entry for it, or we should write it back
	 * to its own backing store.
	 */
	if (page->mapping) {
		set_page_dirty(page);
		goto drop_pte;
	}

	/*
	 * This is a dirty, swappable page.  First of all,
	 * get a suitable swap entry for it, and make sure
	 * we have the swap cache set up to associate the
	 * page with that swap entry.
	 */
	entry = get_swap_page();
	if (!entry.val)
		goto out_unlock_restore; /* No swap space left */

	/* Add it to the swap cache and mark it dirty */
	add_to_swap_cache(page, entry);
	set_page_dirty(page);
	goto set_swap_pte;

out_unlock_restore:
	set_pte(page_table, pte);
	UnlockPage(page);
	return 0;
}

这里的pte_dirty是一个inline函数,定义如下:

static inline int pte_dirty(pte_t pte)		{ return (pte).pte_low & _PAGE_DIRTY; }

在页面表项中有一个D标志位(_PAGE_DIRTY),如果CPU对表项所指的内存页面进行了写操作,就自动把标志位设成1,表示该内存页面已经脏了。如果此标志位为0,就表示相应的内存页面尚未被改写。对这样的页面,如果很久没有受到写访问,就可以把映射解除(而不是暂时断开)。这是因为:如果页面的内容是空白,那么以后需要时可以再来建立映射;或者,如果页面来自通过mmap建立起的文件映射,则在需要时可以根据虚拟地址计算出页面在文件中的位置(相比之下,交换设备上的页面位置不能通过计算得到,所以必须把页面的去向存储在页面表项中)。所以,这里转到前面的标号drop_pte处。注意在这种情况下前面的deactivate_page实际上不起作用,特别是页面表项已在前面83行清0,而page_cache_release则只是递减对空白页面的引用计数。

如果所考察的页面是来自通过mmap建立起的文件映射,则其page结构中的指针mapping指向相应的address_space数据结构。对于这样的页面,如果决定解除映射,而页面表项中的_PAGE_DIRTY标志位为1,就要在转到drop_pte处之前,先把page结构中的PG_dirty标志位设成1,并把页面转移到该文件映射的脏页面队列中。有关的操作set_page_dirty定义如下:

kswapd=>do_try_to_free_pages=>refill_inactive=>swap_out=>swap_out_mm=>swap_out_vma=>swap_out_pgd=> swap_out_pmd=>try_to_swap_out=>set_page_dirty

static inline void set_page_dirty(struct page * page)
{
	if (!test_and_set_bit(PG_dirty, &page->flags))
		__set_page_dirty(page);
}


/*
 * Add a page to the dirty page list.
 */
void __set_page_dirty(struct page *page)
{
	struct address_space *mapping = page->mapping;

	spin_lock(&pagecache_lock);
	list_del(&page->list);
	list_add(&page->list, &mapping->dirty_pages);
	spin_unlock(&pagecache_lock);

	mark_inode_dirty_pages(mapping->host);
}

再往下看try_to_swap_out的代码。当程序执行到这里时,所考察的页面必然是个很久没有受到访问,又不在swapper_space的换入换出队列中,也不属于文件映射,但却是个受到过写访问的脏页面。对于这样的页面必须要为之分配一个盘上页面,并将其内容写到盘上页面中去。首先通过get_swap_page分配一个盘上页面,这是个宏操作:

#define get_swap_page() __get_swap_page(1)

就是说,通过__get_swap_page(1)从交换设备上分配一个页面。其代码由于比较简单,我们把它留给读者。盘上页面的使用计数在分配时设成1,以后每当有进程参与共享同一内存页面时就通过swap_duplicate递增,此外在有进程断开对此页面的映射时也要递增(见99行);反之则通过swap_free递减。如果分配盘上页面失败,就转到out_unlock_restore处恢复原有的映射。

分配了盘上页面以后,就通过add_to_swap_cache将页面链入swapper_space的队列中,以及活跃页面队列中,这个函数的代码以前已经看过了。然后,再通过set_page_dirty将页面转到不活跃脏页面队列中。至于实际的写出,则前面已经看到是page_launder的事情了。

至此,对一个进程的用户空间页面的扫描处理就完成了。swap_out是在一个for循环中调用swap_out_mm的,所以每次调用swap_out都会换出若干进程的若干页面,而refill_inactive又是在嵌套的while循环中调用swap_out的,一直要到系统可供分配的页面,包括潜在可供分配的页面在内不再短缺为止。到那时,do_try_to_free_pages就结束了。回到kswapd的代码中,此时活跃页面队列的情况可能已经有了较大的改变,所以还要再调用一次refill_inactive_scan。这样,kswapd的一次队列例行路线就基本走完了。如前所述,kswapd除定期的执行外,也有可能是被其他进程唤醒的,所以可能有进程正在睡眠中等待其完成,因此通过wake_up_all唤醒这些进程。

读者也许在想,通过swap_out_mm对每个进程页面表的扫描并不保证一定能有页面转入不活跃状态,这样refill_inactive岂不是要无穷无尽地循环下去?事实上,一来程序中对循环的次数有个限制,二来对页面表的扫描是个自适应的过程。如果在对所有进程的一轮扫描后转入不活跃状态页面的数量不足,那么refill_inactive就会又回过头来开始第二轮扫描。而扫描次数的增加会使页面老化的速度也增加,因为页面的寿命实际上是以扫描的次数为单位的。这样,在第一轮扫描中不符合条件的页面在第二轮扫描中就可能符合条件了。最后,在很特殊的情况下,可能最终还是达不到要求,此时就调用oom_kill从系统中杀掉一个进程,通过牺牲局部来保障全局。

最后,再来看看线程kreclaimd的代码,这是在mm/vmscan.c代码如下:

/*
 * Kreclaimd will move pages from the inactive_clean list to the
 * free list, in order to keep atomic allocations possible under
 * all circumstances. Even when kswapd is blocked on IO.
 */
int kreclaimd(void *unused)
{
	struct task_struct *tsk = current;
	pg_data_t *pgdat;

	tsk->session = 1;
	tsk->pgrp = 1;
	strcpy(tsk->comm, "kreclaimd");
	sigfillset(&tsk->blocked);
	current->flags |= PF_MEMALLOC;

	while (1) {

		/*
		 * We sleep until someone wakes us up from
		 * page_alloc.c::__alloc_pages().
		 */
		interruptible_sleep_on(&kreclaimd_wait);

		/*
		 * Move some pages from the inactive_clean lists to
		 * the free lists, if it is needed.
		 */
		pgdat = pgdat_list;
		do {
			int i;
			for(i = 0; i < MAX_NR_ZONES; i++) {
				zone_t *zone = pgdat->node_zones + i;
				if (!zone->size)
					continue;

				while (zone->free_pages < zone->pages_low) {
					struct page * page;
					page = reclaim_page(zone);
					if (!page)
						break;
					__free_page(page);
				}
			}
			pgdat = pgdat->node_next;
		} while (pgdat);
	}
}

对照一下kswapd的代码,就可以看出二者的初始化部分是一样的,程序的结构也相似。注意二者都把其task_struct结构中flags字段的PF_MEMALLOC标志位设成1,表示这两个内核线程都是页面管理机制的维护者。事实上,在以前的版本中只有一个线程kswapd,在2.4版本中才把其中一部分独立出来成为一个线程。不过,这一次是通过reclaim_page扫描各个页面管理区中的不活跃干净页面队列,从中回收页面加以释放。这个函数的代码在mm/vmscan.c中,我们把它留给读者自己阅读。在阅读了上面这些代码以后,读者已经不至于感到困难了。

kreclaimd=>reclaim_page



/**
 * reclaim_page -	reclaims one page from the inactive_clean list
 * @zone: reclaim a page from this zone
 *
 * The pages on the inactive_clean can be instantly reclaimed.
 * The tests look impressive, but most of the time we'll grab
 * the first page of the list and exit successfully.
 */
struct page * reclaim_page(zone_t * zone)
{
	struct page * page = NULL;
	struct list_head * page_lru;
	int maxscan;

	/*
	 * We only need the pagemap_lru_lock if we don't reclaim the page,
	 * but we have to grab the pagecache_lock before the pagemap_lru_lock
	 * to avoid deadlocks and most of the time we'll succeed anyway.
	 */
	spin_lock(&pagecache_lock);
	spin_lock(&pagemap_lru_lock);
	maxscan = zone->inactive_clean_pages;
	while ((page_lru = zone->inactive_clean_list.prev) !=
			&zone->inactive_clean_list && maxscan--) {
		page = list_entry(page_lru, struct page, lru);

		/* Wrong page on list?! (list corruption, should not happen) */
		if (!PageInactiveClean(page)) {
			printk("VM: reclaim_page, wrong page on list.\n");
			list_del(page_lru);
			page->zone->inactive_clean_pages--;
			continue;
		}

		/* Page is or was in use?  Move it to the active list. */
		if (PageTestandClearReferenced(page) || page->age > 0 ||
				(!page->buffers && page_count(page) > 1)) {
			del_page_from_inactive_clean_list(page);
			add_page_to_active_list(page);
			continue;
		}

		/* The page is dirty, or locked, move to inactive_dirty list. */
		if (page->buffers || PageDirty(page) || TryLockPage(page)) {
			del_page_from_inactive_clean_list(page);
			add_page_to_inactive_dirty_list(page);
			continue;
		}

		/* OK, remove the page from the caches. */
                if (PageSwapCache(page)) {
			__delete_from_swap_cache(page);
			goto found_page;
		}

		if (page->mapping) {
			__remove_inode_page(page);
			goto found_page;
		}

		/* We should never ever get here. */
		printk(KERN_ERR "VM: reclaim_page, found unknown page\n");
		list_del(page_lru);
		zone->inactive_clean_pages--;
		UnlockPage(page);
	}
	/* Reset page pointer, maybe we encountered an unfreeable page. */
	page = NULL;
	goto out;

found_page:
	del_page_from_inactive_clean_list(page);
	UnlockPage(page);
	page->age = PAGE_AGE_START;
	if (page_count(page) != 1)
		printk("VM: reclaim_page, found page with count %d!\n",
				page_count(page));
out:
	spin_unlock(&pagemap_lru_lock);
	spin_unlock(&pagecache_lock);
	memory_pressure++;
	return page;
}

参与评论 您还未登录,请先 登录 后发表或查看评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页

打赏作者

guoguangwu

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值