0
点赞
收藏
分享

微信扫一扫

Linux内核机制总结内存管理之页回收(二十三)

爱奔跑的读书者 2022-03-12 阅读 55
linux

文章目录

  • 重要:本系列文章内容摘自<Linux内核深度解析>基于ARM64架构的Linux4.x内核一书,作者余华兵。系列文章主要用于记录Linux内核的大部分机制及参数的总结说明

1 页回收

申请分配页的时候,页分配器首先尝试使用低水线分配页。如果使用低水线分配失败,说明内存轻微不足,页分配器将会唤醒内存节点的页回收内核线程,异步回收页,然后尝试使用最低水线分配页。如果使用最低水线分配失败,说明内存严重不足,页分配器将会直接回收页。

物理页根据是否有存储设备支持分为两类:
(1)交换支持的页:没有存储设备支持的物理页,包括匿名页,以及tmpfs文件系统(内存中的文件系统)的文件页和进程在修改私有的文件映射时复制生成的匿名页。
(2)存储设备支持的文件页。

针对不同的物理页,采用不同的回收策略:
(1)交换支持的页:采用页交换的方法,先把页的数据写到交换区,然后释放物理页。
(2)存储设备支持的文件页:如果是干净的页,即把文件从存储设备读到内存以后没有修改过,可以直接释放;如果是脏页,即把文件从存储设备读到内存以后修改过,那么先写回到存储设备,然后释放物理页。

页回收算法还会回收slab缓存。使用专用slab缓存的内核模块可以使用函数register_shrinker注册收缩器,页回收算法调用所有收缩器的函数以释放对象。

根据什么原则选择回收的物理页?内核使用LRU(Least Recently Used,最近最少使用)算法选择最近最少使用的物理页。

回收物理页的时候,如果物理页被映射到进程的虚拟地址空间,那么需要从页表中删除虚拟页到物理页的映射。怎么知道物理页被映射到哪些虚拟页?需要通过反向映射的数据结构,虚拟页映射到物理页是正向映射,物理页映射到虚拟页是反向映射。

1.1 数据结构

1.1.1 LRU链表

页回收算法使用LRU算法选择回收的页。如下所示,每个内存节点的pglist_data实例有一个成员lruvec,称为LRU向量,LRU向量包含5条LRU链表:
在这里插入图片描述
(1)不活动匿名页LRU链表,用来链接不活动的匿名页,即最近访问频率低的匿名页。
(2)活动匿名页LRU链表,用来链接活动的匿名页,即最近访问频率高的匿名页。
(3)不活动文件页LRU链表,用来链接不活动的文件页,即最近访问频率低的文件页。
(4)活动文件页LRU链表,用来链接活动的文件页,即最近访问频率高的文件页。
(5)不可回收LRU链表,用来链接使用mlock锁定在内存中、不允许回收的物理页。

在LRU链表中,物理页的页描述符的特征如下:
(1)页描述符设置PG_lru标志位,表示物理页在LRU链表中。
(2)页描述符通过成员lru加入LRU链表。
(3)如果是交换支持的物理页,页描述符会设置PG_swapbacked标志位。
(4)如果是活动的物理页,页描述符会设置PG_active标志位。
(5)如果是不可回收的物理页,页描述符会设置PG_unevictable标志位。
每条LRU链表中的物理页按访问时间从大到小排序,链表首部的物理页的访问时间离当前最近,物理页从LRU链表的首部加入,页回收算法从不活动LRU链表的尾部取物理页回收,从活动LRU链表的尾部取物理页并移动到不活动LRU链表中。

怎么确定页的活动程度?确定方法如下:
(1)如果是页表映射的匿名页或文件页,根据页表项中的访问标志位确定页的活动程度。当处理器的内存管理单元把虚拟地址转换成物理地址的时候,如果页表项没有设置访问标志位,就会生成页错误异常。页错误异常处理程序为页表项设置访问标志位,如下所示,函数pte_mkyoung负责为页表项设置访问标志位:
在这里插入图片描述
(2)如果是没有页表映射的文件页,进程通过系统调用read或write访问文件,文件系统在文件的页缓存中查找文件页,为文件页的页描述符设置访问标志位(PG_referenced)。如下所示,进程读EXT4文件系统中的一个文件,函数mark_page_accessed为文件页的页描述符设置访问标志位:
在这里插入图片描述

1.1.2 反向映射

回收页表映射的匿名页或文件页时,需要从页表中删除映射,内核需要知道物理页被映射到哪些进程的虚拟地址空间,需要实现物理页到虚拟页的反向映射。

页描述符中和反向映射相关的成员如下:

include/linux/mm_types.h   
struct page {union {         struct address_space  *mapping;};

     union {         pgoff_t  index;      /* 在映射里面的偏移 */};
     union {struct {
              union {                  atomic_t  _mapcount;};};
     };}

成员mapping
利用指针总是4的整数倍这个特性,成员mapping的最低两位用来作为页映射标志,最低位PAGE_MAPPING_ANON表示匿名页。

如果物理页是匿名页,page.mapping =(结构体anon_vma的地址 | PAGE_MAPPING_ANON)。

如果物理页是文件页,page.mapping指向结构体address_space。

成员index
成员index是在映射里面的偏移,单位是页。如果是匿名映射,那么index是物理页对应的虚拟页在虚拟内存区域中的页偏移;如果是文件映射,那么index是物理页存储的数据在文件中的页偏移。

