页回收机制的原理

主要参考了《深入linux内核》和《Linux内核深度解析》,另外简单浅析了一下相关内容

页回收机制

Linux基数树(radix tree)是将long整数键值与指针相关联的机制,它存储有效率,并且可快速查询,用于整数值与指针的映射(如:IDR机制)、内存管理等。

IDR(ID Radix)机制是将对象的身份鉴别号整数值ID与对象指针建立关联表,完成从ID与指针之间的相互转换。IDR机制使用radix树状结构作为由id进行索引获取指针的稀疏数组,通过使用位图可以快速分配新的ID,IDR机制避免了使用固定尺寸的数组存放指针。IDR机制的API函数在lib/idr.c中实现。

Linux radix树最广泛的用途是用于内存管理,结构address_space通过radix树跟踪绑定到地址映射上的核心页(如用来把交换区的偏移映射到物理页的页描述符),该radix树允许内存管理代码快速查找标识为dirty或writeback的页。其使用的是数据类型unsigned long的固定长度输入的版本。每级代表了输入空间固定位数。Linux radix树的API函数在lib/radix-tree.c中实现。(把页指针和long整数键值映射起来,使能快速查询一个页的信息。

Linux内核利用radix树在文件内偏移快速定位文件缓存页。
Linux(2.6.7) 内核中的分叉为 64(),树高为 6(64位系统)或者 11(32位系统),用来快速定位 32 位或者 64 位偏移,radix tree 中的每一个叶子节点指向文件内相应偏移所对应的Cache项。

【radix树为稀疏树提供了有效的存储,代替固定尺寸数组提供了键值到指针的快速查找。】

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

物理页根据是否有存储设备支持分为两类。

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

针对不同的物理页,采用不同的回收策略:

(1)交换支持的页:采用页交换的方法,先把页的数据写到交换区,然后释放物理页。

(2)存储设备支持的文件页:如果是干净的页,即把文件从存储设备读到内存以后没有修改过,可以直接释放;如果是脏页,即把文件从存储设备读到内存以后修改过,那么先写回到存储设备,然后释放物理页。

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

  • 那我们根据什么的原则选择回收物理页呢?

    Linux内核使用LRU(Least Recently Used,最近最少使用)算法选择最近最少使用的物理页。

  • 回收物理页的时候,如果物理页被映射到进程的虚拟地址空间,那么需要从页表中删除虚拟页到物理页的映射。如何知道物理页被映射到哪些虚拟页?

    需要通过反向映射的数据结构,虚拟页映射到物理页是正向映射,物理页映射到虚拟页是反向映射。

LRU - lruvec

LRU(最近最少使用)链表

页回收算法使用LRU算法选择回收的页。每个内存节点的pglist_data实例就有一个成员lruvec,称为LRU向量,LRU向量包含5条LRU链表。

image-20220807165301922

LRU是双向链表,内核根据页面类型(匿名和文件)与活跃性(活跃和不活跃),分成5种类型的LRU链表:

  • 不活动匿名页LRU链表,用来链接不活动的匿名页,即最近访问频率低的匿名页;
  • 活动匿名页LRU链表,用来链接活动的匿名页,即最近访问频率高的匿名页;
  • 不活动文件页LRU链表,用来链接不活动的文件页,即最近访问频率低的文件页;
  • 活动文件页LRU链表,用来链接活动的文件页,即最近访问频率高的文件页;
  • 不可回收LRU链表,用来链接使用mlock锁定在内存中、不允许回收的物理页。
#define LRU_BASE 0
#define LRU_ACTIVE 1
#define LRU_FILE 2

enum lru_list {
	LRU_INACTIVE_ANON = LRU_BASE, // 不活跃匿名页面链表,需要交换分区才能回收!
	LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE, // 活跃匿名页面链表
	LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE, // 不活跃文件映射页面链表,最优先回收
	LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE, // 活跃文件映射页面链表
	LRU_UNEVICTABLE, // 不可回收页面链表,禁止换出
	NR_LRU_LISTS
};

struct lruvec {
	struct list_head		lists[NR_LRU_LISTS];
	struct zone_reclaim_stat	reclaim_stat;
	/* Evictions & activations on the inactive file list */
	atomic_long_t			inactive_age;
	/* Refaults at the time of last reclaim cycle */
	unsigned long			refaults;
#ifdef CONFIG_MEMCG
	struct pglist_data *pgdat;
#endif
};

在LRU链表中,物理页的页描述符特征:

  • 页描述符设置PG_Iru标志位,表示物理页在LRU链表中;
  • 页描述符通过成员Iru加入LRU链表;
  • 如果是交换支持的物理页,页描述符会设置PG_swapbacked标志位;
  • 如果是活动的物理页,页描述符会设置PG_active标志位;
  • 如果是不可回收的物理页,页描述符会设置PG_unevictable标志位。

每条LRU链表中的物理页按访问时间从大到小排序,链表首部的物理页的访问时间离当前最近,物理页从LRU链表的首部加入,
页回收算法从不活动LRU链表的尾部取物理页回收,从活动LRU链表的尾部取物理页并移动到不活动LRU链表中。

如何确定页的活动?

  1. 如果是页表映射的匿名页或文件页,根据 页表项中的访问标志位 确定页的活动程度。 当处理器的内存管理单元把虚拟地址转换成物理地址的时候,如果页表项没有设置访问标志位,就会生成页错误异常。页错误异常处理程序为页表项设置访问标志位,如图3.107所示,函数pte_mkyoung负责为页表项设置访问标志位。

image-20220807171955103

  1. **如果是没有页表映射的文件页,进程通过系统调用read或write访问文件,文件系统在文件的页缓存中查找文件页,为文件页的页描述符设置访问标志位(PG_referenced)。**如图3.108所示,进程读EXT4文件系统中的一个文件,函数mark_page_accessed为文件页的页描述符设置访问标志位。

image-20220807172047648

反向映射

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

页描述符当中和反向映射具体成员如下:

struct page {
        ...
    union {
        struct address_space *mapping;  /* If low bit clear, points to
                         * inode address_space, or NULL.
                         * If page mapped as anonymous
                         * memory, low bit is set, and
                         * it points to anon_vma object:
                         * see PAGE_MAPPING_ANON below.
                         */
        ...
        /* page_deferred_list().next     -- second tail page */
    };
    ...
    /* Second double word */
    union {
        pgoff_t index;      /* Our offset within mapping. */
       ...
            union {
                atomic_t _mapcount; 
  • mapping
    因为指针变量所指向的变量至少按4个字节对齐,因此可以用最后两位来区分不同的映射。

    • 对于匿名映射,最低位为PAGE_MAPPING_ANON,指向anon_vma结构体,每个匿名页对应唯一的anon_vma;
    • 对于文件映射而言,指向address_space结构体。
  • index
    成员index是在映射里面的偏移,单位是页。

    • 如果是匿名映射,那么index是物理页对应的虚拟页在vm_areat_struct指定的虚拟内存区域中的页偏移;
    • 如果是文件映射,那么index是物理页存储的数据在文件中的页偏移。
  • _mapcount
    **成员_mapcount是映射计数,反映物理页被映射到多少个vm_struct虚拟内存区域。**初始值是−1,加上1以后才是真实的映射计数,建议使用内联函数
    page_mapcount获取页的映射计数。

    注意和mm_struct结构体中的map_count做区分,map_count表示mm_strcut中有多少个vm_struct区域。

img

匿名页的反向映射

page 中的mapping

image-20220807182430302

page通过mapping找到anon_vma,anon_vma 遍历自己管理的红黑树rb_root,找到树上的每个节点AVC(anon_vma_chain),AVC通过成员指针anon_vma找到对应的VMA,这个过程就完成了页表映射查找。需要注意的几点:

1.VMA中也有链表anon_vma_chain管理各个AVC,这里主要用在父子进程之间的管理,略。

2.VMA中有成员指针成员anon_vma,同时AVC中也有成员指针anon_vma,AVC起到桥梁作用所以可以指向VMA和AV,那VMA中为何又需要指向AV呢?

进程创建的流程中一般都是新建AV(anon_vma),然后创建AVC及VMA,然后调用anon_vma_chain_link建立三者之间的关系,但是当一个VMA没有对应页的时候,此时触发pagefault,这里可以快速判断VMA有没有对应的page。

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是链表节点。

struct vm_area_struct {
    ...
    struct list_head anon_vma_chain;
    struct anon_vma *anon_vma;
    ...
};
/*
  * anon_vma 位于私有“相关” vmas 列表的前面,以扫描指向此 anon_vma 的匿名页面是否需要取消映射:列表上的 vmas 将通过分叉或拆分来关联。
  *
  * 由于 vmas 在拆分和合并时来来去去(特别是在 mprotect 中),匿名页面的映射字段不能直接指向 vma:而是指向 anon_vma,在其列表中相关 vmas 可以轻松链接或 未链接。
  *
  * 在取消链接列表中的最后一个 vma 后,我们必须对 anon_vma 对象本身进行垃圾收集:一旦 vma 列表为空,我们保证没有页面可以指向此 anon_vma。
  */
struct anon_vma {
	struct anon_vma *root;		/* Root of this anon_vma tree */
	struct rw_semaphore rwsem;	/* W: modification, R: walking the list */
	
    atomic_t refcount;

	unsigned degree;

	struct anon_vma *parent;	/* Parent of this anon_vma */

	struct rb_root rb_root;	/* Interval tree of private "related" vmas */
};

下图的anon_vma_node应该是anno_vma_chain

一个进程中多个vma可以共享同一个anon_vma作为匿名映射的节点。

查询一个匿名页被映射到的所有虚拟页和对应页表项pte的过程如下。

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得到页全局目录的起始地址,而后查询到相对应的pte

一个进程分叉生成子进程

从一个进程分叉生成子进程的时候,子进程把父进程的虚拟内存完全复制一份,如图3.111和图3.112所示,子进程把父进程的每个vm_area_struct实例复制一份,对每个vm_area_struct实例执行下面的操作。

image-20220807215231082

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指向这棵树的根

文件页的反向映射

文件页的反向映射的数据结构如图3.113所示。

image-20220807223539931

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得到页全局目录的起始地址。

私有文件映射的写时复制

如图3.114所示,对于私有的文件映射,在写的时候生成页错误异常,页错误异常处理程序执行写时复制,新的物理页和文件脱离关系,属于匿名页。

image-20220807223727693

发起页回收

申请分配页的时候页分配器首先尝试使用低水线分配页,

  • 如果使用低水线分配失败,说明内存轻微不足,页分配器将会唤醒所有符合分配条件的内存节点的页回收线程,异步回收页,然后尝试使用最低水线分配页。
  • 如果分配失败,说明内存严重不足,页分配器将会直接回收页。
  • 如果直接回收页失败,那么判断是否应该重新尝试回收页。
image-20220807223858316

异步回收

每个内存节点有一个页回收线程,如果内存节点的所有内存区域的空闲页数小于高水线,页回收线程就反复尝试回收页,调用函数shrink_node以回收内存节点中的页。

在进行内存分配的时候,如果分配器(比如buddy allocator)发现当前空余内存的值低于"low"但高于"min",说明现在内存面临一定的压力,那么在此次内存分配完成后,kswapd将被唤醒,以执行内存回收操作。在这种情况下,内存分配虽然会触发内存回收,但不存在被内存回收所阻塞的问题,两者的执行关系是异步的(之前的kswapd实现是周期性触发)。

这里所说的"空余内存"其实是一个zone总的空余内存减去其lowmem_reserve的值。对于kswapd来说,要回收多少内存才算完成任务呢?只要把空余内存的大小恢复到"high"对应的watermark值就可以了,当然,这取决于当前空余内存和"high"值之间的差距,差距越大,需要回收的内存也就越多。"low"可以被认为是一个警戒水位线,而"high"则是一个安全的水位线。

直接回收

直接内存回收发生在慢速分配当中

首先唤醒所有node结点的kswap内核线程,然后才会调用get_page_from_ freelist()尝试用min阀值从zonelist的zone中获取连续页框,如果失败,对zonelist的zone进行异步压缩,异步压缩之后再次调用get page_from_freelist()尝试用min阀值从zonelist的zone中获取连续页框,如是还是失败,就会进入到直接内存回收。

在进行直接内存回收时,进程是有可能加入到node的pgdat->pfmemalloc_wait这个等待队列中,当kswap进行内存回收之后,如果node空闲内存达到平衡状态,那么就会唤醒pgdat-> pfmemalloc_wait中的进程,
其实加入到pgdat->pfmemalloc_wait这个等待队列的进程,自身就不会进行直接内存回收,而是让kswapd进行,之后kswapd会唤醒它们即可。

直接回收页的执行流程如图3.117所示,针对备用区域列表中符合分配条件的每个内存区域,调用函数shrink_node来回收内存区域所属的内存节点中的页。回收页是以内存节点为单位执行的,函数shrink_node负责回收内存节点中的页。

image-20220807224259841

回收页是以内存节点为单位执行的,函数shrink_node负责回收内存节点中的页,执行流程如图3.118所示。

image-20220807224346219

(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控制扫描操作,这个结构体不仅用于高层函数向低层函数传递控制指令,也用于反向传递结果。

判断是否应该重试回收页

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

static inline bool
should_reclaim_retry(gfp_t gfp_mask, unsigned order,
		     struct alloc_context *ac, int alloc_flags,
		     bool did_some_progress, int *no_progress_loops)
{
	struct zone *zone;
	struct zoneref *z;

	/*
    对于昂贵的分配,直接回收可能,但是不意味着申请的阶数有可用空闲页块,
    因为内存可能高度碎片化所以总是增加计数器no_progress_loops。
    no_progress_loops是直接回收没有进展的计数器
	 */
	if (did_some_progress && order <= PAGE_ALLOC_COSTLY_ORDER)
		*no_progress_loops = 0;
	else
		(*no_progress_loops)++;

	/*
	如果直接回收没有进展超过16次,那么检查高阶原子类型是否有空闲页,
	如果有,那么转换成申请的迁移类型,然后再重试分配。
	 */
	if (*no_progress_loops > MAX_RECLAIM_RETRIES) {
		/* Before OOM, exhaust highatomic_reserve */
		return unreserve_highatomic_pageblock(ac, true);
	}

	/*
	 遍历每个目标内存区域,如果考虑了所有可回收的页,没有一个目标区域能满足分配请求,那么只有杀掉进程
	*/
	for_each_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx,
					ac->nodemask) {
		unsigned long available;
		unsigned long reclaimable;
		unsigned long min_wmark = min_wmark_pages(zone);
		bool wmark;

        /* 如果回收了所有可回收的页,空闲页数是否大于水线?如果大于,函数__zone_watermark_ok返回真 */
		available = reclaimable = zone_reclaimable_pages(zone);
		available += zone_page_state_snapshot(zone, NR_FREE_PAGES);
		wmark = __zone_watermark_ok(zone, order, min_wmark,
				ac_classzone_idx(ac), alloc_flags, available);
		trace_reclaim_retry_zone(z, order, reclaimable,
				available, min_wmark, *no_progress_loops, wmark);
        
		if (wmark) {
        /* 如果直接回收没有进展,并且脏页和正在回写的页在可回收页中的比例超过一半,那么应该等待回写完成,使回收减速,阻止过早杀死进程 */
			if (!did_some_progress) {
				unsigned long write_pending;

				write_pending = zone_page_state_snapshot(zone,
							NR_ZONE_WRITE_PENDING);

				if (2 * write_pending > reclaimable) {
					congestion_wait(BLK_RW_ASYNC, HZ/10);
					return true;
				}
			}

			if (current->flags & PF_WQ_WORKER)
				schedule_timeout_uninterruptible(1);
			else
				cond_resched();

			return true;
		}
	}

	return false;
}

计算扫描的页数

页回收算法每次扫描多少页?扫描多少个匿名页和多少个文件页,怎么分配匿名页和文件页的比例?

扫描优先级用来控制一次扫描的页数,如果扫描优先级是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]是从不活动变为活动的文件页的数量。

收缩活动页链表 shrink_active_list

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

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

  • inactive = 不活动页链表中内存区域类型小于或等于回收的最高区域类型的
    总页数
    active = 活动页链表中内存区域类型小于或等于回收的最高区域类型的总页数
    gb = 把(inactive + active)从页数转换成字节数,单位是GB。
    如果gb大于0
    inactive_ratio = (10 * gb) ^ 0.5
    否则
    inactive_ratio = 1
    如果(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的执行流程如图3.119所示。

image-20220807225945495

(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标准库,被很多进程链接,如果把这些共享库的代码段的物理页回收了,影响很大,每个进程执行时都会生成页错误异常,重新把共享库的虚拟页映射到物理页。

回收不活动页

回收不活动页 shrink_inactive_list

函数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() 执行流程源码分析如下:

mm\vmscan.c

image-20220807230626855

(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 处理临时链表

回收不活动页的主要工作由函数shrink_page_list()实现,具体执行流程源码分析如下:

image-20220807230732117

(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)页表映射的页。

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

(2)没有页表映射的文件页。

采用两次机会算法:进程第一次访问时,如果页描述符没有设置访问标志位,那么设置访问标志位;进程第二次访问时,发现页描述符设置了访问标志位,将不活动页转换成活动页

页交换(回收匿名页)

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

使用方法

编译内核时需要开启配置宏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

例如: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闪存存储数据,不适合启用交换区;
如果设备使用机械硬盘存储数据,可以启用交换区。

技术原理

数据结构
交换区首部

交换区的第一页是交换区首部,内核使用数据结构swap_header描述交换区首部,具体源码分析如下:

include\linux\swap.h

union swap_header {
	struct { // MAGIC结构部分只是用来辨认
		char reserved[PAGE_SIZE - 10];
		char magic[10];			/* SWAP-SPACE or SWAPSPACE2 */
	} magic;
	struct {
		char		bootbits[1024];	// 保留用来存放像DISK LABLE这些信息
		__u32		version; // 局部版本
		__u32		last_page; // 上次可用的页
		__u32		nr_badpages; // SWAP area中已知的BAD PAGES的个数
		unsigned char	sws_uuid[16];
		unsigned char	sws_volume[16];
		__u32		padding[117]; // 磁盘的一个段一般位512个字节
		__u32		badpages[1];
	} info;
};

  • 前面1024字节空闲,为引导程序预留空间,这种做法使得交换区可以处在磁盘的起始位置。
  • 成员version是交换区的版本号。
  • 成员last_page是最后一页的页号。
  • 成员nr_badpages是坏页的数量,从成员badpages的位置开始存放坏页的页号。
  • 最后10字节是魔幻数,用来区分交换区格式,内核已经不支持旧的格式“SWAP- SPACE”,只支持格式“SWAPSPACE2”。
交换区信息

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

mm/swap_file.c

struct swap_info_struct *swap_info[MAX_SWAPFILES];

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

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

交换区间

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

如图3.122所示,交换区信息的成员first_swap_extent存储第一个交换区间的信息,交换区间的成员start_page是起始槽位的页号,成员nr_pages是槽位的数量,成员start_block是起始磁盘块号,成员list用来链接同一个交换区的所有交换区间。

image-20220808010030592

交换槽位缓存

为了加快为换出页分配交换槽位的速度,每个处理器有一个交换槽位缓存swp_slots,数据结构如图3.123所示。

image-20220808010152898

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

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

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

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

交换项

内核定义了数据类型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)”表示页不在内存中。

交换缓存

每个交换区有若干个交换缓存,每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];

如图3.124所示,全局数组swapper_spaces的每一项指向一个交换区的交换地址空间数组,数组的大小是(交换区的总页数/214),每个交换地址空间的主要成员如下。

image-20220808010945935

● 成员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])
启用交换区

命令“swapon”通过系统调用sys_swapon启用交换区,系统调用sys_swapon有两个参数。

1)const char __user *specialfile:文件路径。如果交换区是磁盘分区,文件路径是块设备文件的路径。

2)int swap_flags:标志位,其中第0~14位存储交换区的优先级,第15位指示是否指定了优先级。

换出匿名页 shrink_inactive_list

函数shrink_inactive_list回收不活动匿名页的执行流程如图3.125所示。

image-20220808021920491

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存储交换项。

换入匿名页(页错误异常)

匿名页被换出到交换区以后,访问页时,生成页错误异常。如图3.126 所示,函数handle_pte_fault发现“页表项不是空表项,但是页不在内存中”,知道页已经被换出到交换区,调用函数do_swap_page以把页从交换区读到内存中。函数do_swap_page的执行流程如下。

image-20220808022126200

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,更新页表缓存。

回收slab缓存(略)

使用slab缓存的内核模块可以注册收缩器,页回收算法遍历收缩器链表,调用每个收缩器来收缩slab缓存,释放对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值