内存管理-61-页回收-1-理论学习

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

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

(1) 交换支持的页:

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

(2)存储设备支持的文件页

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

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

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

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

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


一、数据结构

1. LRU链表

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

1-1

 

在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() 负责为页表项设置访问标志位。

handle_mm_fault //memory.c
    __handle_mm_fault //memory.c
        handle_pte_fault //memory.c
            pte_mkyoung //pgtable.h 设置 PTE_AF 位
            ptep_set_access_flags //fault.c

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

read
    vfs_read //read_write.c
        __vfs_read //read_write.c
            new_sync_read //read_write.c
                call_read_iter //fs.h
                    file->f_op->read_iter = ext4_file_read_iter 
                        generic_file_read_iter //file.c
                            generic_file_buffered_read //filemap.c 原文是do_generic_file_read(), 5.4内核上不存在了
                                mark_page_accessed //swap.c


2. 反向映射

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

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

//include/1inux/mm_types.h

struct page (
    ...
    union {
        struct address_space mapping;
        pgoff_t index; /* 在映射里面的偏移 */
        ...
    };
    union {
        atomic_t _mapcount;
        ...
    };
    ...
};

成员介绍:

(1) mapping

利用指针总是4的整数倍这个特性,成员 mapping 的最低两位用来作为页映射标志,最低位 PAGE_MAPPING_ANON 表示匿名页####。
如果物理页是匿名页,page.mapping = (struct anon_vma 的地址 | PAGE_MAPPING ANON)。
如果物理页是文件页 page.mapping 指向结构体 address_space。

(2) index

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

(3) _mapcount

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


2.1 匿名页的反向映射

匿名页的反向映射的数据结构如 图3.109 和 图3.110 所示。

1-2