成员_mapcount
成员_mapcount是映射计数,反映物理页被映射到多少个虚拟内存区域。初始值是−1,加上1以后才是真实的映射计数,建议使用内联函数page_mapcount获取页的映射计数。

(1)匿名页的反向映射。
匿名页的反向映射的数据结构如下所示:
在这里插入图片描述
在这里插入图片描述
1)结构体page的成员mapping指向一个anon_vma实例,并且设置了PAGE_MAPPING_ANON标志位。
2)结构体anon_vma用来组织匿名页被映射到的所有虚拟内存区域。
3)结构体anon_vma_chain充当中介,关联anon_vma实例和vm_area_struct实例。
4)一个匿名页可能被映射到多个虚拟内存区域,anon_vma实例通过中介anon_vma_chain把所有vm_area_struct实例放在区间树中,区间树是用红黑树实现的,anon_vma实例的成员rb_root指向区间树的根,中介anon_vma_chain的成员rb是红黑树的节点。
5)一个虚拟内存区域可能关联多个anon_vma实例,即父进程的anon_vma实例和当前进程的anon_vma实例。vm_area_struct实例通过中介anon_vma_chain把所有anon_vma实例放在一条链表中,成员anon_vma_chain是链表的头节点,中介anon_vma_chain的成员same_vma是链表节点。

查询一个匿名页被映射到的所有虚拟页的过程如下:
1)根据页描述符的成员mapping得到结构体anon_vma。
2)根据结构体anon_vma的成员rb_root得到区间树的根。
3)通过遍历区间树可以得到物理页被映射到的所有虚拟内存区域,从anon_vma_chain实例的成员vma得到vm_area_struct实例。
4)根据vm_area_struct实例的成员vm_start得到虚拟内存区域的起始地址,根据页描述符的成员index得到虚拟页在虚拟内存区域中的页偏移,将两者相加得到虚拟页的起始地址。
5)根据vm_area_struct实例的成员vm_mm得到进程的内存描述符,根据内存描述符的成员pgd得到页全局目录的起始地址。

从一个进程分叉生成子进程的时候,子进程把父进程的虚拟内存完全复制一份,如下所示,子进程把父进程的每个vm_area_struct实例复制一份,对每个vm_area_struct实例执行下面的操作:
在这里插入图片描述
在这里插入图片描述
1)通过anon_vma_chain实例加入父进程的anon_vma实例的区间树中。
2)创建自己的anon_vma实例,把vm_area_struct实例加入anon_vma实例的区间树中。
3)vm_area_struct实例通过anon_vma_chain把父进程的anon_vma实例和自己的anon_vma实例放在一条双向链表中。
4)父子进程的anon_vma实例组成一棵树:子进程的anon_vma实例的成员parent指向父进程的anon_vma实例,成员root指向这棵树的根。

(2)文件页的反向映射。
文件页的反向映射的数据结构如下所示:
在这里插入图片描述
1)存储设备上的文件系统有一个描述文件系统信息的超级块,挂载文件系统时在内存中创建一个超级块的副本,即super_block实例。
2)文件系统中的每个文件有一个描述文件属性的索引节点,读文件时在内存中创建一个索引节点的副本,即inode实例,成员i_mapping指向一个地址空间结构体address_space。
3)打开文件时,在内存中创建一个文件打开实例file,成员f_mapping继承inode实例的成员i_mapping。
4)读文件时,分配物理页,页描述符的成员mapping继承file实例的成员i_mapping,成员index是物理页存储的数据在文件中的偏移,单位是页。
5)每个文件有一个地址空间结构体address_space,用来建立数据缓存(在内存中为某种数据创建的缓存)和数据来源(即存储设备)之间的关联。地址空间结构体address_space的成员i_mmap指向区间树,区间树是使用红黑树实现的,用来把文件区间映射到虚拟内存区域,索引是虚拟内存区域对应的文件页偏移(vm_area_struct.vm_pgoff)。

查询一个文件页被映射到的所有虚拟页的过程如下:
1)根据页描述符的成员mapping得到地址空间结构体address_space。
2)根据地址空间结构体address_space的成员i_mmap得到区间树的根。
3)遍历区间树,虚拟内存区域对应的文件区间是[成员vm_pgoff,成员vm_pgoff + 虚拟内存区域的页数−1],页描述符的成员index是物理页存储的数据在文件中的页偏移。如果页描述符的成员index属于虚拟内存区域对应的文件区间,就说明文件页被映射到这个虚拟内存区域中的虚拟页。
4)文件页被映射到的虚拟页的起始地址是“虚拟内存区域的成员vm_start+(页描述符的成员index−虚拟内存区域的成员vm_pgoff)×页长度”。
5)根据vm_area_struct实例的成员vm_mm得到进程的内存描述符,根据内存描述符的成员pgd得到页全局目录的起始地址。

如下所示,对于私有的文件映射,在写的时候生成页错误异常,页错误异常处理程序执行写时复制,新的物理页和文件脱离关系,属于匿名页:
在这里插入图片描述

1.2 发起回收

