各种缓存(二)

上一篇了解了cache,tlb,页缓存和mmap,这篇则主要关注交换缓存和交换区。前面几种缓存都是为了系统能更快地读取数据:页缓存将文件数据缓存至内存中减少磁盘io, tlb缓存页表数据便于地址翻译找到物理页面,cache则将物理页面中的数据进行缓存便于CPU读取。但要满足用户的需求,或者一直满足内存密集型应用程序的需求,无论计算机上可用的物理内存有多少都是不够的,因此内核需要将很少使用的部分内存换出到块设备以提供更多的主内存,这种机制为页交换或换页,交换区和交换缓存则是和页交换相关的。

 

可换出页

只有少量几种页可以换出到交换区,对其他页来说换出到块设备与之对应的后备存储器即可。如果一个很少使用的页的后备存储器是一个块设备(如文件),那么就无需换出被修改的页,而是直接与块设备同步,腾出的页帧可以重用,如果再次需要该数据,可以从来源重新建立该页。如果页的后备存储器是一个文件但是不能在内存中修改,那么在不需要的情况下可直接丢弃该页,而需要交换到交换区的为以下几种:

  • 类别为MAP_ANONYMOUS的页,没有关联到文件(或属于/dev/zero的一个映射),例如进程的栈或者是使用mmap匿名映射的内存区。
  • 进程的私有映射用于映射修改后不向底层块设备回写的文件,通常换出到交换区,因此此时不能从文件恢复页的内容,内核使用MAP_PRIVATE标志来创建此类映射。
  • 所有属于进程堆以及使用malloc分配的页(malloc又使用了brk系统调用或匿名映射)。
  • 用于实现某种进程间通信机制的页,例如用于在进程之间交换数据的共享内存页。

需要注意的是,由内核本身使用的内存页绝不会换出,用于将外设映射到内存空间的页也不能换出。

文件页和匿名页:page->mapping末位为0时,说明为文件映射页,mapping指向对应文件的address_space;page->mapping末位为1时,指向anon_vma(包含了1至多个vma),说明为匿名映射页。在内存回收时,匿名页将会被交换到交换区而保存起来。交换之后页将被释放。匿名页没有后备存储,因此需要将其写入交换区。交换区用来为匿名页提供备份,匿名页可以分为三类:属于进程匿名线性区(如用户态堆栈、堆)的页;属于进程私有内存映射的脏页;属于IPC共享内存的页。

 

交换区的组织

换出的页或者保存在一个没有文件系统的专用分区中,或者存储在某个现有文件系统中的一个定长文件中,可以同时使用几个这样的区域,还可以根据各个交换区的速度不同,为其指定优先级。内核使用交换区时可以根据优先级进行选择。每个交换区都细分为若干连续的槽,每个槽的长度刚好与系统的一个页帧相同。

本质上,系统中的任何一页都可以容纳到交换区的任一槽中,此外内核还使用了一种聚集构造法,使得能够尽快访问交换区。进程内存区中连续的页将按照特定的聚集大小逐一写到硬盘上,如果交换区中没有更多空间可容纳此长度的聚集,内核可以使用其他任何位置上的空闲槽位。

如果内核使用了几个优先级相同的交换区,内核将使用一种循环进程来确保尽可能均匀地利用各个交换区。如果交换区的优先级不同,内核首先使用高优先级的交换区,然后逐渐转移到优先级较低的交换区。内核使用位图用于跟踪交换区中各槽位的使用/空闲状态。

有两个用户空间工具可用于创建和启用交换区,分别是mkswap(用于格式化一个交换分区/文件)和swapon(用于启用一个交换区)

交换子系统主要功能为:在磁盘上建立交换区;管理交换区空间,分配与释放页槽;利用已被换出的页的pte的换出页标识符追踪数据在交换区中的位置;提供函数从ram中把页换出到交换区或换入到ram;

交换区可以用来扩展内存地址空间,使之被用户态进程有效的使用。一个系统上运行的应用所需要的内存总量可能会超出系统中当前的物理内存总量,其原理就是将暂时不用的内存交换出去,待用到的时候再交换进来。从内存中换出的页存放在交换区(swap area)中。交换区可架设在磁盘分区、大文件甚至内存型文件系统中。每个交换区都由一组页槽(page slot)组成,每个页槽大小一页。交换区的第一个页槽永久存放有关交换区的信息,使用结构swap_header表示。每个活动的交换区都有自己的swap_info_struct。

