Linux 中页的回收

来源:互联网 发布:日本经济数据2016 编辑:程序博客网 时间:2024/06/01 10:18

1.回收的时间点

在linux中,为用户进程分配页框采用了延迟分配的策略,即使用“请求调页”的机制,这种机制的一个特点是无法主动的对页框进行回收,因为它无法准确判断进程的哪些页框已经可以回收。因此,内核必须在一些关键的时间点上对页框进行回收,这主要有三种情况:

       I.内存紧缺回收:发生alloc_page_buffers()、__getblk()、__alloc_pages()等内存分配函数失败时;

       II.睡眠回收:发生在进入suspend-to-disk状态时;

       III.周期回收:由kswapd内核线程和events内核线程引起的周期性回收.

2.回收页的选取

       Linux中的页框回收算法(PFRA)区别四种不同的页:不可回收页、可交换页、可同步页和可丢弃页。排除内核的保留页、锁定页(不可回收页),以及一些用于磁盘高速缓存、slab分配器的页(可丢弃页和部分可同步页),我们只关心属于用户态进程地址空间的页和页高速缓存(page cache)中的页。前者是在用户态下可直接访问的页,可以不需要唤醒操作系统,后者则是属于文件系统的页在内存中的缓存。与此同时,LRU链表中也只包含这两种类型的页,并将它们分为active链表和inactive链表。PFRA就是通过从inactive链表“窃取”页的方式来回收空闲页,而active链表中的页将用来补充inactive链表。

       用户态进程空间的页还可以分为匿名页和文件映射页,前者包括进程用户态的堆、栈等中的页;而后者是属于文件系统的页,并以mmap()的方式映射到进程地址空间中。之所以这样区分是因为文件映射页较为特殊,它的页在空间上其实是属于页高速缓存,但在进行读访问时则与匿名页一样,直接在用户层进行。写访问需要区别页的映射形式:共享映射和私有映射。这两种方式的区别将在后文进行描述。

       在页框回收的过程中,mark_page_accessed()函数、page_referenced()函数和refill_inactive_zone()函数(在3.11版本中为shrink_active_list()函数)是非常重要的辅助函数。内存分配失败时会激活内存紧缺回收,do_try_to_free_pages()会开始页框回收的流程。由于内存的每个管理区(zone)都有自己的LRU链表,因此会对每个不同的zone进行页框回收。

       页框回收算法有几个总的原则,其中最为重要的一条就是在回收用户态进程地址空间的页之前,应该首先回收磁盘与内存高速缓存中的页,因为回收这些页不会修改进程的页表项。对于每一个内存管理区,refill_inactive_zone()函数将活动链表中的页补充到非活动链表中,以便用于回收。这是在每次执行真正的页框回收算法之前必须先完成的工作。而该函数会使用一个叫做交换倾向(swap tendency)的经验值,来决定是移动所有的页还是只移动属于页高速缓存中的页(尽可能的保留用户空间的页)。具体的说,函数扫描zone->active_list中的页,将选定的页放在一个临时链表l_hold中,然后在对这个临时链表进行扫描,对于属于进程地址空间的页,以下三种情况它将重新被放回active链表,即暂时不被回收:

       I.交换倾向值小于100;

       II.是匿名页但又没有激活的交换区;

       III.page_referenced()函数返回正数;

对于第I种情况,在通常情况下,只有该内存管理区的交换倾向值大于100时,进程地址空间的页才开始被回收;对于第II种情况,回收的匿名页数据本应写到交换区上,但是没有激活的交换区,因此这类页不被回收;而对于第III种情况,page_referenced()函数返回正数表明该页最近被访问过,因此不应该被回收。

页框回收算法对于扫描到的每个页都会执行page_referenced()函数,如果PG_referenced标志或页表项中的Accessed标志置位,则该函数返回1,否则返回0;该函数首先检查页描述符的PG_referenced标志,如果标志置位则清0。然后利用反向映射的方法,对引用该页的所有页表项中的Accessed标志进行检查并清0。最后一步过程是相当必要的,特别是对于用户空间的页来说,对它访问是在用户态直接完成的,没有操作系统的帮助,因此,某些对页的修改不会引起例如PG_referenced和PG_dirty等标志位的变化,我们就只能通过硬件上的这些标志来进行辨别。这种情况还有很多,比如当需要交换一个共享页时,内核也是通过反向映射查找到每一个引用该页的页表项,并检查Accessed标志是否被清0,如果没有,则表示该页正在被使用,内核会将它清0,并返回SWAP_FAIL;同时假如页可以被回收,内核检查页表项的Dirty标志是否置位,若是则将页的PG_dirty标志置位。

       现在的问题就在于页描述符中的PG_referenced标志和页表项中的Accessed等标志是什么时候被置位的。首先来说,对于页表项中的Accessed标志,每当MMU对相应的页框进行寻址时就设置这个标志,而对于Dirty标志,每当对一个页框进行写操作时就设置这个标志。而这两个标志的主要作用也是在页被交换时由操作系统使用,并且很重要的一点,MMU从来不重置这两个标志,而是必须由操作系统去完成,正如上面所讲的一样。而当内核必须把一个页标记为访问过时,就调用mark_page_accessed()函数,该函数将页的PG_referenced标志置位。同时,当函数检测到某一页的PG_referenced标志已经置位,并且该页属于inactive链表时,它会将该页移动到active链表,并将PG_referenced标志清0。下面是mark_page_accessed()被调用的几种常见的情况:

       I.当按需装入一个进程匿名页时(由do_anonymous_page()函数执行);

       II.当按需装入内存映射文件的一个页时(由filemap_nopage()函数执行);

       III.当换入一个页时(由do_swap_page()函数执行);

       IV.当从文件读取数据页时(由do_generic_file_read()函数执行);