如下所示,申请分配页的时候,页分配器首先尝试使用低水线分配页。如果使用低水线分配失败,说明内存轻微不足,页分配器将会唤醒所有符合分配条件的内存节点的页回收线程,异步回收页,然后尝试使用最低水线分配页。如果分配失败,说明内存严重不足,页分配器将会直接回收页。如果直接回收页失败,那么判断是否应该重新尝试回收页。
在这里插入图片描述
1.异步回收
每个内存节点有一个页回收线程,执行流程如下所示。如果内存节点的所有内存区域的空闲页数小于高水线,页回收线程就会反复尝试回收页,调用函数shrink_node以回收内存节点中的页。
在这里插入图片描述
2.直接回收
直接回收页的执行流程如下所示,针对备用区域列表中符合分配条件的每个内存区域,调用函数shrink_node来回收内存区域所属的内存节点中的页。
在这里插入图片描述
回收页是以内存节点为单位执行的,函数shrink_node负责回收内存节点中的页,执行流程如下所示:
在这里插入图片描述
(1)回收内存节点中的页。
1)调用函数get_scan_count,计算需要扫描多少个不活动匿名页、活动匿名页、不活动文件页和活动文件页。
2)依次扫描不活动匿名页、活动匿名页、不活动文件页和活动文件页4条LRU链表,针对每条LRU链表,处理如下:

如果是活动LRU链表,并且不活动页比较少,那么调用函数shrink_active_list,把一部分活动页转移到不活动链表中。

如果是不活动LRU链表,那么调用函数shrink_inactive_list以回收不活动页。

(2)调用函数shrink_slab以回收slab缓存。
函数balance_pgdat和try_to_free_pages使用结构体scan_control控制扫描操作,这个结构体不仅用于高层函数向低层函数传递控制指令,也用于反向传递结果,主要成员如下所示:

成  员说  明
unsigned long nr_to_reclaim应该回收多少页,执行直接页回收的时候取32页
gfp_t gfp_mask申请分配页的掩码。调用者申请页时可能不允许向下调用到底层文件系统,或者不允许读写存储设备,需要把这些约束传给页回收算法
enum zone_type reclaim_idx回收的最高内存区域
int order申请分配页的阶数。比如调用者正在申请n阶页,希望页回收算法能满足申请n阶页的需求
nodemask_t *nodemask调用者允许扫描的内存节点的掩码,如果是空指针,表示扫描所有内存节点
int priority扫描优先级,一次扫描的页数是(LRU链表的总页数 >> 扫描优先级),初始值是12
unsigned int may_writepage:1是否允许把修改过的文件页回写到存储设备
unsigned int may_unmap:1是否允许回收页表映射的物理页
unsigned int may_swap:1是否允许把匿名页换出到交换区
unsigned long nr_scanned用来报告扫描过的不活动页的数量
unsigned long nr_reclaimed用来报告回收了多少页

3.判断是否应该重试回收页
函数should_reclaim_retry判断是否应该重试回收页,如果直接回收16次全都失败,或者即使回收所有可回收的页,也还是无法满足水线,那么应该放弃重试回收。

1.3 计算扫描的页数

页回收算法每次扫描多少页?扫描多少个匿名页和多少个文件页,怎么分配匿名页和文件页的比例?
扫描优先级用来控制一次扫描的页数,如果扫描优先级是n,那么一次扫描的页数是(LRU链表中的总页数 >> n),可以看出:“扫描优先级的值越小,扫描的页越多”。页回收算法从默认优先级12开始,如果回收的页数没有达到目标,那么提高扫描优先级,把扫描优先级的值减1,然后继续扫描。扫描优先级的最小值为0,表示扫描LRU链表中的所有页。

两个参数用来控制扫描的匿名页和文件页的比例:
(1)参数“swappiness”控制换出匿名页的积极程度,取值范围是 0~100,值越大表示匿名页的比例越高,默认值是60。可以通过文件“/proc/sys/vm/swappiness”配置换出匿名页的积极程度。
(2)针对匿名页和文件页分别统计最近扫描的页数和从不活动变为活动的页数,计算比例(从不活动变为活动的页数 / 最近扫描的页数)。如果匿名页的比例值比较大,说明匿名页的活动程度高,文件页的活动程度低,那么应该降低扫描的匿名页所占的比例,提高扫描的文件页所占的比例。

函数get_scan_count针对不活动匿名页、活动匿名页、不活动文件页和活动文件页4条LRU链表,计算每条LRU链表需要扫描的页数,其算法如下:

anon_prio = swappiness
file_prio = 200 - anon_prio

ap = anon_prio / (reclaim_stat->recent_rotated[0] / reclaim_stat->recent_scanned[0])
fp = file_prio / (reclaim_stat->recent_rotated[1] / reclaim_stat->recent_scanned[1])

size = LRU链表中内存区域类型小于或等于回收的最高区域类型的总页数
scan = size >> 扫描优先级
如果是匿名页,scan = scan * ap / (ap + fp)
如果是文件页,scan = scan * fp / (ap + fp)

其中,reclaim_stat->recent_scanned[0]是最近扫描过的匿名页的数量,reclaim_stat-> recent_rotated[0]是从不活动变为活动的匿名页的数量;reclaim_stat->recent_scanned[1]是最近扫描过的文件页的数量,reclaim_stat->recent_rotated[1]是从不活动变为活动的文件页的数量。

1.4 收缩活动页链表

当不活动页比较少的时候,页回收算法收缩活动页链表,也就是从活动页链表的尾部取物理页并转移到不活动页链表中,把活动页转换成不活动页。

函数inactive_list_is_low判断不活动页是不是比较少,其算法如下:

inactive = 不活动页链表中内存区域类型小于或等于回收的最高区域类型的总页数active = 活动页链表中内存区域类型小于或等于回收的最高区域类型的总页数gb = 把(inactive + active)从页数转换成字节数,单位是GB。如果gb大于0
在这里插入图片描述
如果(inactive * inactive_ratio < active),说明不活动页比较少。