pte共有三种状态:1)当页不属于进程的地址空间(进程页表下),或者页框还没有分配给进程时,此时是空项;2)最后一位为0,表示该页被换出,此时pte表示为换出页标识符(swap_entry_t),该标识符由三个部分充满一个long:最高5bit表示来自哪个swap分区,2bit表示是否来自于shmem/tempfs,24bit表示在页槽中的offset,交换区最多有2^24个页槽(64GB);3)最后一位为1,表示页在ram中。

 

交换缓存

交换缓存在选择换出页的操作和实际执行页交换的机制之间充当协调者,即在页面选择策略和用于在内存和交换区之间传输数据的机制之间,交换缓存充当代理人的角色,这两个部分通过交换缓存交互。交换缓存用于以下目的,具体取决于页交换请求的方向(读入内存或写入交换区):

  • 在换出页时,页面选择逻辑首先选择一个适当的、很少使用的页帧。该页帧缓冲在页缓存中,然后将其转移到交换缓存。
  • 如果换出页由几个进程在同时使用,内核必须设置进程页目录中的对应页表项,使之指向交换文件中相关的位置。在其中某个进程访问该页的数据时,该页将再次换入,该进程对应此页的页表项将设置为该页当前的内存地址。但是这样会导致一个问题:其他进程仍然指向交换文件中的位置。因此在换入共享页时,他们将停留在交换缓存中,直到所有进程都已经从交换区请求该页,并都知道了该页在内存中新的位置位置。没有交换缓存,内核无法确定一个共享的内存页是否已经换入内存,将不可避免地导致对数据的冗余读取。但是在引入逆向映射机制后,通过rmap可找到引用该页数据的所有进程,这意味着引用该页的所有进程中的相关页表项都可以更新,指向交换区中对应的位置。这意味着该页的数据可以立即换出而无须在交换缓存中保持很长一段时间。

换出页在页表中通过一种专门的页表项来标记,其中会存储:1)一个标志,表示页已经换出;2)该页所在交换区的编号;3)对应槽位的偏移量,用于在交换区中查找该页所在的槽位。一个pte_t实例可通过pte_to_swap_entry函数转换为一个swap_entry_t实例,该实例存储了交换分区的标识和该交换分区内部的偏移量,以便唯一确定一页。

就数据结构而言,交换缓存就是一个页缓存,swapper_space中表示了相关函数及结构:

struct address_space swapper_spaces[MAX_SWAPFILES] = {
    [0 ... MAX_SWAPFILES - 1] = {
        .page_tree    = RADIX_TREE_INIT(GFP_ATOMIC|__GFP_NOWARN),
        .a_ops        = &swap_aops,
        .backing_dev_info = &swap_backing_dev_info,
    }
};

其中通过swap_ops来处理通过交换缓存提供的地址空间,这些函数是交换缓存与系统交换区进行数据传输的接口:

static const struct address_space_operations swap_aops = {
    .writepage    = swap_writepage,
    .set_page_dirty    = swap_set_page_dirty,
    .migratepage    = migrate_page,
};

swap_writepage将脏页与底层块设备同步,其目的并非用来维护物理内存和块设备之间的一致性。其目的是将页从交换缓存移除,将其数据传输到交换区。

swap_set_page_dirty用于将页标记为脏。

 

向交换区来回传送页会引发很多竞争条件,具体的说,交换子系统必须仔细处理下面的情形:

1)多重换入:两个进程可能同时要换入同一个共享匿名页;

2)同时换入换出:一个进程可能换入正由PFRA(页框回收机制)换出的页;

交换缓存(swap cache)的引入就是为了解决这类同步问题的。关键的原则是:没有检查交换缓存是否已包含了所涉及的页,就不能进行换入或换出操作。有了交换缓存,涉及同一页的并发交换操作总是作用于同一个页框的。因此,内核可以安全的依赖页描述符的PG_locked标志,以避免任何竞争条件。