(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 的成员,是红黑树的节点。
(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 得到页全局目录的起始地址。

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

1-3

图3.112:

1-4

(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.2 文件页的反向映射

文件页的反向映射的数据结构如 图3.113 所示。
(1) 存储设备上的文件系统有一个描述文件系统信息的超级块,挂载文件系统时在内存中创建一个超级块的副本,即 super_block 实例。
(2) 文件系统中的每个文件有一个描述文件属性的索引节点,读文件时在内存中创建一个索引节点的副本,即 inode 实例,成员 i_mapping 指向一个地址空间结构体 address_space。
(3) 打开文件时,在内存中创建一个文件打开实例 file, 成员 f_mapping 继承 inode 实例的成员 i_mapping。

1-5

(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) 文件页被映射到的虚拟页的起始地址是 “虚拟内存区域的成员 vmstart + 页描述符的成员index - 虚拟内存区域的成员 vm_pgoff) x 页长度”。
(5) 根据 vm_area_struct 实例的成员 vm_mm 得到进程的内存描述符,根据内存描述符的成员 pgd 得到页全局目录的起始地址。

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

1-6


二、发起页回收

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

1-7


1. 异步回收

内核内存节点有一个页回收线程,执行流程如 图3.116 所示。如果内存节点的所有内存区域的空闲页数小于高水线,页回收线程就会反复尝试回收页####,调用函数 shrink_node() 以回收内存节点中的页。

2. 直接回收

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

1-8

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

1-9


2.1 回收内存节点中的页

(1) 调用的数 get_scan_count(), 计算需要扫描多少个不活跃匿名页、活跃匿名页、不活跃文件页和活跃文件页。

(2) 依次扫描####不活跃匿名页、活跃匿名页、不活跃文件页和活跃文件页4条LRU链表,针对每条LRU链表,处理如下。
a. 如果是活跃LRU链表, 并且不活跃页比较少,那么调用数 shrink_acive_list() 把一部分活跃页转移到不活跃链表中:
b. 如果是不活动LRU链表,那么调用函数 shrink_inactive_list() 以回收不活跃页。


2.2 调用函数 shrink_slab() 以回收slab缓存

函数 balance_pdat() 和 try_to_free_pages() 使用结构体 scan_contol 控制扫描操作, 这个结构体不仅用于高层函数向低层函数传递控制指令,也用于反向传递结果,主要成员如下:

struct scan_control { //vmscan.c
    unsigned long nr_to_reclaim;
    nodemask_t *nodemask;
    struct mem_cgroup *target_mem_cgroup;
    unsigned int may_writepage:1;
    unsigned int may_unmap:1;
    unsigned int may_swap:1;
    unsigned int memcg_low_reclaim:1;
    unsigned int memcg_low_skipped:1;
    unsigned int hibernation_mode:1;
    unsigned int compaction_ready:1;
    s8 order;
    s8 priority;
    s8 reclaim_idx;
    gfp_t gfp_mask;
    unsigned long nr_scanned;
    unsigned long nr_reclaimed;
    struct {
        unsigned int dirty;
        unsigned int unqueued_dirty;
        unsigned int congested;
        unsigned int writeback;
        unsigned int immediate;
        unsigned int file_taken;
        unsigned int taken;
    } nr;
    struct reclaim_state reclaim_state;
    struct vm_area_struct *target_vma;
};

成员介绍:

nr_to_reclaim: 应该回收多少页,执行直接页回收的时候取32页.

gfp_mask: 触发回收的申请分配页的掩码。调用者中请页时可能不允许向下调用到底层文件系统, 或者不允许读写存储设备, 需要把这些约事传给页回收算法。

reclaim_idx: 回收的最高内存区域。

order: 申请分配页的阶数。比如调用者正在申请n阶页, 希望页回收算法能满足申请n阶页的需求.

nodemask: 调用者允许扫描的内存节点的掩码。如果是空指针, 表示扫描所有内存节点。

priority: 扫描优先级,一次扫描的页数是(LRU链表的总页数 >> 扫描优先级), 初始值是12.

may_writepage: 是否允许把修改过的文件页回写到在设备.

may_unmap: 是香允许回收页表映射的物理页.

may_swap: 是否允许把匿名页换出到交换区.

nr_scanned: 用来报告扫描过的不活跃页的数量。

nr_reclaimed: 用来报告回收了多少页。


3. 判断是否应该重试页回收

函数 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) //mm/page_a1loc.c
{
    struct zone *zone;
    struct zoneref *z;
    bool ret = false;

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

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

    /*
     * 针对每个目标内存区域,处理如下。
     * 如果回收了所有可回收的页,空闲页数是否大于水线?如果大于,函数 __zone_watermark_ok()
     * 返回真。
     * 如果直接回收没有进展,并且脏页和正在回写的页在可回收页中的比例超过一半,那么应该等待
     * 回写完成,使回收减速,阻止过早杀死进程。
     */
    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;

        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;
                }
            }

            ret = true;
            goto out;
        }
    }
out:
    if (current->flags & PF_WQ_WORKER)
        schedule_timeout_uninterruptible(1);
    else
        cond_resched();
    return ret;
}


三、计算扫描的页数

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

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

2. 两个参数用来控制扫描的匿名页和文件页的比例

(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_scanned[0] + 1) / (reclaim_stat->recent_rotated[0] + 1);
    fp = file_prio * (reclaim_stat->recent_scanned[1] + 1) / (reclaim_stat->recent_rotated[1] + 1);

    size = LRU链表中内存区域类型小于或等于回收的最高区域类型的总页数。
    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] 是从不活跃变为活跃的文件页的数量。


四、收缩活跃页链表

当不活跃页比较少的时候,页回收算法收缩活跃页链表,也就是从活跃页链表的尾部取物理页并转移到不活跃页链表中,把活跃页转换成不活跃页。
函数 inactive_list_is_low() 判断不活跃页是不是比较少,其算法如下:

inactive = 不活跃页链表中内存区域类型小于或等于回收的最高区域类型的总页数.
acive = 活跃页链表中内存区域类型小于或等于回收的最高区域类型的总页数。
gb = 把 (inactive + active) 从页数转换成字节数, 单位是GB
如果8大于0: inactive_ratio = 根号(10*gb)
否则: 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 所示。