函数shrink_active_list负责从活动页链表中转移物理页到不活动页链表中,有4个参数:
(1)unsigned long nr_to_scan:指定扫描的页数。
(2)struct lruvec *lruvec:LRU向量的地址。
(3)struct scan_control *sc:扫描控制结构体。
(4)enum lru_list lru:LRU链表的索引,取值是LRU_ACTIVE_ANON(活动匿名页LRU链表)或LRU_ACTIVE_FILE(活动文件页LRU链表)。

函数shrink_active_list的执行流程如下所示:
在这里插入图片描述
(1)调用函数isolate_lru_pages,从活动页链表的尾部取指定页数添加到临时链表l_hold中,清除页的LRU标志位。页所属的内存区域必须小于或等于回收的最高区域。
(2)针对临时链表l_hold中的每个页,处理如下:

1)调用函数page_referenced来判断页最近是否被访问过。
2)如果页最近被访问过,并且是程序的代码段所在的物理页,那么保留在活动页链表中,添加到临时的活动页链表l_active中。
3)将活动页转换成不活动页,清除页的活动标志。
4)添加到临时的不活动页链表l_inactive中。

(3)有些活动页保留在活动页链表中,把临时的活动页链表l_active中的页添加到活动页链表的首部。
(4)将有些活动页转换成不活动页,把临时的不活动页链表l_inactive中的页添加到不活动页链表的首部。
(5)调用函数free_hot_cold_page_list释放引用计数变为0的页,作为缓存冷页(即页的数据不在处理器的缓存中)释放。在回收的过程中,在其他地方可能已经释放了页,当页回收算法把页的引用计数减1的时候,发现引用计数变成0,直接释放页。

将活动页转换成不活动页的规则如下:
(1)对有执行权限并且有存储设备支持的文件页(就是程序的代码段所在的物理页)做了特殊处理:如果页表项设置了访问标志位,那么保留在活动页链表中;如果页表项没有设置访问标志位,那么转移到不活动页链表中。
(2)如果是匿名页或其他类型的文件页,转移到不活动页链表中。

为什么对代码段的物理页做特殊处理呢?可能是因为考虑到有些共享库,比如C标准库,被很多进程链接,如果把这些共享库的代码段的物理页回收了,影响很大,每个进程执行时都会生成页错误异常,重新把共享库的虚拟页映射到物理页。

1.5 回收不活动页

函数shrink_inactive_list负责回收不活动页,有4个参数:
(1)unsigned long nr_to_scan:指定扫描的页数。
(2)struct lruvec *lruvec:LRU向量的地址。
(3)struct scan_control *sc:扫描控制结构体。
(4)enum lru_list lru:LRU链表的索引,取值是LRU_INACTIVE_ANON(不活动匿名页LRU链表)或LRU_INACTIVE_FILE(不活动文件页LRU链表)。

函数shrink_inactive_list的执行流程如下所示:
在这里插入图片描述
(1)调用函数isolate_lru_pages,从不活动页链表的尾部取指定页数添加到临时链表page_list中。
(2)调用函数shrink_page_list来处理临时链表page_list中的所有页。
(3)有些不活动页可能被转换成活动页,有些不活动页可能保留在不活动页链表中,调用函数putback_inactive_pages,把这些不活动页放回到对应的链表中。
(4)调用函数free_hot_cold_page_list释放引用计数变为0的页,作为缓存冷页释放。

回收不活动页的主要工作是由函数shrink_page_list实现的,执行流程如下所示:
在这里插入图片描述
(1)针对临时链表page_list中的每个页,执行下面的操作:

1)调用函数page_check_references,检查页最近是否被访问过,返回处理方式。
2)如果处理方式是转换成活动页,那么设置活动标志,添加到临时链表ret_pages中。
3)如果处理方式是保留在不活动页链表中,那么添加到临时链表ret_pages中。
4)如果处理方式是回收,执行下面的操作。

如果是匿名页,调用函数add_to_swap以添加到交换缓存中。

如果是页表映射的页,调用函数try_to_unmap,从页表中删除映射,通过反向映射的数据结构可以知道物理页被映射到哪些虚拟内存区域。

如果是脏页,调用函数pageout,把文件页写回到存储设备,或者把匿名页写回到交换区。

把页从交换缓存或页缓存中删除:如果是交换支持的页,从交换缓存中删除;如果是文件页,从文件的页缓存中删除。

把页添加到临时链表free_pages中。

(2)释放临时链表free_pages中的页。
(3)临时链表ret_pages存放转换成活动页或保留在不活动页链表中的不活动页,把临时链表ret_pages中的页转移到临时链表page_list中返回。
不活动页转换成活动页的情况如下:

(1)页表映射的页。
1)交换支持的页,如果页表项设置了访问标志位,那么将不活动页转换成活动页。
2)有存储设备支持的文件页,采用两次机会算法:如果页回收算法连续两次选中一个不活动页,并且每次不活动页最近被访问过,那么将不活动页转换成活动页。
3)对程序的代码段所在的页做了特殊处理:如果页表项设置了访问标志位,那么将不活动页转换成活动页。

(2)没有页表映射的文件页。
采用两次机会算法:进程第一次访问时,如果页描述符没有设置访问标志位,那么设置访问标志位;进程第二次访问时,发现页描述符设置了访问标志位,将不活动页转换成活动页。
不活动页保留在不活动页链表中或者回收的情况如下。
(1)页表映射的不活动页。
1)如果页表项设置了访问标志位,那么页保留在不活动页链表中,清除页表项的访问标志位,给页描述符设置访问标志位。
2)如果页表项没有设置访问标志位,根据页的类型处理。

