上一篇了解了cache,tlb,页缓存和mmap,这篇则主要关注交换缓存和交换区。前面几种缓存都是为了系统能更快地读取数据:页缓存将文件数据缓存至内存中减小磁盘io, tlb缓存页表数据便于地址翻译找到物理页面,cache则将物理页面中的数据进行缓存便于CPU读取。但要知足用户的需求,或者一直知足内存密集型应用程序的需求,不管计算机上可用的物理内存有多少都是不够的,所以内核须要将不多使用的部份内存换出到块设备以提供更多的主内存,这种机制为页交换或换页,交换区和交换缓存则是和页交换相关的。linux
可换出页算法
只有少许几种页能够换出到交换区,对其余页来讲换出到块设备与之对应的后备存储器便可。若是一个不多使用的页的后备存储器是一个块设备(如文件),那么就无需换出被修改的页,而是直接与块设备同步,腾出的页帧能够重用,若是再次须要该数据,能够历来源从新创建该页。若是页的后备存储器是一个文件可是不能在内存中修改,那么在不须要的状况下可直接丢弃该页,而须要交换到交换区的为如下几种:数组
须要注意的是,由内核自己使用的内存页毫不会换出,用于将外设映射到内存空间的页也不能换出。缓存
文件页和匿名页:page->mapping末位为0时,说明为文件映射页,mapping指向对应文件的address_space;page->mapping末位为1时,指向anon_vma(包含了1至多个vma),说明为匿名映射页。在内存回收时,匿名页将会被交换到交换区而保存起来。交换以后页将被释放。匿名页没有后备存储,所以须要将其写入交换区。交换区用来为匿名页提供备份,匿名页能够分为三类:属于进程匿名线性区(如用户态堆栈、堆)的页;属于进程私有内存映射的脏页;属于IPC共享内存的页。安全
交换区的组织数据结构
换出的页或者保存在一个没有文件系统的专用分区中,或者存储在某个现有文件系统中的一个定长文件中,能够同时使用几个这样的区域,还能够根据各个交换区的速度不一样,为其指定优先级。内核使用交换区时能够根据优先级进行选择。每一个交换区都细分为若干连续的槽,每一个槽的长度恰好与系统的一个页帧相同。架构
本质上,系统中的任何一页均可以容纳到交换区的任一槽中,此外内核还使用了一种汇集构造法,使得可以尽快访问交换区。进程内存区中连续的页将按照特定的汇集大小逐一写到硬盘上,若是交换区中没有更多空间可容纳此长度的汇集,内核可使用其余任何位置上的空闲槽位。并发
若是内核使用了几个优先级相同的交换区,内核将使用一种循环进程来确保尽量均匀地利用各个交换区。若是交换区的优先级不一样,内核首先使用高优先级的交换区,而后逐渐转移到优先级较低的交换区。内核使用位图用于跟踪交换区中各槽位的使用/空闲状态。app
有两个用户空间工具可用于建立和启用交换区,分别是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中。
交换缓存
交换缓存在选择换出页的操做和实际执行页交换的机制之间充当协调者,即在页面选择策略和用于在内存和交换区之间传输数据的机制之间,交换缓存充当代理人的角色,这两个部分经过交换缓存交互。交换缓存用于如下目的,具体取决于页交换请求的方向(读入内存或写入交换区):
换出页在页表中经过一种专门的页表项来标记,其中会存储: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则用来存放交换缓存中的页数。
添加新页
可以使用下面两个内核方法向交换缓存中添加页:
使用函数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将其从通常的页缓存删除)
页面回收
页面回收在两个地方触发:
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详解