1-10

(1) 调用函数 isolate_lru_pages(), 从活动页链表的尾部取指定页数添加到临时链表 1_hold 中, 清除页的LRU标志位。页所属的内存区域必须小于或等于回收的最高区域。

(2) 针对临时链表 1_hold 中的每个页,处理如下。
a. 调用函数 page_referenced() 来判断页最近是否被访问过。
b. 如果页最近被访问过,并且是程序的代码段所在的物理页,那么保留在活动页链表中,添加到临时的活动页链表 1_active 中。
c. 将活动页转换成不活动页,清除页的活动标志。
d. 添加到临时的不活动页链表 1_inactive 中。
(3) 有些活动页保留在活动页链表中,把临时的活动页链表 1_active 中的页添加到活动页链表的首部。

(4) 将有些活动页转换成不活动页,把临时的不活动页链表 1_inactive 中的页添加到不活动页链表的首部。

(5) 调用函数 free_hot_cold_page_list() 释放引用计数变为0的页,作为缓存冷页(即页的数据不在处理器的缓存中)释放。在回收的过程中, 在其他地方可能已经释放了页,当页回收算法把页的引用计数减1的时候,发现引用计数变成0,直接释放页。


将活动页转换成不活动页的规则如下:

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

(2) 如果是匿名页或其他类型的文件页,转移到不活动页链表中。

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


五、回收不活动页

1. 函数 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() 的执行流程如 图3.120 所示。

1-11

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

2. 回收不活动页的主要工作是由函数 shrink_page_list() 实现的,执行流程如 图3.121 所示。

1-12

(1) 针对临时链表 page_list 中的每个页,执行下面的操作。
a. 调用函数 page_check_references(), 检查页最近是否被访问过,返回处理方式。
b. 如果处理方式是转换成活动页,那么设置活动标志,添加到临时链表 ret_pages 中。
c. 如果处理方式是保留在不活动页链表中,那么添加到临时链表 ret_pages 中。
d. 如果处理方式是回收,执行下面的操作:
1) 如果是匿名页,调用函数 add_to_swap() 以添加到交换缓存中。####
2) 如果是页表映射的页,调用函数 try_to_unmap(),从页表中删除映射,通过反向映射的数据结构可以知道物理页被映射到哪些虚拟内存区域。
3) 如果是脏页,调用函数 pageout(), 把文件页写回到存储设备,或者把匿名页写回到交换区。
4) 把页从交换缓存或页缓存中删除: 如果是交换支持的页,从交换缓存中删除;如果是文件页,从文件的页缓中删除。
5) 把页添加到临时链表 free_pages() 中。

(2) 释放临时链表 free_pages() 中的页。

(3) 临时链表 ret_pages 存放转换成活动页或保留在不活动页链表中的不活动页,把临时链表 ret_pages 中的页转移到临时链表 page_list 中返回。


补充: 什么是表映射的页:

可以简单理解为进程虚拟地址经过页表能找到对应物理页的那一页。如果 PTE 里是 present 的物理页帧号(PFN),这就是“已页表映射”的页。
已映射页:访问该虚拟地址时可直接命中物理页。
未映射页:PTE 不存在或 not-present,访问会触发缺页异常(page fault),再由内核按需分配/读盘/建立映射。

常见的“页表映射页”类型有:
匿名页:如堆、栈、匿名 mmap 分配出来并已经触发过实际分配的页。
文件页:文件 mmap 后被访问、已经装入 page cache 并建立了进程页表映射的页。
共享页:多个进程 PTE 指向同一个物理页(如共享库代码页)。

关键点:
“页在内存里”不一定“当前被某个进程页表映射”; 一个物理页可以被多个进程映射; 被换出到 swap 后,PTE 可能变成 swap entry,不再是 present 映射。