如果是交换支持的页,立即回收。

存储设备支持的文件页:如果页描述符设置了访问标志位,那么只回收干净的页(页的数据自从读到内存中没有被修改),脏页(页的数据自从读到内存中被修改过)保留在不活动页链表中;如果页描述符没有设置访问标志位,那么立即回收。

(2)没有页表映射的文件页。
1)如果页描述符设置了访问标志位,那么只回收干净的页,脏页保留在不活动页链表中。
2)如果页描述符没有设置访问标志位,那么立即回收。

1.6 页交换

页交换(swap)的原理是:当内存不足的时候,把最近很少访问的没有存储设备支持的物理页的数据暂时保存到交换区,释放内存空间,当交换区中存储的页被访问的时候,再把数据从交换区读到内存中。

交换区可以是一个磁盘分区,也可以是存储设备上的一个文件。

1.使用方法
编译内核时需要开启配置宏CONFIG_SWAP,默认开启。

使用磁盘分区作为交换区的配置方法如下:
(1)使用fdisk命令(例如fdisk /dev/sda)创建磁盘分区,在fdisk中用“t”命令把分区类型修改为十六进制的数值82(Linux交换分区的类型),最后用“w”命令保存fdisk操作。
(2)使用命令“mkswap”格式化交换分区,命令格式是“mkswap [options] device [size]”。
例如:假设交换分区是“/dev/sda1”,执行命令“mkswap /dev/sda1”以进行格式化。
(3)使用命令“swapon”启用交换区,命令格式是“swapon [options] specialfile”。
例如:假设交换分区是“/dev/sda1”,执行命令“swapon /dev/sda1”启用交换区。

使用文件作为交换区的配置方法如下:
(1)使用dd命令创建文件。
例如:创建文件“/root/swap”,块长度是1MB,块的数量是2048,文件的长度是2048MB。
dd if=/dev/zero of=/root/swap bs=1M count=2048
(2)使用命令“mkswap”格式化文件。
例如:mkswap /root/swap
(3)使用命令“swapon”启用交换区。
例如:swapon /root/swap

在内存比较小的设备上,可以使用ZRAM设备作为交换区。ZRAM是基于内存的块设备,写到ZRAM设备的页被压缩后存储在内存中,可以节省内存空间,相当于扩大内存容量。编译内核时需要开启配置宏CONFIG_ZRAM。配置方法如下:

(1)如果把ZRAM编译成内核模块,可以使用命令“modprobe”加载模块,参数“num_devices”用来指定创建多少个ZRAM设备,默认值是1。
modprobe zram num_devices=4
“num_devices=4”表示创建4个ZRAM设备,设备名称是“/dev/zram{0,1,2,3}”。
(2)指定ZRAM设备的容量,建议为总内存的10%~25%。如果ZRAM设备的容量是zram_size,物理页的长度是page_size,那么ZRAM设备最多可以把(zram_size/page_size)个物理页的数据压缩后存储在内存中。
假设把ZRAM0设备的容量设置为512MB:echo 512M > /sys/block/zram0/disksize
(3)格式化ZRAM设备。
假设格式化ZRAM0设备:mkswap /dev/zram0
(4)启用交换区。
假设启用ZRAM0设备:swapon /dev/zram0

如果配置了多个交换区,可以使用命令“swapon”的选项“-p priority”指定交换区的优先级,取值范围是[0,32767],值越大表示优先级越高。

可以把交换区添加到文件“/etc/fstab”中,然后执行命令“swapon -a”来启用文件“/etc/fstab”中的所有交换区。

可以使用命令“swapoff”禁用交换区。

可以执行命令“cat /proc/swaps”或“swapon -s”来查看交换区。

目前常用的存储设备是:机械硬盘、固态硬盘和NAND闪存。

固态硬盘使用NAND闪存作为存储介质,固态硬盘中的控制器运行闪存转换层固化程序,把闪存转换成块设备,使固态硬盘对外表现为块设备。

NAND闪存的特点是:写入数据之前需要把擦除块擦除,每个擦除块的擦除次数有限,范围是105~106,频繁地写数据会缩短闪存的寿命。

所以,如果设备使用固态硬盘或NAND闪存存储数据,不适合启用交换区;如果设备使用机械硬盘存储数据,可以启用交换区。

交换区的缺点是读写速度慢,影响程序的执行性能,为了缓解这个问题,内核3.11版本引入了zswap,它是交换页的轻量级压缩缓存,目前还是实验特性。zswap把准备换出的页压缩到动态分配的内存池,仅当压缩缓存的大小达到限制时才会把压缩缓存里面的页写到交换区。zswap以消耗处理器周期为代价,大幅度减少读写交换区的次数,带来了重大的性能提升,因为解压缩比从交换区读更快。

编译内核时需要开启配置宏CONFIG_ZSWAP。zswap默认是禁止的,可以使用模块参数“enabled”启用zswap:在引导内核时使用内核参数“zswap.enabled=1”,或者在运行时执行“echo 1 > /sys/module/zswap/parameters/enabled”。

可以使用模块参数“max_pool_percent”设置压缩缓存占用内存的最大百分比,默认值是20。例如:把压缩缓存占用内存的最大百分比设置成25%,可以在引导内核时使用内核参数“zswap.max_pool_percent=25”,或者在运行时执行“echo 25 > /sys/module/zswap/ parameters/max_pool_percent”。