前面三种情况都是在缺页中断发生时触发的,内核可以通过页表项区分这三种情况。

3.匿名页的分配

       之所以提到页的分配,是因为内核的设计总是很精巧的,在分配页框时就为后来的回收做了很多的准备工作。内核对于用户态进程的内存总是延迟分配的,当进程以malloc()等方式请求动态内存时,内核并没有立即给它分配相应的页框,而是分配给它一个属于进程地址空间的线性区(memory region)的使用权,这个线性区即VMA。当这段空间被访问时就会触发缺页中断,然后才会进行页框的分配。这也是所谓的“请求调页”机制。正如前面所说的一样,缺页中断处理例程能够区别几种不同缺页方式。详细来说,在handle_pte_fault()函数中,若判断条件pte_none()为真,则表明此页框还未分配,然后do_no_page()函数会检查该线性区的vm_ops字段和vm_ops_nopage字段,这是用于区分匿名页还是文件映射页。如果是匿名页则调用do_anonymous_page()函数,后者不仅调用alloc_page()函数进行页框分配,对于写操作,还进行将页增加到active链表、设置PG_referenced标志、设置页表项中相应的标志位等操作;而读操作的工作要少一些,因为内核随后可以用写时复制(COW)的方式来进行剩余的工作。无论是对页描述符中的标志还是对页表项中的标志,在分配时就对它们进行置位是很必要的,因为内核推迟分配已经到了不能再推迟的地步,这些页显然是马上将要被访问的,这也避免了PFRA立即将这些页回收掉。

       另外一点,同时将页描述符的标志和页表项中的标志置位也是很必要的。设想这样一种情况:属于用户态进程地址空间的某页第一次访问时,通过缺页中断处理程序进行了页框分配,并设置了相应的标志位,而接下来PFRA在回收时对该页进行了扫描,对该页调用了page_referenced()标志,虽然该页没有被回收且还在active链表中,但其PG_referenced标志和Accessed标志被清0,下一次该页被访问时,由于在用户态执行并没有唤醒操作系统,其PG_referenced标志不会被置位,而MMU可以将Accessed标志置位,这样下次将要被交换时,内核也能通过页表项中的标志发现该页正在被使用,并返回SWAP_FAIL。

4.内存映射页的分配与回收

       内存映射是将属于文件系统的页加入到用户态进程地址空间中,进程在访问时不需要在经过操作系统就可直接访问,也就是mmap的一种最常见的应用。在现有体系结构下,这些页在物理空间上是属于页高速缓存的,但是在进程地址空间中有相应的VMA与之关联(进程页表中有相应的映射关系)。这里必须要区别共享型和私有型两种不同的内存映射方式。对于共享型内存映射,在线性区页上的任何写操作都会修改文件系统上的文件,并且这种修改对于其他共享进程是可见的。而私有型内存映射只是为了读文件才建立的,任何写操作都会使得内核停止映射该文件中的页,并且不修改文件本身的内容。简单地说,进程试图修改一个私有内存映射的页时,内核就把该页框进行复制,并对进程的页表项映射关系进行修改。原来的页框仍在页高速缓存中,但不再属于内存映射。

       文件内存映射也依赖于“请求调页”机制,前面已经提到,do_no_page()函数会通过检查线性区的vm_ops字段和vm_ops_nopage字段来区分匿名页和文件映射页。如果是文件映射页,就是调用filemap_nopage()函数进行接下来的工作。

       映射页的回收方式显然是同步而不是交换,因为它本身是属于文件系统(或者说页高速缓存)的。举个例子,进程可以使用msync()系统调用把属于共享型内存映射的脏页刷新到磁盘。其中,filemap_sync()函数扫描线性区中线性地址区间所对应的页表项,对于找到的每个页,用反向映射的方式将所有页表项的Dirty标志置位,并设置该页的PG_dirty标志,以便之后进行同步操作。
0 0
原创粉丝点击