上一篇了解了cache,tlb,页缓存和mmap,这篇则主要关注交换缓存和交换区。前面几种缓存都是为了系统能更快地读取数据:页缓存将文件数据缓存至内存中减少磁盘io, tlb缓存页表数据便于地址翻译找到物理页面,cache则将物理页面中的数据进行缓存便于CPU读取。但要满足用户的需求,或者一直满足内存密集型应用程序的需求,无论计算机上可用的物理内存有多少都是不够的,因此内核需要将很少使用的部分内存换出到块设备以提供更多的主内存,这种机制为页交换或换页,交换区和交换缓存则是和页交换相关的。
可换出页
只有少量几种页可以换出到交换区,对其他页来说换出到块设备与之对于的后备存储器即可。如果一个很少使用的页的后备存储器是一个块设备(如文件),那么就无需换出被修改的页,而是直接与块设备同步,腾出的页帧可以重用,如果再次需要该数据,可以从来源重新建立该页。如果页的后备存储器是一个文件但是不能在内存中修改,那么在不需要的情况下可直接丢弃该页,而需要交换到交换区的为以下几种:
需要注意的是,由内核本身使用的内存页绝不会换出,用于将外设映射到内存空间的页也不能换出。
交换区的组织
换出的页或者保存在一个没有文件系统的专用分区中,或者存储在某个现有文件系统中的一个定长文件中,可以同时使用几个这样的区域,还可以根据各个交换区的速度不同,为其指定优先级。内核使用交换区时可以根据优先级进行选择。每个交换区都细分为若干连续的槽,每个槽的长度刚好与系统的一个页帧相同。
本质上,系统中的任何一页都可以容纳到交换区的任一槽中,此外内核还使用了一种聚集构造法,使得能够尽快访问交换区。进程内存区中连续的页将按照特定的聚集大小逐一写到硬盘上,如果交换区中没有更多空间可容纳此长度的聚集,内核可以使用其他任何位置上的空闲槽位。
如果内核使用了几个优先级相同的交换区,内核将使用一种循环进程来确保尽可能均匀地利用各个交换区。如果交换区的优先级不同,内核首先使用高优先级的交换区,然后逐渐转移到优先级较低的交换区。内核使用位图用于跟踪交换区中各槽位的使用/空闲状态。
有两个用户空间工具可用于创建和启用交换区,分别是mkswap(用于格式化一个交换分区/文件)和swapon(用于启用一个交换区)
交换缓存
交换缓存在选择换出页的操作和实际执行页交换的机制之间充当协调者,即在页面选择策略和用于在内存和交换区之间传输数据的机制之间,交换缓存充当代理人的角色,这两个部分通过交换缓存交互。交换缓存用于以下目的,具体取决于页交换请求的方向(读入内存或写入交换区):
换出页在页表中通过一种专门的页表项来标记,其中会存储: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用于将页标记为脏。
添加新页
可使用下面两个内核方法向交换缓存中添加页:
使用函数get_swap_page在交换区中分配槽位,之后将需要换出的page实例设置PG_swapcache标志并将交换标识符swap_entry_t保存早page的private成员中,在页的内容实际换出时还需构造一个体系结构相关的页表项,然后将全局变量total_swapcache_pages加1来更新统计信息,还需将页插入到由swapper_space建立的基数树。最后,SetPageUpdate和SetPageDirty修改页的标志,因为页的内容尚未包含在交换区,对于交换页来说,对于的底层块设备是交换区,因而同步(几乎)就等价于页换出,将数据从内存传输到交换区是由与swapper_space关联的特定于地址空间的操作完成的,然后更新页表。
数据回写
数据回写由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将其从一般的页缓存删除)
页面回收
页面回收在两个地方触发:
处理交换缺页异常
访问换出页导致的缺页异常,由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内核架构》
原文:https://www.cnblogs.com/ccxikka/p/10544781.html