2.技术原理
(1)数据结构。
1)交换区格式。
交换区的第一页是交换区首部,内核使用数据结构swap_header描述交换区首部:

include/linux/swap.h
union swap_header {
    struct {
         char reserved[PAGE_SIZE - 10];
         char magic[10];             /* SWAP-SPACE或者SWAPSPACE2 */
    } magic;
    struct {
         char     bootbits[1024];    /* 存放磁盘标签和其他空间 */
         __u32         version;
         __u32         last_page;
         __u32         nr_badpages;
         unsigned char sws_uuid[16];
         unsigned char sws_volume[16];
         __u32         padding[117];
         __u32         badpages[1];
    } info;
};
  • 前面1024字节空闲,为引导程序预留空间,这种做法使得交换区可以处在磁盘的起始位置。
  • 成员version是交换区的版本号。
  • 成员last_page是最后一页的页号。
  • 成员nr_badpages是坏页的数量,从成员badpages的位置开始存放坏页的页号。
  • 最后10字节是魔幻数,用来区分交换区格式,内核已经不支持旧的格式“SWAP- SPACE”,只支持格式“SWAPSPACE2”。

2)交换区信息。
内核定义了交换区信息数组swap_info,每个数组项存储一个交换区的信息。数组项的数量是在编译时由宏MAX_SWAPFILES指定的,通常是32,说明最多可以启用32个交换区:

mm/swap_file.c
struct swap_info_struct *swap_info[MAX_SWAPFILES];

交换区分为多个连续的槽(slot),每个槽位的长度等于页的长度。

聚集(cluster)由32(宏SWAPFILE_CLUSTER)个连续槽位组成,通过按顺序分配槽位把换出的页聚集在一起,避免分散到整个交换区。聚集带来的好处是可以把连续槽位按顺序写到存储设备上,对于机械硬盘,可以减少磁头寻找磁道的时间,提高写的性能。

交换区按优先级从高到低排序,首先从优先级高的交换区分配槽位。对于优先级相同的交换区,轮流从每个交换区分配槽位,每次从交换区分配槽位后,把交换区移到优先级相同的交换区的最后面。

结构体swap_info_struct描述交换区的信息,主要成员如下所示:

成  员说  明
unsigned long flags标志位。常用的标志位如下:(1)SWP_USED表示当前数组项处于使用状态(2)SWP_WRITEOK表示交换区可写,禁用交换区以后交换区不可写(3)SWP_BLKDEV表示交换区是块设备,即磁盘分区
signed short prio优先级
struct plist_node list用来加入有效交换区链表,按优先级从高到低排序,头节点是swap_active_head。启用交换区以后交换区是有效的,禁用交换区以后交换区不再是有效的
struct plist_node avail_list用来加入可用交换区链表,按优先级从高到低排序,头节点是swap_avail_head。可用交换区是指有效的并且没有满的交换区
signed char type交换区的索引
unsigned int max交换区的最大页数,和成员pages不同的是:max不仅包括可用槽位,也包括损坏的或用于管理目的的槽位。硬盘出现坏块的情况很少见,所以max通常等于pages加1
unsigned int pages交换区可用槽位的总数
unsigned int inuse_pages交换区正在使用的页的数量
unsigned char *swap_map交换映射,指向一个数组,每字节对应交换区中的每个槽位,低6位存储每个槽位的使用计数,也就是共享换出页的进程的数量;高2位是标志位,SWAP_HAS_CACHE表示页在交换缓存中
unsigned int lowest_bit数组swap_map中第一个空闲数组项的索引
unsigned int highest_bit数组swap_map中最后一个空闲数组项的索引
struct block_device *bdev指向块设备。如果交换区是磁盘分区,bdev指向磁盘分区对应的块设备;如果交换区是文件,bdev指向文件所在的块设备
struct file *swap_file指向交换区关联的文件的打开实例。如果交换区是磁盘分区,swap_file指向磁盘分区对应的块设备文件的打开实例;如果交换区是文件,swap_file指向文件的打开实例
unsigned int cluster_next当前聚集中下一次分配的槽位的索引
unsigned int cluster_nr当前聚集中可用的槽位数量
struct swap_extent first_swap_extent交换区间链表

3)交换区间。
交换区间(swap extent)用来把交换区的连续槽位映射到连续的磁盘块。如果交换区是磁盘分区,因为磁盘分区的块是连续的,所以只需要一个交换区间。如果交换区是文件,因为文件对应的磁盘块不一定是连续的,所以对于每个连续的磁盘块范围,需要使用一个交换区间来存储交换区的连续槽位和磁盘块范围的映射关系。

如下所示,交换区信息的成员first_swap_extent存储第一个交换区间的信息,交换区间的成员start_page是起始槽位的页号,成员nr_pages是槽位的数量,成员start_block是起始磁盘块号,成员list用来链接同一个交换区的所有交换区间。
在这里插入图片描述
4)交换槽位缓存。
为了加快为换出页分配交换槽位的速度,每个处理器有一个交换槽位缓存swp_slots,数据结构如下所示:
在这里插入图片描述

  • 成员slots指向交换槽位数组,数组的大小是宏SWAP_SLOTS_CACHE_SIZE,即64。
  • 成员nr是空闲槽位的数量。
  • 成员cur是当前已分配的槽位数量,也是下次分配的数组索引。
  • 成员alloc_lock用来保护slots、nr和cur三个成员。

为换出页分配交换槽位的时候,首先从当前处理器的交换槽位缓存分配,如果交换槽位缓存没有空闲槽位,那么从交换区分配槽位以重新填充交换槽位缓存。