一句话记忆:页表映射的页 = 当前有虚拟地址 -> 物理页,这条有效翻译关系的页。


3. 不活动页转换成活动页的情况如下

(1) 页表映射的页。

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

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

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


补充: 什么是"没有页表映射的文件页"

指文件页已经在页缓存里,但当前没有任何进程的页表项指向它。

可以分成两层理解:
(1) 它是文件页: 来源是文件系统的 page cache(例如通过 read 读入,或 mmap 后曾经被读入)。
(2) 它没有页表映射: 没有任何进程的 PTE 映射到这页,也就是 mapcount 为 0(常见语义)。

典型场景:
(1) 进程用 read 读取文件, 数据进入 page cache,但不是通过 mmap 建立用户页表映射,所以这类页通常就是“无页表映射文件页”。
(2) 曾经 mmap 过但后来 unmapped,例如 进程退出、munmap、VMA 回收后,页可能仍留在 page cache 中,但不再有任何页表映射。

此类型页回收成本低,无页表映射的 clean 文件页可以直接回收(丢弃),需要时再从磁盘读回。因此回收优先级更高,内存紧张时,内核通常更愿意先回收这类页,而不是匿名脏页或仍被映射的活跃页。

和“有页表映射文件页”的区别概括起来就是:
有页表映射文件页 = 在 page cache 里,且正在被某个进程虚拟地址直接映射访问。
没有页表映射文件页 = 只在 page cache 里缓存着,目前没有进程地址空间引用它。


4. 不活动页保留在不活动页链表中或者回收的情况

(1) 页表映射的不活动页。
a. 如果页表项设置了访问标志位,那么页保留在不活动页链表中,清除页表项的访问标志位,给页描述符设置访问标志位。
b. 如果页表项没有设置访问标志位,根据页的类型处理:
如果是交换支持的页,立即回收。
如果是存储设备支持的文件页:如果页描述符设置了访问标志位,那么只回收于净的页(页的数据自从读到内存中没有被修改), 脏页(页的数据自从读到内存中被修改过)保留在不活动页链表中;如果页描述符没有设置访问标志位,那么立即回收。

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


六、页交换

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

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


1. 使用方法

编译内核时需要开启配置宏 CONFIG_SWAP,默认开启。

1.1 使用磁盘分区作为交换区:

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

1.2 使用文件作为交换区:

(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"

1.3 使用ZRAM作为交换区

在内存比较小的设备上,可以使用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%(Android实际是50%)。如果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"来查看交换区。

注: Android中的swapon指令只支持 -d/-p 两个选项。


1.4 不同swap后端特点

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

固态硬盘使用NAND闪存作为存储介质,固态硬盘中的控制器运行闪存转换层固化程序,把闪存转换成块设备,使固态硬盘对外表现为块设备。
NAND闪存的特点是: 写入数据之前需要把擦除块擦除,每个擦除块的擦除次数有限,范围是10^5-10^6,频繁地写数据会缩短闪存的寿命。
所以, 如果设备使用固态硬盘或NAND闪存存储数据,不适合启用交换区;如果设备使用机械硬盘存储数据,可以启用交换区。

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

注: Android 中默认不使能 CONFIG_ZSWAP 关闭 zswap, 此功能应该和ZRAM重叠了。

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

2.1 数据结构

1. 交换区格式

交换区的第一页是交换区首部,内核使用数据结构 swap_header 描述交换区首部:

//include/linux/swap.h

union swap_header {
    struct {
        char reserved[PAGE_SIZE - 10];
        char magic[10];                /* "SWAP-SPACE" or "SWAPSPACE2" */
    } magic;
    struct {
        char        bootbits[1024];    /* Space for disklabel etc. */
        __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字节是魔幻数,用来区分交换区格式,内核已经不支持旧的格式"SWAPSPACE",只支持格式"SWAPSPACE2"。


2. 交换区信息

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

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

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

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

结构体 swap_info_struct 描述交换区的信息:

struct swap_info_struct { //swap.h
    unsigned long    flags;        /* SWP_USED etc: see above */
    signed short    prio;        /* swap priority of this type */
    struct plist_node list;        /* entry in swap_active_head */
    signed char    type;            /* strange name for an index */
    unsigned int    max;        /* extent of the swap_map */
    unsigned char *swap_map;    /* vmalloc'ed array of usage counts */
    struct swap_cluster_info *cluster_info; /* cluster info. Only for SSD */
    struct swap_cluster_list free_clusters; /* free clusters list */
    unsigned int lowest_bit;    /* index of first free in swap_map */
    unsigned int highest_bit;    /* index of last free in swap_map */
    unsigned int pages;        /* total of usable pages of swap */
    unsigned int inuse_pages;    /* number of those currently in use */
    unsigned int cluster_next;    /* likely index for next allocation */
    unsigned int cluster_nr;    /* countdown to next cluster search */
    struct percpu_cluster __percpu *percpu_cluster; /* per cpu's swap location */
    struct rb_root swap_extent_root;/* root of the swap extent rbtree */
    struct block_device *bdev;    /* swap device or bdev of swap file */
    struct file *swap_file;        /* seldom referenced */
    unsigned int old_block_size;    /* seldom referenced */
    spinlock_t lock;
    spinlock_t cont_lock;
    struct work_struct discard_work; /* discard worker */
    struct swap_cluster_list discard_clusters; /* discard clusters list */
    struct plist_node avail_lists[0];
};

主要成员如下:

flags:
标志位,常用标志位如下:
(1) SWP_USED 表示当前数组项处于使用状态。
(2) SWP_WRITEOK 表示交换区可写,禁用交换区以后交换区不可写。
(3) SWP_BLKDEV 表示交换区是块设备,即磁盘分区。

prio: 优先级

list:
用来加入有效交换区链表,按优先级从高到低排序,头节点是 swap_active_head。启用交换区以后交换区是有效的,禁用交换区以后交换区不再是有效的.

avail_lists[0]:
用来加入可用交换区链表,按优先级从高到低排序,头节点是 swap_avail_head。可用交换区是指有效的并且没有满的交换区。

type: 交换区的索引

max:
交换区的最大页数,和成员 pages 不同的是:max 不仅包括可用槽位,也包括损坏的或用于管理目的的槽位。硬盘出现坏块的情况很少见,所以 max 通常等于 pages 加 1.

pages: 交换区可用槽位的总数.

inuse_pages: 交换区正在使用的页的数量.

swap_map:
交换映射, 指向一个数组,每字节对应交换区中的每个槽位, 低6位存储每个槽位的使用计数,也就是共享换出页的进程的数量;高2位是标
志位,SWAP_HAS_CACHE 表示页在交换缓存中。

lowest_bit: 数组 swap_map 中第一个空闲数组项的索引.

highest_bit: 数组 swap_map 中最后一个空闲数组项的索引.

bdev: 指向块设备。如果交换区是磁盘分区,bdev指向磁盘分区对应的块设备;如果交换区是文件,bdev指向文件所在的块设备

swap_file:
指向交换区关联的文件的打开实例。如果交换区是磁盘分区,swap_file 指向磁盘分区对应的块设备文件的打开实例;如果交换区是文件,
swap_file 指向文件的打开实例.

cluster_next: 当前聚集中下一次分配的槽位的索引.

cluster_nr: 当前聚集中可用的槽位数量.

swap_extent_root: 交换区间树。


3. 交换区间

交换区间(swap extent)用来把交换区的连续槽位映射到连续的磁盘块。如果交换区是磁盘分区,因为磁盘分区的块是连续的,所以只需要一个交换区间。如果交换区是文件,因为文件对应的磁盘块不一定是连续的,所以对于每个连续的磁盘块范围,需要使用一个交换区间来存储交换区的连续槽位和磁盘块范围的映射关系。
如 图3.122 所示,交换区信息的成员 first_swap_extent 存储第一个交换区间的信息,交换区间的成员 start_page 是起始槽位的页号,成员 nr_pages 是槽位的数量,成员 start_block 是起始磁盘块号,成员 list 用来链接同一个交换区的所有交换区间。

1-13

注: msm-5.4内核中改为区间树了。


4. 交换槽位缓存

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

1-14


结构体:

struct swap_slots_cache { //swap_slots.h
    bool    lock_initialized;
    struct mutex    alloc_lock; /* protects slots, nr, cur */
    swp_entry_t    *slots;
    int        nr;
    int        cur;
    spinlock_t    free_lock;  /* protects slots_ret, n_ret */
    swp_entry_t    *slots_ret;
    int        n_ret;
};

成员介绍:

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位存储交换区的索引,其他位存储页在交换区中的偏移(单位是页)。注: msm-5.4内核中由 swp_entry() 看是高6位。

//include/1inux/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. 交换缓存

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

为什么需要交换缓存?
换出页可能由多个进程共享,进程的页表项存储页在交换区中的位置。当某个进程访问页的数据时, 把页从交换区换入内存中,把页表项指向内存页。问题是:其他进程怎么找到内存页?
从交换区换入页的时候,把页放在交换缓存中,直到共享同一个页的所有进程请求换入页, 知道这一页在内存中新的位置为止。如果没有交换缓存,内核无法确定一个共享的内存页是不是已经换入内存中。
交换区信息结构体有一个交换映射, 每个字节对应交换区中的每个槽位,低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 的每一项指向一个交换区的交换地址空间数组,数组的大小是(交换区的总页数/2^14),每个交换地址空间的主要成员如下。

1-15

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

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

#define SWAP_ADDRESS_SPACE_SHIFT    14
#define SWAP_ADDRESS_SPACE_PAGES    (1 << SWAP_ADDRESS_SPACE_SHIFT)
extern struct address_space *swapper_spaces[]; //每个指针又指向一个数组了
#define swap_address_space(entry)                \
    (&swapper_spaces[swp_type(entry)][swp_offset(entry)    >> SWAP_ADDRESS_SPACE_SHIFT]) //低58位又右移14位


7. 启用交换区

命令"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(),处理如下:
a. 设置交换区的优先级。如果没有指定优先级,那么取全局变量 least_priority 减一以后的值,全局变量 least_priority 的初始值是0,可见优先级是一个负数。
b. 全局变量 nr_swap_pages 存储所有交换区的空闲页数总和加上当前交换区的总页数。
c. 全局变量 total_swap_pages 存储所有交换区的页数总和加上当前交换区的总页数。
d. 把交换区加入链表 swap_active_head 和 swap_avail_head, 按优先级从高到低排序。


8. 换出匿名页

函数 shrink inactive list() 回收不活动匿名页的执行流程如 图3.125 所示。

1-16

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


9. 换入匿名页

匿名页被换出到交换区以后,访问页时,生成页错误异常。如 图3.126 所示:

1-17

函数 handle_pte_fault() 发现“页表项不是空表项,但是页不在内存中”,知道页已经被换出到交换区,调用函数 do_swappage() 以把页从交换区读到内存中。函数 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(), 更新页表缓存。


七、回收slab缓存

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


1.编程接口

使用slab缓存的内核模块可以使用函数 register_shriker() 注收缩器

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);
    long batch;    /* reclaim batch size, 0 = default */
    int seeks;    /* seeks to recreate an obj */
    unsigned flags;
    struct list_head list;
    int id;
    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 的第二个参数 s c用来传递控制信息,结构体 shrink_control 如下:

//include/linux/shrinker.h
struct shrink_control {
    gfp_t gfp_mask;
    int nid;
    unsigned long nr_to_scan;
    unsigned long nr_scanned;
    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()。

 

posted on 2026-04-01 17:34  Hello-World3  阅读(0)  评论(0)    收藏  举报

导航