linux内存管理-物理页面的使用与周转

除CPU之外,对于像Linux这样的现代操作系统来说,物理存储页面可以说是最基本、最重要的资源了。物理存储页面在系统中的使用和周转就好像资金在企业中的使用和周转一样重要。因此,我们对此最好能有更多一些的了解。

首先要澄清本系列博客中使用的几个术语。“虚存页面”,是指在虚拟地址空间中一个固定大小,边界与页面大小(4KB)对齐的区间及其内容。虚存页面最终要落实到,或者说要映射到某种物理存储介质上,那就是物理页面。根据具体介质的不同,一个物理页面可以在内存中,也可以在磁盘上。为了区分这两种情况,本博客将分别称之为(物理)内存页面和盘上(物理)页面。此外,在某项外部设备上,例如在网络接口卡上,用来存储一个页面内容的那部分介质,也称为一个物理页面。所以,当我们在谈及物理内存页面的分配和释放的时候,指的仅是物理介质,而在谈及页面的换入换出时则指的是其内容。读者,特别是非计算机专业的读者,一定要清楚并记住这一点。

如前所述,每个进程的虚存空间是很大的(用户空间为3GB)。不过,每个进程实际上使用的空间则要小得多,一般不会超过几M。特别的,传统的linux(以及Unix)可执行程序通常都是比较小的,例如几十KB或一二百KB。可是,当系统中有几百个、上千个进程同时存在的时候,对存储空间的需求总量就很大了。在这样的情况下,要为系统配备足够的内存就很难。所以,在计算机技术的发展历史上很早就有了把内存的内容与一个专用的磁盘空间交换技术,即把暂时不用的信息(内容)存放到磁盘上,为其他急用的信息腾出空间,到需要时再从磁盘上读进来的技术。早期的盘区交换技术是建立在段式存储管理的基础上的,当一个进程暂时不运行的时候就可以把它(代码段和数据段等)交换出去(把其他进程换进来,故曰交换),到调度这个进程运行时再交换回来。显然,这样的盘区交换是很粗糙的,对系统性能的影响也比较大,所以后来发展起了建立在页式存储管理基础上的按需页面交换技术。

在计算机技术中,时间和空间时一对矛盾,常常需要在二者之间折中权衡,有时候是以空间换时间,有时候是以时间换空间。而页面的交换,则是典型的以时间换空间。必须指出,这只是不得已而为之。特别是在有实时要求的系统中。是不宜采用页面交换的,因为它使程序的执行在时间上有了较大的不确定性。因此,linux提供了用来开启和关闭页面交换机制的系统调用,不过我们在本系列博客的叙述中假定它是打开的。

在介绍页面周转的策略之前,先要对物理页面、特别是磁盘页面的抽象描述作一个简要说明。

前面已经简略的介绍过,为了方便(物理)内存页面的管理,每一个内存页面都对应一个page数据结构。每一个物理内存页面之有page数据结构(以及每个进程之有task_struct结构),就好像每个人之有户口或者档案一样。一个物理上存在的人,如果没有户口,从管理的角度来说便是不存在的。同样,一个物理上存在的内存页面,如果没有一个相应的page结构,就根本不会被系统看到。在系统的初始化阶段,内核根据检测到的物理内存的大小,为每一个页面都建立一个page结构,形成一个page结构的数组,并使一个全局变量mem_map指向这个数组。同时,又按需要将这些页面拼合成物理地址连续的许多内存页面块,再根据块的大小建立起若干管理区(zone),而在每个管理区中则设置一个空闲块队列,以便物理内存页面的分配使用。这一些,我们已经在前面看到过了。

与此类似,交换设备(通常是磁盘,也可以是普通文件)的每个物理页面也是要在内存中有个相应的数据结构(或者说户口),不过那要简单的多,实际上只是一个计数,表示该页面是否已被分配使用,以及有几个用户在共享这个页面。对盘上页面的管理是按文件或磁盘设备来进行的。内核中定义了一个swap_info_struct数据结构,用以描述和管理用于页面交换的文件或设备。它的定义如下:

struct swap_info_struct {
	unsigned int flags;
	kdev_t swap_device;
	spinlock_t sdev_lock;
	struct dentry * swap_file;
	struct vfsmount *swap_vfsmnt;
	unsigned short * swap_map;
	unsigned int lowest_bit;
	unsigned int highest_bit;
	unsigned int cluster_next;
	unsigned int cluster_nr;
	int prio;			/* swap priority */
	int pages;
	unsigned long max;
	int next;			/* next entry on swap list */
};

其中的指针swap_map指向一个数组,该数组中的每一个无符号短整型即代表盘上(或普通文件中)的一个物理页面,而数组的下标则决定了该页面在盘上或文件中的位置。数组的大小取决于pages。它表示该页面交换设备或文件的大小。设备上(或文件中,设备也是一种文件,下同)的第一个页面,也即swap_map[0]所代表的那个页面是不用与页面交换的,它包含了该设备或文件自身的一些信息以及一个表明哪些页面可供使用的位图。这些信息最初是把该设备格式化页面交换区时设置的。根据不同的页面交换区格式(以及版本),还有一些其他的页面也不供页面交换使用。这些页面都集中在开头和结尾两个地方,所以swap_info_struct结构中的lowest_bit和highest_bit就说明文件中从什么地方开始到什么地方为止是供页面交换使用的。另一个字段max则表示该设备或文件中最大的页面号,也就是设备或文件的物理大小。

由于存储介质是转动的磁盘,将地址连续的页面存储在连续的磁盘扇区中不见得是最有效的方法,所以在分配盘上页面空间时尽可能按集群(cluster)方式进行,而字段cluster_next和cluster_nr就是为此而设置的。

linux内核允许使用多个页面交换设备(或文件),所以在内核中建立了一个swap_info_struct结构的阵列(数组)swap_info,定义如下:

struct swap_info_struct swap_info[MAX_SWAPFILES];

同时,还设立了一个队列swap_list,将各个可以分配物理页面的磁盘设备或文件的swap_info_struct结构按优先级链接在一起:

struct swap_list_t swap_list = {-1, -1};

这里的swap_list_t数据结构定义如下:

struct swap_list_t {
	int head;	/* head of priority-ordered swapfile list */
	int next;	/* swapfile to be used next */
};

开始队列为空,所以head和next均为-1。当系统调用swap_on指定将一个文件用于页面交换时,就将该文件的swap_info_struct结构链入队列中。

就像通过pte_t数据结构(页面表项)将物理内存页面与虚存页面建立联系一样,盘上页面也有这么一个swp_entry_t数据结构,定义如下:

/*
 * A swap entry has to fit into a "unsigned long", as
 * the entry is hidden in the "index" field of the
 * swapper address space.
 *
 * We have to move it here, since not every user of fs.h is including
 * mm.h, but m.h is including fs.h via sched .h :-/
 */
typedef struct {
	unsigned long val;
} swp_entry_t;

可见,一个swp_entry_t结构实际上只是一个32位无符号整数。但是这个32位整数实际上分成3部分,如图:

文件include/asm-i386/pgtable.h中还为type和offset两个位段的访问以及pte_t结构之间的关系定义了几个宏操作:

/* Encode and de-code a swap entry */
#define SWP_TYPE(x)			(((x).val >> 1) & 0x3f)
#define SWP_OFFSET(x)			((x).val >> 8)
#define SWP_ENTRY(type, offset)		((swp_entry_t) { ((type) << 1) | ((offset) << 8) })
#define pte_to_swp_entry(pte)		((swp_entry_t) { (pte).pte_low })
#define swp_entry_to_pte(x)		((pte_t) { (x).val })