如果所有交换区的空闲槽位总数小于(在线处理器数量 2 SWAP_SLOTS_CACHE_SIZE),那么禁止使用每处理器交换槽位缓存。
如果所有交换区的空闲槽位总数大于(在线处理器数量 5 SWAP_SLOTS_CACHE_SIZE),那么启用每处理器交换槽位缓存。

5)交换项。
内核定义了数据类型swp_entry_t以存储换出页在交换区中的位置,我们称为交换项,高7位存储交换区的索引,其他位存储页在交换区中的偏移(单位是页)。

include/linux/mm_types.h
typedef struct {
    unsigned long val;
} swp_entry_t

内核定义了3个内联函数:
swp_entry(type,offset)用来把交换区的索引和偏移转换成交换项。
swp_type(entry)用来从交换项提取索引字段。
swp_offset(entry)用来从交换项提取偏移字段。

把匿名页换出到交换区的时候,需要在页表项中存储页在交换区中的位置,页表项存储交换区位置的格式由各种处理器架构自己定义,数据类型swp_entry_t是处理器架构无关的。内核定义了两个内联函数以转换页表项和交换项。

swp_entry_to_pte(entry)用来把交换项转换成页表项。

pte_to_swp_entry(pte)用来把页表项转换成交换项。

如果页表项满足条件“!pte_none(pte) && !pte_present(pte)”,说明页被换出到交换区,其中“!pte_none(pte)”表示页表项不是空表项,“!pte_present(pte)”表示页不在内存中。

6)交换缓存。
每个交换区有若干个交换缓存,每214页对应一个交换缓存,交换缓存的数量是(交换区的总页数/214)。

为什么需要交换缓存?
换出页可能由多个进程共享,进程的页表项存储页在交换区中的位置。当某个进程访问页的数据时,把页从交换区换入内存中,把页表项指向内存页。问题是:其他进程怎么找到内存页?
从交换区换入页的时候,把页放在交换缓存中,直到共享同一个页的所有进程请求换入页,知道这一页在内存中新的位置为止。如果没有交换缓存,内核无法确定一个共享的内存页是不是已经换入内存中。

交换区信息结构体有一个交换映射,每个字节对应交换区中的每个槽位,低6位存储每个槽位的使用计数,也就是共享换出页的进程的数量。每当一个进程请求换入页的时候,就会把使用计数减1,减到0时说明共享内存页的所有进程已经请求换入页;高2位是标志位,SWAP_HAS_CACHE表示页在交换缓存中。

交换缓存是使用地址空间结构体address_space实现的,用来把交换区的槽位映射到内存页,全局数组swapper_spaces存储每个交换区的交换地址空间数组的地址,全局数组nr_swapper_spaces存储每个交换区的交换缓存数量。

mm/swap_state.c
struct address_space *swapper_spaces[MAX_SWAPFILES];
static unsigned int nr_swapper_spaces[MAX_SWAPFILES];

如下所示,全局数组swapper_spaces的每一项指向一个交换区的交换地址空间数组,数组的大小是(交换区的总页数/214),每个交换地址空间的主要成员如下:
在这里插入图片描述

  • 成员page_tree是基数树(radix tree)的根,用来把交换区的偏移映射到物理页的页描述符,内核的基数树是16叉树或64叉树。
  • 成员a_ops指向交换地址空间操作集合swap_aops,后者的writepage方法是函数swap_writepage,用来把页写到交换区。

宏swap_address_space(entry)用来获取交换项对应的交换地址空间:

include/linux/swap.h
#define SWAP_ADDRESS_SPACE_SHIFT  14
#define swap_address_space(entry) (&swapper_spaces[swp_type(entry)][swp_offset(entry) >> SWAP_ADDRESS_SPACE_SHIFT])

(2)启用交换区。
命令“swapon”通过系统调用sys_swapon启用交换区,系统调用sys_swapon有两个参数。
1)const char __user *specialfile:文件路径。如果交换区是磁盘分区,文件路径是块设备文件的路径。
2)int swap_flags:标志位,其中第0~14位存储交换区的优先级,第15位指示是否指定了优先级。
系统调用sys_swapon的执行过程如下。
1)调用函数alloc_swap_info,分配交换区信息结构体,在交换区信息数组中查找一个空闲的数组项,设置数组项指向刚分配的交换区信息结构体。
2)打开文件,设置交换区信息结构体的成员swap_file指向文件的打开实例。
3)调用函数claim_swapfile,设置交换区信息结构体的成员bdev:如果交换区是磁盘分区,指向磁盘分区对应的块设备;如果交换区是文件,指向文件所在的块设备。
4)读入交换区的第一页,解析交换区的首部,得到交换区的总页数和坏页数量。
5)调用函数setup_swap_map_and_extents,设置交换映射和交换区间。
6)调用函数init_swap_address_space,初始化交换区的交换缓存。
7)调用函数enable_swap_info,处理如下:
设置交换区的优先级。如果没有指定优先级,那么取全局变量least_priority减一以后的值,全局变量least_priority的初始值是0,可见优先级是一个负数。
全局变量nr_swap_pages存储所有交换区的空闲页数总和加上当前交换区的总页数。
全局变量total_swap_pages存储所有交换区的页数总和加上当前交换区的总页数。
把交换区加入链表swap_active_head和swap_avail_head,按优先级从高到低排序。