如两个进程共享同一换出页,当第一个进程试图访问页时,内核开始换入页操作,第一步就是检查页框是否在交换缓存中,假定页框不在交换缓存中,那么内核就分配一个新页框并把它插入到交换缓存,然后开始I/O操作,从交换区读入页的数据;同时,第二个进程访问该共享匿名页,与上面相同,内核开始换入操作,检查涉及的页框是否在交换缓存中。现在页框在交换缓存,因此内核只是访问页框描述符,在PG_locked标志清0之前(即I/O数据传输完毕之前),让当前进程睡眠。

当换入换出操作同时出现时,交换缓存起着至关重要的作用。shrink_list()函数要开始换出一个匿名页,就必须当try_to_unmap()从进程(所有拥有该页的进程)的用户态页表中成功删除了该页后才可以。但是当换出的页写操作还在执行的时候,这些进程中可能有某个进程要访问该页,而产生换入操作。在写入磁盘前,待换出的页由shrink_list()存放在交换缓存。考虑页由两个进程(A和B)共享的情况。最初,两个进程的页表项都引用该页框,该页有两个拥有者。当PFRA选择回收页时,shrink_list()把页框插入交换缓存。然后PFRA调用try_to_unmap()从这两个进程的页表项中删除对该页框的引用。一旦这个函数结束,该页框就只有交换缓存引用它,而引用页槽的有这两个进程和交换缓存。假如正当页中的数据写入磁盘时,进程B又访问该页,即它要用该页内部的线性地址访问它,那么缺页异常处理程序会发现页框正在交换缓存中,并把物理地址放回进程B的页表项。如果上面并发的换入操作没发生,换出操作结束,则shrink_list()会从交换缓存删除该页框并把它释放到伙伴系统。

可以认为交换缓存是一个临时区域,该区域存有正在被换入或换出的匿名页描述符。当换入或换出结束时(对于共享匿名页,换入换出操作必须对共享该页的所有进程进行),匿名页描述符就可以从交换缓存删除。

交换缓存由页缓存数据结构和过程实现。页缓存的核心就是一组基数树,基数树算法可以从address_space对象地址(即该页的拥有者)和偏移量值推算出页描述符的地址。在交换缓存中页的存放方式是隔页存放,并有如下特征:页描述符的mapping字段为null;页描述符的PG_swapcache标志置位;private字段存放于该页有关的换出页标识符;此外当页被放入交换缓存时,页描述符的count字段和页槽引用计数器的值都会增加,因为交换缓存既要使用页框,也要使用页槽。最后,交换缓存中的所有页只使用struct address_space swapper_spaces[MAX_SWAPFILES],因此一个交换分区的交换缓存对应一个基数树(由struct address_space.page_tree指向),换出页标识符中有对所属交换分区的标识,根据基数树对交换缓存中的页进行寻址。struct address_space.nrpages则用来存放交换缓存中的页数。

 

添加新页

可使用下面两个内核方法向交换缓存中添加页:

  • 在内核想要主动换出一页时会调用add_to_swap,即当策略算法确定可用内存不足时。该例程不仅将页添加到交换缓存中(在页数据写出到磁盘之前,会一直停留在其中),还在某个交换区中为该页分配一个槽位,尽管数据不会在此时复制到硬盘,但内核仍然必须考虑为该页选择交换区和对应的槽位。
  • 当从交换区读入由几个进程共享的一页(可根据交换区中的使用计数器判定)时,该页将同时保持在交换区和交换缓存中,直至被再次换出,或被所有共享该页的进程换入。内核通过add_to_swap_cache函数实现该行为,该函数将一页添加到交换缓存中而不对交换区进行操作。

使用函数get_swap_page在交换区中分配槽位,之后将需要换出的page实例设置PG_swapcache标志并将交换标识符swap_entry_t保存在page的private成员中,在页的内容实际换出时还需构造一个体系结构相关的页表项,然后将全局变量total_swapcache_pages加1来更新统计信息,还需将页插入到由swapper_space建立的基数树。最后,SetPageUpdate和SetPageDirty修改页的标志,因为页的内容尚未包含在交换区。对于交换页来说,对于的底层块设备是交换区,因而同步(几乎)就等价于页换出,将数据从内存传输到交换区是由与swapper_space关联的特定于地址空间的操作完成的,最后更新页表。