这里offset表示页面在一个磁盘设备或文件中的位置,也就是文件中的逻辑页面号;而type则是指该页面在哪一个文件中,是个序号。这个位段的命名很容易引起读者的误解,明明是指页面交换设备或文件的序号(一共可以容纳127个这样的文件,但实际上则视系统的配置而定,远小于127),为什么却称之为type呢?估计这是从pte_t结构中过来的。读者可能记得,pte_t实际上也是一个32位无符号整数,其中最高的20位为物理页面起始地址的高20位(物理页面起始地址的低12位永远为0,因为页面都是4KB边界对齐的),而与这7位相对应的则都是些表示页面各种性质的标志位,如Q/W,U/S,等等,所以称之为type位段。而swp_entry_t与pte_t两种数据结构大小相同,关系非常密切。当一个页面在内存中时,页面表中的表项pte_t的最低位P标志为1,表示页面在内存中,而其余各位指明物理内存页面的地址及页面的属性。而当一个页面在磁盘上时,则相应的页面表项不再指向一个物理内存页面,而是变成一个swp_entry_t表项,指示着这个页面的去向。由于此时其最低位为0,表示页面不在内存,所以CPU中的MMU单元对其余各位都忽略不顾,而留待系统软件自己加以解释。在linux内核中,就用它来惟一的确定一个页面在盘上的位置,包括在哪一个文件或设备,以及页面在此文件中的相对位置。

所以,当页面在内存时,页面表中的相应表项确定了地址的映射关系;而当页面不在内存时,则指明了物理页面的去向和所在。读者在阅读内核的源码时,不妨将SWP_TYPE(entry)想象成SWP_FILE(entry)。

下面转入本博客标题所说的物理页面周转的介绍。我们还是通过一些函数的代码来帮助读者理解。

先介绍一下用来释放一个磁盘页面的函数__swap_free。通过这个函数的阅读,读者可以加深对上面这一段说明的理解。此函数的代码在文件mm/swapfile.c中。而分配磁盘页面的函数__get_swap_page也在同一个文件中,读者不妨自行阅读。

先来看__swap_free的开头几行:

/*
 * Caller has made sure that the swapdevice corresponding to entry
 * is still around or has not been recycled.
 */