(3)换出匿名页。
函数shrink_inactive_list回收不活动匿名页的执行流程如下所示:
在这里插入图片描述
1)调用函数 add_to_swap,从优先级最高的交换区分配交换槽位,把页加入交换缓存。
2)调用函数page_mapping,获取交换地址空间。
3)调用函数try_to_unmap,根据反向映射的数据结构找到物理页被映射到的所有虚拟页,针对每个虚拟页,执行操作:首先从进程的页表中删除旧的映射,如果页表项设置了脏标志位,那么把脏标志位转移到页描述符,然后在交换映射中把交换槽位的使用计数加1,最后在页表项中保存交换区的索引和偏移。
4)如果是脏页,那么调用函数pageout,把页回写到存储设备,函数pageout调用交换地址空间的writepage方法swap_writepage,把页写到交换区。
5)调用函数__remove_mapping,把匿名页从交换缓存中删除。
6)把页添加到释放链表free_pages中。

函数add_to_swap的执行流程如下:
1)调用函数get_swap_page,从优先级最高的交换区分配一个槽位。
2)如果是透明巨型页,拆分成普通页。
3)调用函数add_to_swap_cache,把页添加到交换缓存中,给页描述符设置标志位PG_swapcache,表示页在交换缓存中,页描述符的成员private存储交换项。

(4)换入匿名页。
匿名页被换出到交换区以后,访问页时,生成页错误异常。如下所示,函数handle_pte_fault发现“页表项不是空表项,但是页不在内存中”,知道页已经被换出到交换区,调用函数do_swap_page以把页从交换区读到内存中。函数do_swap_page的执行流程如下:
在这里插入图片描述
1)调用函数pte_to_swp_entry,把页表项转换成交换项,交换项包含了交换区的索引和偏移。
2)调用函数lookup_swap_cache,在交换缓存中根据交换区的偏移查找页。
3)如果页不在交换缓存中,那么调用函数swapin_readahead,把页从交换区读到交换缓存。
4)在页表中添加映射。
5)调用函数do_page_add_anon_rmap,添加反向映射。
6)调用函数activate_page,把页添加到活动匿名页LRU链表中。
7)调用函数swap_free,在交换映射中把交换槽位的使用计数减1。
8)如果已分配槽位数量大于或等于总槽位数的一半,或者页被锁定在内存中,那么调用函数try_to_free_swap,尝试释放交换槽位:如果交换槽位的使用计数是0,那么把页从交换缓存中删除,并且释放交换槽位。
9)如果执行写操作,那么调用函数do_wp_page以执行写时复制。
10)调用函数update_mmu_cache,更新页表缓存。

1.7 回收slab缓存

使用slab缓存的内核模块可以注册收缩器,页回收算法遍历收缩器链表,调用每个收缩器来收缩slab缓存,释放对象。
1.编程接口
使用slab缓存的内核模块可以使用函数register_shrinker注册收缩器:

int register_shrinker(struct shrinker *shrinker);

使用函数unregister_shrinker注销收缩器:

void unregister_shrinker(struct shrinker *shrinker);

2.数据结构
收缩器的数据结构如下:

include/linux/shrinker.h
struct shrinker {
     unsigned long (*count_objects)(struct shrinker *,
                          struct shrink_control *sc);
     unsigned long (*scan_objects)(struct shrinker *,
                          struct shrink_control *sc);

     int seeks;
     long batch;
     unsigned long flags;

     /* 这些成员是内部使用的 */
     struct list_head list;
     atomic_long_t *nr_deferred;
};

(1)方法count_objects:返回可释放对象的数量。
(2)方法scan_objects:释放对象,返回释放的对象数量。如果返回SHRINK_STOP,表示停止扫描。
(3)成员seeks:控制扫描的对象数量的因子,扫描的对象数量和这个因子成反比,即因子越大,扫描的对象越少。如果使用者不知道合适的数值,可以设置为宏DEFAULT_SEEKS,值为2。
(4)成员batch:批量释放的数量,如果为0,使用默认值128。
(5)成员flags:标志位,目前定义了两个标志位,SHRINKER_NUMA_AWARE表示感知NUMA内存节点,SHRINKER_MEMCG_AWARE表示感知内存控制组。
(6)成员list:内部使用的成员,用来把收缩器添加到收缩器链表中。
(7)成员nr_deferred:内部使用的成员,记录每个内存节点延迟到下一次扫描的对象数量。
方法scan_objects的第二个参数sc用来传递控制信息,结构体shrink_control如下:

include/linux/shrinker.h
struct shrink_control {
     gfp_t gfp_mask;
     unsigned long nr_to_scan;
     int nid;
     struct mem_cgroup *memcg;
};

(1)成员gfp_mask:分配掩码。
(2)成员nr_to_scan:应该扫描的对象数量。
(3)成员nid:对于感知NUMA内存节点的收缩器,需要知道当前正在回收的内存节点的编号。
(4)成员memcg:对于感知内存控制组的收缩器,需要知道正在回收的内存控制组。

3.技术原理
函数shrink_slab负责回收slab缓存,有5个参数。
(1)gfp_t gfp_mask:分配掩码。
(2)int nid:内存节点编号。
(3)struct mem_cgroup *memcg:内存控制组。
(4)unsigned long nr_scanned:在LRU链表中扫描过的页数。
(5)unsigned long nr_eligible:LRU链表中符合扫描条件的总页数。

函数shrink_slab遍历收缩器链表shrinker_list,针对每个收缩器,把主要工作委托给函数do_shrink_slab。

举报

相关推荐

0 条评论