插入交换缓存的函数为__add_to_swap_cache(),主要执行步骤为:1)调用get_page(),增加该page的引用计数_mapcount(或称_refcount);2)置位PG_swapcache;3)将page->private设置为页槽索引;4)调用swap_address_space()从上面的swapper_spaces中获得address_space;5)调用radix_tree_insert()将页插入到基数树中(address_space->page_tree)

 

数据回写(页换出)

页换出的过程为:

1)准备交换缓存。如果shrink_page_list()函数确认某页为匿名页(PageAnon()函数返回1)而且交换缓存中没有相应的页框(页描述符的PG_swapcache标志为0),内核就调用add_to_swap()函数。该函数会在交换区分配一个页槽,并把一个页框(其页描述符作为参数传递进来)插入交换缓存。函数主要执行步骤如下:调用get_swap_page()分配一个新的页槽,如果失败则返回0;调用add_to_swap_cache(),插入基数树。

2)更新页表项。通过调用try_to_unmap()来确定引用了该匿名页的每个用户态页表项的地址,然后将换出页标识符写入其中。大概调用过程为:try_to_unmap()->remap_walk()->remap_walk_anon() –> rwc->remap_one()->try_to_unmap_one,通过page->private获得entry,构造出一个swp_pte->set_pte_at(),将swp_pte设置给pte

3)将数据写入交换区。1)检查页是否是脏页,如果是则pageout()将会被执行。其具体逻辑为:调用is_page_cache_freeable()判断该页的引用数,除了调用者、基数树(即swapcache)之外,还可能有某些buffer在引用该页(此时page的PG_private或PG_private2必定有置位);2)如果页的mapping为空则要么退出pageout(),要么该页属于buffer。通过page_has_private()来判断是否如此。如果是的话,则通过try_to_free_buffer()来释放缓冲区(这个缓冲区是文件系统缓冲);3)清零PG_dirty,pageout()回调page->mapping->a_ops->writepage(),而page的mapping指向全局变量swapper_spaces数组中某元素,从而调用swap_writepage,具体逻辑为:

在try_to_free_swap()中调用page_swapcount()检查是否至少有一个用户态进程引用该页。这里并不检查page->_mapcount,而是检查对应的页槽的引用计数。如果引用数为0,则从基数树中删除页框索引;

调用__swap_writepage,传入bio_end_io_t类型的回调函数end_swap_bio_write(),首先检查交换分区有无SWP_FILE,即是否正常开启并运行中。

调用bdev_write_page(),向块设备中写入指定页。参数有:struct swap_info_struct->bdev、page所对应的sector、要交换的page。进入该函数时,页被锁住且PG_writeback不置位,退出时状态相反。

最后将page释放。取消PG_locked。并将page->lru加入到free_pages。最后,数组free_pages会被free_hot_cold_page_list()释放,而交换不成功的页则要被putback。

数据回写由swap_writepage完成,内核首先调用remove_exclusive_swap_cache检查相关页是否只由交换缓存使用而内核其他部分都不再使用,是的话则可以换出,然后填充struct bio实例,包括块层需要的所有参数,然后使用setpagewriteback设置PG_writeback标志,通过submit_bio将写请求发送至块层,在写请求执行时,块层会将PG_writeback标志清除。将页的内容写入到交换区对应的槽位后,还需更新页表。一方面页表项需要指定该页不在内存(_PAGE_PRESENT标志清除表示该页已经换出,_PAGE_FILE标志位清除表示该页在交换缓存中,用于非线性映射的页表项也不会设置_PAGE_PRESENT,但可以通过_PAGE_FILE标志位与换出页相区分),另一方面还需指向对应槽位在交换区中的位置。(进行页面回收时,在页写回交换区后,如果页保存在交换缓存中则可以用__delete_from_swap_cache将该页从交换缓存删除,如果页不在交换缓存中,则使用__remove_from_page_cache将其从一般的页缓存删除)

 

页面回收