void __swap_free(swp_entry_t entry, unsigned short count)
{
	struct swap_info_struct * p;
	unsigned long offset, type;

	if (!entry.val)
		goto out;

	type = SWP_TYPE(entry);
	if (type >= nr_swapfiles)
		goto bad_nofile;
	p = & swap_info[type];
	if (!(p->flags & SWP_USED))
		goto bad_device;

如果entry.val为0,就显然不需要做任何事,因为在任何页面交换设备或文件中页面0是不用与页面交换的。接着,如前所述,SWP_TYPE所返回的实际上是页面交换设备的序号,即其swap_info_struct结构在swap_info数组中的下标。所以,159行以此为下标从swap_info中取得具体文件的swap_info_struct结构。文件找到以后,下面就来看具体的页面了:

	offset = SWP_OFFSET(entry);
	if (offset >= p->max)
		goto bad_offset;
	if (!p->swap_map[offset])
		goto bad_free;
	swap_list_lock();
	if (p->prio > swap_info[swap_list.next].prio)
		swap_list.next = type;
	swap_device_lock(p);
	if (p->swap_map[offset] < SWAP_MAP_MAX) {
		if (p->swap_map[offset] < count)
			goto bad_count;
		if (!(p->swap_map[offset] -= count)) {
			if (offset < p->lowest_bit)
				p->lowest_bit = offset;
			if (offset > p->highest_bit)
				p->highest_bit = offset;
			nr_swap_pages++;
		}
	}
	swap_device_unlock(p);
	swap_list_unlock();
out:
	return;

如前所述,offset是页面在文件中的位置,当然不能大于文件本身所提供的最大值。而p->swap_map[offset] 是该页面的分配(和使用)计数,如为0就表明尚未分配。同时,分配计数也不应大于SWAP_MAP_MAX。函数的调用参数count表示有几个使用者释放该页面,所以从计数中减去count。当计数达到0时,这个页面就真正变成空闲了。此时,如果页面落在当前可供分配的范围之外,就要相应地调整这个范围的边界lowest_bit和highest_bit,同时,可供分配的盘上页面的数量nr_swap_pages也增加了。值得注意的是,释放磁盘页面的操作实际上并不涉及磁盘操作。而只是在内存中账面上的操作,表示磁盘上那个页面的内容已经作废。所以,花费的代价是极小的。

知道了内核怎样管理内存页面和盘上页面以后,就可以来看看内存页面的周转了。当一个内存页面空闲,也就是留在某一个空闲页面管理区的空闲队列中时,其page结构中的计数count为0,而在分配页面时将其设置成1。这是在函数rmqueue中通过set_page_count设置的,我们在前面已经看到过。

所谓内存页面的周转有两方面的意思。其一是页面的分配、使用和回收,并不一定涉及页面的盘区交换。其二才是盘区交换,而交换的目的最终也是页面的回收。并非所有的内存页面都是可以交换出去的。事实上,只有映射到用户空间的页面才会被交换出,即系统空间的页面则不在此列。这里要说明一下,在内核中可以访问所有的物理页面,换言之所有的物理页面在系统空间中都是有映射的。所谓用户空间的页面,是指在至少一个进程的用户空间中有映射的页面,反之则为(只能由)内核使用的页面。

按页面的内容和性质,用户空间的页面由下面几种:

  • 普通的用户空间页面,包括进程的代码段、数据段、堆栈段,以及动态分配的存储堆。其中有些页面从用户程序即进程的角度看是静态的(如代码段),但从系统的角度看仍是动态分配的。
  • 通过系统调用mmap映射到用户空间的已打开文件的内容。
  • 进程间的共享内存区

这些页面既涉及分配、使用和回收,也涉及页面的换出、换入。

凡是映射到系统空间的页面都不会被换出,但还是可以按使用和周转的不同而大致上分成几类。首先,内核代码和内核中全局变量所占的内存页面既不需要经过分配也不会被释放,这部分空间是静态的。(相比之下,进程的代码段和全局变量都在用户空间,所占的内存页面都是动态的,使用前要经过分配,最后都会被释放,并且中途可能被换出而回收后另行分配)

除此之外,内核中使用的内存页面也要经过动态分配,但永远都保留在内存中,不会被交换出去。此类常驻内存的页面根据其内容的性质可以分成两类。

一类是一旦使用完毕便无保存的价值,所以立即便可释放、回收。这类页面的周转很简单,就是空闲->(分配)->使用->(释放)->(空闲)。这种途径的内核页面大致上有这样一些:

  • 内核中通过kmalloc或vmalloc分配、用作某些临时性使用和为管理目的而设的数据结构,如vm_area_struct数据结构等等。这些数据结构一旦使用完毕便无保存价值,所以立即便可释放。不过由于一个页面中往往有多个同种数据结构,所以要到整个页面都空闲时才能把页面释放。
  • 内核中通过alloc_pages分配,用作某些临时性使用和为管理目的的内存页面,如每个进程的系统堆栈所在的两个页面,以及从系统空间复制参数时使用的页面等等。这些页面也是一旦使用完毕便无保存的价值,所以立即便可释放。

另一类虽然使用完毕了,但是其内容仍有保存的价值。只要条件允许,把这些页面养起来也许可以提高以后的操作效率。这类页面(或数据结构)在释放之后要放入一个LRU队列,经过一段时间的缓冲让其老化;如果在此期间忽然又要用到其内容了,便直接将页面连内容分配给用户;否则便继续老化,直到条件不再允许时才加以回收。这种用途的内核页面大致上有下面这些:

  • 在文件系统操作中用来缓冲存储一些文件目录结构dentry的空间。
  • 在文件系统操作中用来缓冲存储一些inode结构的空间。
  • 用于文件系统的读写操作的缓冲区。

这些页面的内容时从文件系统中直接读入或经过综合取得的,释放后立即回收另作他用也并无不可,但是那样以后要用时就又要付出代价了。

相比之下,页面交换时最复杂的,所以我们将花较大的篇幅来介绍。

显然,最简单的页面交换策略就是:每当缺页异常时便分配一个内存页面,并把在磁盘上的页面读入到分配得到的内存页面中。如果没有空闲页面可供分配,就设法将一个或几个内存页面换出到磁盘上,从而腾出一些内存页面来。但是,这样完全消极的页面交换策略有个缺点,就是页面的交换总是临阵磨枪,发生在系统繁忙的时候而没有调度的余地。比较积极的做法是定期的,最好是在系统相对空闲时,挑选一些页面预先换出而腾出一些内存页面,从而在系统中维持一定的空闲页面供应量,使得在缺页异常发生时总是有空闲内存页面可供分配。至于挑选的准则,一般都是LRU,即挑选最近最少使用的页面。但是,这种积极的页面交换策略实行起来也有问题,因为实际上并不存在一种方法可以准确地预测对页面的访问。所以,完全有可能发生这样的现象。就是一个页面已经好久没有受到访问了,但是刚把它换出到磁盘上,却又有访问了,于是只好又赶紧把它换进来。在最坏的情况下,有可能整个系统的处理能都被这样的换入换出所饱和,而实际上根本不能进行有效的运算和操作。有人把此种现象称为(页面的)抖动。

为了防止这种情况的发生,可以将页面的换出和内存页面的释放分成两步来做。当系统挑选出若干内存页面准备换出时,将这些页面的内容写入相应的磁盘页面中,并且将相应页面表项的内容改成指向盘上页面(P标志位0,表示页面不在内存中),但是所占据的内存页面却并不立即释放,而是将其page结构留在一个暂存(cache)队列(或缓冲队列)中,只是使其从活跃状态转入了不活跃状态,就像军人从现役转入了预备役。至于内存页面的退役,即最后释放,则推迟到以后有条件地进行。这样,如果在一个页面被换出以后立即又受到访问而发生缺页异常,就可以从物理页面的暂存队列中找回相应的页面,还是没有受到访问,那就到了最后退役的时候了。如果留在暂存队列中页面又受到访问,确切的说是发生了以此页面为目标的页面异常,那么只要恢复这个页面的映射并使其脱离暂存队列就可以了,此时该页面又回到了活跃状态。

这种策略显然可以减少抖动的可能,并且减少系统在页面交换上的花费。可是,如果更深入地考察这个问题,就可以看出其实还是可以改进。首先,在准备换出一个页面时并不一定要把它的内容写入磁盘。如果自从最近一次换入该页面以后从未写过这个页面,那么这个页面是干净的,也就是与盘上页面的内容相一致,这样的页面当然不用写出去。其次,即使是脏页面,也不必立即就写出去,而可以先从页面映射表断开,经过一段时间的冷却或老化后再写出去,从而变成干净页面。至于干净页面,则还可以继续缓冲到真有必要时才加以回收,因为回收一个干净页面的花费是很小的。

综上所述,物理内存页面换入换出的周转要点如下:

  1. 空闲。页面的page数据结构通过其队列头结构list链入某个页面管理区(zone)的空闲区队列free_area。页面的使用计数count为0。
  2. 分配。通过函数__alloc_pages或者__get_free_page从某个空闲队列中分配内存页面,并将所分配页面的使用计数count置成1,其page数据结构的队列头list结构则变成空闲。
  3. 活跃状态。页面的page数据结构通过其队列头结构lru链入到活跃队列active_list,并且至少有一个进程的(用户空间)页面表项指向该页面。每当为页面建立或恢复映射时,都使页面的使用计数count加1。
  4. 不活跃状态(脏)。页面的page数据结构通过其队列头结构lru链入不活跃脏页面队列inactive_dirty_list,但是原则上不再有任何进程的页面表项指向该页面。每当断开页面的映射时都使页面的使用即使count减少1。
  5. 将不活跃脏页面的内容写入交换设备,并将页面的page数据结构从不活跃脏页面队列inactive_dirty_list转移至某个不活跃干净页面队列中。
  6. 不活跃状态(干净)。页面的page数据结构通过其队列头结构lru链入某个不活跃干净页面队列,每个页面管理区都有一个不活跃干净页面队列inactive_clean_list。
  7. 如果在转入不活跃状态以后的一段时间内页面受到访问,则又转入活跃状态并恢复映射。
  8. 当有需要时,就从干净页面队列中回收页面,或退回到空闲队列中,或直接另行分配。

当然,实际的实现还要更复杂一些。

为了实现这种策略,在page数据结构中设置了所需的各种成分,并在内核中设置了全局性的active_list和inactive_dirty_list两个LRU队列,还在每个页面管理区中设置了一个inactive_clean_list。根据页面的page结构在这些LRU队列中的位置,就可以知道这个页面转入不活跃状态后的时间的长短,为回收页面提供参考。同时,还通过一个全局的address_space数据结构的swapper_space,把所有可交换内存页面管理起来,每个可交换内存页面的page数据结构都通过其队列头结构list链入其中的一个队列。此外,为加快在暂存队列中的搜索,又设置了一个杂凑表page_hash_table。

让我们看看内核是怎样将一个内存页面链入这些队列的。内核在为某个需要换入的页面分配了一个空闲内存页面以后,就通过add_to_swap_cache将其page结构链入相应的队列,这个函数的定义如下:


void add_to_swap_cache(struct page *page, swp_entry_t entry)
{
	unsigned long flags;

#ifdef SWAP_CACHE_INFO
	swap_cache_add_total++;
#endif
	if (!PageLocked(page))
		BUG();
	if (PageTestandSetSwapCache(page))
		BUG();
	if (page->mapping)
		BUG();
	flags = page->flags & ~((1 << PG_error) | (1 << PG_arch_1));
	page->flags = flags | (1 << PG_uptodate);
	add_to_page_cache_locked(page, &swapper_space, entry.val);
}

在调用这个函数前要先将页面锁住,以免受到干扰。因为是刚分配的空闲页面,其PG_swap_cache标志位必须为0,指针mapping也必须是0。同时,页面的内容时刚从交换设备读入的,当然与盘上页面一致,所以把PG_uptodate标志位设成1。函数add_to_page_cache_locked的代码如下:

add_to_swap_cache=>add_to_page_cache_locked


/*
 * Add a page to the inode page cache.
 *
 * The caller must have locked the page and 
 * set all the page flags correctly..
 */
void add_to_page_cache_locked(struct page * page, struct address_space *mapping, unsigned long index)
{
	if (!PageLocked(page))
		BUG();

	page_cache_get(page);
	spin_lock(&pagecache_lock);
	page->index = index;
	add_page_to_inode_queue(mapping, page);
	add_page_to_hash_queue(page, page_hash(mapping, index));
	lru_cache_add(page);
	spin_unlock(&pagecache_lock);
}

调用参数mapping是一个address_space结构指针,就是&swapper_space。这种数据结构的定义如下:


struct address_space {
	struct list_head	clean_pages;	/* list of clean pages */
	struct list_head	dirty_pages;	/* list of dirty pages */
	struct list_head	locked_pages;	/* list of locked pages */
	unsigned long		nrpages;	/* number of total pages */
	struct address_space_operations *a_ops;	/* methods */
	struct inode		*host;		/* owner: inode, block_device */
	struct vm_area_struct	*i_mmap;	/* list of private mappings */
	struct vm_area_struct	*i_mmap_shared; /* list of shared mappings */
	spinlock_t		i_shared_lock;  /* and spinlock protecting it */
};

结构中有三个队列头,前两个分别用于干净的和脏的页面(需要写出),另一个队列头locked_pages用于需要暂时锁定在内存不让换出的页面。数据结构swapper_space的定义如下:

struct address_space swapper_space = {
	LIST_HEAD_INIT(swapper_space.clean_pages),
	LIST_HEAD_INIT(swapper_space.dirty_pages),
	LIST_HEAD_INIT(swapper_space.locked_pages),
	0,				/* nrpages	*/
	&swap_aops,
};

结构中的最后一个属性指向另一个数据结构swap_aops,里面包含了各种swap操作的函数指针,

从函数add_to_page_cache_locked中可以看到,页面page被加入三个队列中。下面读者会看到,page结构通过其队列头list链入暂存队列swapper_space,通过指针next_hash和双重指针pprev_hash链入某个杂凑队列,并通过其队列头lru链入LRU队列active_list。

代码中的page_cache_get实际定义为get_page,实际上只是将页面的使用计数page->count加1。

#define get_page(p)		atomic_inc(&(p)->count)

#define page_cache_get(x)	get_page(x)

先将给定的page结构通过add_page_to_inode_queue加入到swapper_space中的clean_pages队列中,代码如下:

static inline void add_page_to_inode_queue(struct address_space *mapping, struct page * page)
{
	struct list_head *head = &mapping->clean_pages;

	mapping->nrpages++;
	list_add(&page->list, head);
	page->mapping = mapping;
}

可见,链入的是swapper_space中的clean_pages队列,刚从交换设备读入的页面当然是干净页面。为什么这个函数叫add_page_to_inode_queue呢?这是因为页面的缓冲不光是为页面交换而设的,文件的读、写操作也要用到这种缓冲机制。通常来自同一个文件的页面就通过一个address_space数据结构来管理,而代表着一个文件的inode结构中有个成分i_data,那就是一个address_space数据结构。从这个意义上说,用来管理可交换页面的address_space数据结构swapper_space只是个特例。

然后通过add_page_to_hash_queue将其链入到某个杂凑队列中,其代码如下:


static void add_page_to_hash_queue(struct page * page, struct page **p)
{
	struct page *next = *p;

	*p = page;
	page->next_hash = next;
	page->pprev_hash = p;
	if (next)
		next->pprev_hash = &page->next_hash;
	if (page->buffers)
		PAGE_BUG(page);
	atomic_inc(&page_cache_size);
}

链入的具体队列取决于杂凑值:

#define page_hash(mapping,index) (page_hash_table+_page_hashfn(mapping,index))

最后将页面的page数据结构通过lru_cache_add链入到内核中的LRU队列active_list中,代码如下:


/**
 * lru_cache_add: add a page to the page lists
 * @page: the page to add
 */
void lru_cache_add(struct page * page)
{
	spin_lock(&pagemap_lru_lock);
	if (!PageLocked(page))
		BUG();
	DEBUG_ADD_PAGE
	add_page_to_active_list(page);
	/* This should be relatively rare */
	if (!page->age)
		deactivate_page_nolock(page);
	spin_unlock(&pagemap_lru_lock);
}

这里的add_page_to_active_list是个宏操作,定义如下:

#define add_page_to_active_list(page) { \
	DEBUG_ADD_PAGE \
	ZERO_PAGE_BUG \
	SetPageActive(page); \
	list_add(&(page)->lru, &active_list); \
	nr_active_pages++; \
}