页面回收在两个地方触发:

  • 如果内核检测到在某个操作期间内存严重不足,将调用try_to_free_pages检查当前内存域中所有页,并释放最不常用的那些
  • 一个后台守护进程kswapd会定期检查内存使用情况,并检测即将发生的内存不足。

Linux使用LRU算法进行内存回收,并给每个zone都提供了5个LRU链表:Active Anon Page,活跃的匿名页,page->flags带有PG_active;Inactive Anon Page,不活跃的匿名页,page->flags不带有PG_active;Active File Cache,活跃的文件缓存,page->flags带有PG_active;Inactive File Cache,不活跃的文件缓存,page->flags不带有PG_active;unevictable,不可回收页,page->flags带有PG_unevictable;

共包含四种操作:将新分配的页加入到lru链表;将inactive的页从放到inactive list的链表尾部;将active的页转移到inactive list;将inactive的页移到active list;

而inactive list尾部的页,将在内存回收时优先被回收(写回或者交换)。

 

处理交换缺页异常(页换入)

页换入的过程:

当进程试图对一个已被换出的页进行寻址时,必然会发生页的换入。在以下条件全满足时,缺页异常处理程序会触发一个换入操作:1)引起异常的地址所在的页是一个有效的页,也就是说,它属于当前进程的一个线性区;2)页不在内存中,也就是页表项的Present标志被清除;3)与页有关的页表项不为空,但是PG_dirty位被清零,意味着页表项乃是一个换出页标识符。

换入时首先检查该页是否在交换缓存中,若是则直接返回,若没有,则需要根据pte的换出页标识符从对应的交换区的页槽中读取该页。首先需要分配一个新的内存页容纳从交换区读取的数据,如果页分配成功,内核将添加该page实例到交换缓存,并将其添加到活动页的LRU缓存,然后与换出页类似,通过swap_readpage发起从硬盘到物理内存的数据传输(如交换区是一个文件,则其file描述符保存在swap_info_struct->swap_file中,之后读取页面与读取文件类似。):get_swap_bio产生一个适当的bio请求,而submit_bio将该请求发送到块层。其中add_page_to_swap_cache自动锁定页,swap_readpage通知块层在页已经完全读入后调用end_swap_bio_read。如果顺利会对该页设置PG_uptodate标志并解锁。因为读操作是异步的,但在页标记为PG_uptodate并解锁时,内核可以确认其中已经填充了所需的数据。

 

访问换出页导致的缺页异常,由mm/memory.c中的do_swap_page处理,代码流程如下:

内核不仅要检查所请求的页是否仍然或已经在交换缓存中,它还使用一种简单的预读方法一次性从交换区读入几页,预防未来可能出现的缺页异常。

换出页所在的交换区和槽位信息都保持在页表项中,内核首先使用pte_to_swp_entry将页表项转换为一个swp_entry_t实例,然后使用lookup_swap_cache检查所需的页是否在交换缓存中,若在交换缓存中则直接返回,如果该页的数据尚未写出,或该页是共享的,此前已经由另一个进程读入那么就有可能在交换缓存中找到。如果不在交换缓存中,内核不仅必须要读取该页,还必须发起一个预读操作读取几个预期可能使用的页。如果未在交换缓存中找到该页,内核则分配一个新的内存页,容纳从交换区读取的数据,如果页分配成功,内核将添加该page实例到交换缓存,并将其添加到活动页的LRU缓存,然后通过swap_readpage发起从硬盘到物理内存的数据传输。

在页已经换入后,需要用mark_page_accessed标记该页,使内核认定其已经访问过,然后将该页插入进程页表,此后调用page_add_anon_rmap加入逆向映射,然后检查是否可以释放交换区中对应的槽位。

如果该页是以读/写模式访问,内核必须用过调用d0_wp_page来结束操作,这将创建该页的一个副本,并将其添加到导致异常的进程的页表中,将原始页的使用计数器减1.

 

本篇只简述了交换区和交换缓存的相关概念及操作流程,对于具体的数据结构和函数实现未作分析,内容参考《深入Linux内核架构》及linux swap与zram详解

posted @ 2019-03-17 11:17  ccxikka  Views(1231)  Comments(0Edit  收藏  举报