由于page数据结构可以通过其同一个队列头lru链入不同的LRU队列,所以需要有PG_active、PG_inactive_dirty、PG_inactive_clean等标志位来表明目前是在哪一个队列中。以后我们将会看到页面在这些队列的转移。

存储管理不完全是内核的事,用户进程可以在相当程度上参与对内存的管理,可以在一定的范围内对于其本身的内存管理向内核提出一些要求,例如通过系统调用mmap将一文件映射到它的用户空间。特别是特权用户进程,还掌握着对换入、换出机制的全局性控制权,这就是系统调用swapon和swapoff。调用界面为:

swapon(const char *path, int swapflags)

swapoff(const char *path)

这两个系统调用是为特权用户进程设置的,用以开始或终止把某个特定的盘区或文件用于页面的换入换出。当所有的盘区和文件都不再用于页面交换时,存储管理的机制就退化到单纯的地址映射和保护。在实践中,这样做有时候是必要的。一些嵌入式系统,常常用flash memory(闪存)来代替磁盘介质。对flash memory的写操作是很麻烦费时间的,需要将存储中的内容先抹去,然后才写入,而抹去的过程又很慢(与磁盘读写相比较)。显然,flash memory是不适合用作页面交换的。所以在这样的系统中应该将盘区交换关闭。事实上,在linux内核刚引导进来之初,所有的页面交换都是关闭的,内核在初始化期间要执行/etc/rc.d/rc.S命令文件,而这个文件中的命令之一就是与系统调用swapon相应的实用程序swapon,只要把这个命令行从文件中拿掉就没有页面交换了。

此外,还有几个用于共享内存的系统调用,也是与存储管理有关的。由于习惯上将共享内存归入进程间通信的范畴,对这个几个系统调用将在进程间通信的博客中另行介绍。

おすすめ

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