【原创】(十)Linux内存管理 - zoned page frame allocator - 5

背景

  • Read the fucking source code! --By 鲁迅
  • A picture is worth a thousand words. --By 高尔基

说明:

  1. Kernel版本:4.14
  2. ARM64处理器,Contex-A53,双核
  3. 使用工具:Source Insight 3.5, Visio

1. 概述

本文将讨论memory reclaim内存回收这个话题。

在内存分配出现不足时,可以通过唤醒kswapd内核线程来异步回收,或者通过direct reclaim直接回收来处理。在针对不同的物理页会采取相应的回收策略,而页回收算法采用LRU(Least Recently Used)来选择物理页。

直奔主题吧。

2. LRU和pagevec

2.1 数据结构

简单来说,每个Node节点会维护一个lrvvec结构,该结构用于存放5种不同类型的LRU链表,在内存进行回收时,在LRU链表中检索最少使用的页面进行处理。

为了提高性能,每个CPU有5个struct pagevecs结构,存储一定数量的页面(14),最终一次性把这些页面加入到LRU链表中。

上述的描述不太直观,先看代码,后看图,一目了然!

typedef struct pglist_data {
...
/* Fields commonly accessed by the page reclaim scanner */
struct lruvec       lruvec;
...
}

/*  5种不同类型的LRU链表 */
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

/* 14 pointers + two long's align the pagevec structure to a power of two */
#define PAGEVEC_SIZE    14
struct pagevec {
    unsigned long nr;
    unsigned long cold;
    struct page *pages[PAGEVEC_SIZE];  //存放14个page结构
};

/*  每个CPU定义5种类型 */
static DEFINE_PER_CPU(struct pagevec, lru_add_pvec);
static DEFINE_PER_CPU(struct pagevec, lru_rotate_pvecs);
static DEFINE_PER_CPU(struct pagevec, lru_deactivate_file_pvecs);
static DEFINE_PER_CPU(struct pagevec, lru_lazyfree_pvecs);
#ifdef CONFIG_SMP
static DEFINE_PER_CPU(struct pagevec, activate_page_pvecs);
#endif

上述的数据结构,可以用下图来进行说明:

简单来说,在物理内存进行回收的时候可以选择两种方式:

  • 直接回收,比如某些只读代码段等;
  • 页面内容保存后再回收;

针对页面内容保存又分为两种情况:

  1. swap支持的页,写入到swap分区后回收,包括进程堆栈段数据段等使用的匿名页,共享内存页等,swap区可以是一个磁盘分区,也可以是存储设备上的一个文件;
  2. 存储设备支持的页,写入到存储设备后回收,主要是针对文件操作,如果不是脏页就直接释放,否则需要先写回;

有上述这几种情况,便产生了5种LRU链表,其中ACTIVEINACTIVE用于表示最近的访问频率,最终页面也是在这些链表间流转。UNEVITABLE,表示被锁定在内存中,不允许回收的物理页,比如像内核中大部分页框都不允许回收。

2.2 流程分析

看一下LRU链表的整体操作:

上图中,主要实现的功能就是将CPU缓存的页面,转移到lruvec链表中,而在转移过程中,最终会调用pagevec_lru_move_fn函数,实际的转移函数是传递给pagevec_lru_move_fn的函数指针。在这些具体的转移函数中,会对Page结构状态位进行判断,清零,设置等处理,并最终调用del_page_from_lru_list/add_page_to_lru_list接口来从一个链表中删除,并加入到另一个链表中。

首先看看图中最右侧部分中,关于Page状态,在内核中include/linux/page-flags.h中有描述,罗列关键字段如下:

enum pageflags {
    PG_locked,      /* Page is locked. Don't touch. */
    PG_referenced, //最近是否被访问
    PG_dirty,  //脏页
    PG_lru,   //处于LRU链表中
    PG_active, //活动页
    PG_swapbacked,      /* Page is backed by RAM/swap */
    PG_unevictable,     /* Page is "unevictable"  */
}    

针对这些状态在该头文件中还有一系列的宏来判断和设置等处理,罗列几个如下:

ClearPageActive(page);
ClearPageReferenced(page);
SetPageReclaim(page);
PageWriteback(page);
PageLRU(page);
PageUnevictable(page);
...

上述的每个CPU5种缓存struct pagevec,基本描述了LRU链表的几种操作:

  • lru_add_pvec:缓存不属于LRU链表的页,新加入的页;
  • lru_rotate_pvecs:缓存已经在INACTIVE LRU链表中的非活动页,将这些页添加到INACTIVE LRU链表的尾部;
  • lru_deactivate_pvecs:缓存已经在ACTIVE LRU链表中的页,清除掉PG_activate, PG_referenced标志后,将这些页加入到INACTIVE LRU链表中;
  • lru_lazyfree_pvecs:缓存匿名页,清除掉PG_activate, PG_referenced, PG_swapbacked标志后,将这些页加入到LRU_INACTIVE_FILE链表中;
  • activate_page_pvecs:将LRU中的页加入到ACTIVE LRU链表中;

分析一个典型的流程吧,看看缓存中的页是如何加入到lruvecLRU链表中,对应到图中的执行流为:pagevec_lru_add --> pagevec_lru_move_fn --> __pagevec_lru_add_fn,分别看看这三个函数,代码简单直接附上:

/*
 * Add the passed pages to the LRU, then drop the caller's refcount
 * on them.  Reinitialises the caller's pagevec.
 */
void __pagevec_lru_add(struct pagevec *pvec)
{
    //直接调用pagevec_lru_move_fn函数,并传入转移函数指针
    pagevec_lru_move_fn(pvec, __pagevec_lru_add_fn, NULL);
}
EXPORT_SYMBOL(__pagevec_lru_add);

static void pagevec_lru_move_fn(struct pagevec *pvec,
    void (*move_fn)(struct page *page, struct lruvec *lruvec, void *arg),
    void *arg)
{
    int i;
    struct pglist_data *pgdat = NULL;
    struct lruvec *lruvec;
    unsigned long flags = 0;

    //遍历缓存中的所有页
    for (i = 0; i < pagevec_count(pvec); i++) {
        struct page *page = pvec->pages[i];
        struct pglist_data *pagepgdat = page_pgdat(page);

       //判断是否为同一个node,同一个node不需要加锁,否则需要加锁处理
        if (pagepgdat != pgdat) {
            if (pgdat)
                spin_unlock_irqrestore(&pgdat->lru_lock, flags);
            pgdat = pagepgdat;
            spin_lock_irqsave(&pgdat->lru_lock, flags);
        }

       //找到目标lruvec,最终页转移到该结构中的LRU链表中
        lruvec = mem_cgroup_page_lruvec(page, pgdat);
        (*move_fn)(page, lruvec, arg);  //根据传入的函数进行回调
    }
    if (pgdat)
        spin_unlock_irqrestore(&pgdat->lru_lock, flags);
    //减少page的引用值,当引用值为0时,从LRU链表中移除页表并释放掉
    release_pages(pvec->pages, pvec->nr, pvec->cold);
    //重置pvec结构
    pagevec_reinit(pvec);
}

static void __pagevec_lru_add_fn(struct page *page, struct lruvec *lruvec,
                 void *arg)
{
    int file = page_is_file_cache(page);
    int active = PageActive(page);
    enum lru_list lru = page_lru(page);

    VM_BUG_ON_PAGE(PageLRU(page), page);
    //设置page的状态位,表示处于Active状态
    SetPageLRU(page);
    //加入到链表中
    add_page_to_lru_list(page, lruvec, lru);
    //更新lruvec中的reclaim_state统计信息
    update_page_reclaim_stat(lruvec, file, active);
    trace_mm_lru_insertion(page, lru);
}

具体的分析在注释中标明了,其余4种缓存类型的迁移都大体类似,至于何时进行迁移以及策略,这个在下文中关于内存回收的进一步分析中再阐述。

正常情况下,LRU链表之间的转移是不需要的,只有在需要进行内存回收的时候,才需要去在ACTIVEINACTIVE之间去操作。

进入具体的回收分析吧。

3. 页面回收

3.1 数据结构

memory compact类似,页面回收也有一个与之相关的数据结构:struct scan_control

struct scan_control {
    /* How many pages shrink_list() should reclaim */
    unsigned long nr_to_reclaim;

    /* This context's GFP mask */
    gfp_t gfp_mask;

    /* Allocation order */
    int order;

    /*
     * Nodemask of nodes allowed by the caller. If NULL, all nodes
     * are scanned.
     */
    nodemask_t  *nodemask;

    /*
     * The memory cgroup that hit its limit and as a result is the
     * primary target of this reclaim invocation.
     */
    struct mem_cgroup *target_mem_cgroup;

    /* Scan (total_size >> priority) pages at once */
    int priority;

    /* The highest zone to isolate pages for reclaim from */
    enum zone_type reclaim_idx;

    /* Writepage batching in laptop mode; RECLAIM_WRITE */
    unsigned int may_writepage:1;

    /* Can mapped pages be reclaimed? */
    unsigned int may_unmap:1;

    /* Can pages be swapped as part of reclaim? */
    unsigned int may_swap:1;

    /*
     * Cgroups are not reclaimed below their configured memory.low,
     * unless we threaten to OOM. If any cgroups are skipped due to
     * memory.low and nothing was reclaimed, go back for memory.low.
     */
    unsigned int memcg_low_reclaim:1;
    unsigned int memcg_low_skipped:1;

    unsigned int hibernation_mode:1;

    /* One of the zones is ready for compaction */
    unsigned int compaction_ready:1;

    /* Incremented by the number of inactive pages that were scanned */
    unsigned long nr_scanned;

    /* Number of pages freed so far during a call to shrink_zones() */
    unsigned long nr_reclaimed;
};
  • nr_to_reclaim:需要回收的页面数量;
  • gfp_mask:申请分配的掩码,用户申请页面时可以通过设置标志来限制调用底层文件系统或不允许读写存储设备,最终传递给LRU处理;
  • order:申请分配的阶数值,最终期望内存回收后能满足申请要求;
  • nodemask:内存节点掩码,空指针则访问所有的节点;
  • priority:扫描LRU链表的优先级,用于计算每次扫描页面的数量(total_size >> priority,初始值12),值越小,扫描的页面数越大,逐级增加扫描粒度;
  • may_writepage:是否允许把修改过文件页写回存储设备;
  • may_unmap:是否取消页面的映射并进行回收处理;
  • may_swap:是否将匿名页交换到swap分区,并进行回收处理;
  • nr_scanned:统计扫描过的非活动页面总数;
  • nr_reclaimed:统计回收了的页面总数;

3.2 总体流程分析

与页面压缩类似,有两种方式来触发页面回收:

  1. 内存节点中的内存空闲页面低于low watermark时,kswapd内核线程被唤醒,进行异步回收;
  2. 在内存分配的时候,遇到内存不足,空闲页面低于min watermark时,直接进行回收;

两种方式的调用流程如下图所示:

3.3 直接回收

  • __alloc_pages_slowpath
    该函数调用_perform_reclaim来对页面进行回收处理后,再重新申请分配页面,如果第一次申请失败,将pcp缓存清空后再retry。
  • __perform_reclaim
    该函数中做了以下工作:
  1. 如果设置了cpuset_memory_pressure_enabled,则先更新当前任务的cpuset频率表fmeter;
  2. 将当前任务的标志置上PF_MEMALLOC,防止递归调用页面回收例程;
  3. 调用try_to_free_pages来进行回收处理;
  4. 恢复当前任务的标志;
  • try_to_free_pages
    try_to_free_pages函数中,主要完成了以下工作:
  1. 初始化struct scan_control sc结构;
  2. 调用throttle_direct_reclaim函数进行判断,该函数会对用户任务的直接回收请求进行限制;
  3. 调用do_try_to_free_pages进行回收处理;

再来看看throttle_direct_reclaim函数中调用的alloc_direct_reclaim

只有throttle_direct_reclaim函数返回值为false,页面的回收才会进一步往下执行。

  • do_try_to_free_pages
  1. 通过delayacct_freepages_start/delayacct_freepages_end量化页面回收的时间开销;
  2. 随着回收优先级的调整,通过vmpressure_prio来更新memory pressure值;
  3. 循环调用shrink_zones来回收页面,回收页面足够了或者可以进行内存压缩时,就会跳出循环不再进行回收处理;

3.4 异步回收

kswapd内核线程,当空闲页面低于watermark时会被唤醒,进行页面回收处理,balance_pgdat是回收的主函数,如下图:

异步回收线程和同步直接回收存进程在交互的地方:

  1. 在低水位情况下进程在直接回收时会唤醒kswapd线程;
  2. 异步回收时,kswapd线程也会通过wake_up_all(&pgdat->pfmemalloc_wait)来唤醒等待在该队列上进行同步回收的进程;

kswapd内核线程会在内存节点达到平衡状态时,退出LRU链表的扫描。

3.5 shrink_node

前边铺垫了很多,真正的主角要上场了,不管是同步还是异步的回收,最终都落实在shrink_node函数上。

shrink_node的调用关系如上图所示,下边将针对关键函数进行分析。

  • get_scan_count
    这个函数用于获取针对文件页和匿名页的扫描页面数。这个函数决定内存回收每次扫描多少页,匿名页和文件页分别是多少,比例如何分配等。
    在函数的执行过程中,根据四种扫描平衡的方法标签来最终选择计算方式,四种扫描平衡标签如下:
enum scan_balance {
    SCAN_EQUAL,  // 计算出的扫描值按原样使用
    SCAN_FRACT,  // 将分数应用于计算的扫描值
    SCAN_ANON,  // 对于文件页LRU,将扫描次数更改为0
    SCAN_FILE,     // 对于匿名页LRU,将扫描次数更改为0
};

来一张图:

  • shrink_node_memcg
    shrink_node_memcg函数中,调用了get_scan_count函数之后,获取到了扫描页面的信息后,就开始进入主题对LRU链表进行扫描处理了。它会对匿名页和文件页做平衡处理,选择更合适的页面来进行回收。当回收的页面超过了目标页面数后,将停止对文件页和匿名页两者间LRU页面数少的那一方的扫描,并调整对页面数多的另一方的扫描速度。最后,如果不活跃页面少于活跃页面,则需要将活跃页面迁移到不活跃页面链表中。
    来一张图:
  • shrink_list
    shrink_list函数中主要是从lruvec的链表中进行页面回收:
  1. 仅当活动页面数多于非活动页面数时才调用shrink_active_list对活动链表处理;
  2. 调用shrink_inactive_list对非活动链表进行处理;
  • shrink_active_list
    从函数的调用关系图中可以看出,shrink_active_list/shrink_inactive_list函数都调用了isolate_lru_pages函数,有必要先了解一下这个函数。
    isolate_lru_pages函数,完成的工作就是从指定的lruvec中链表扫描目标数量的页面进行分离处理,并将分离的页面以链表形式返回。而在这个过程中,有些特殊页面不能进行分离处理时,会被rotate到LRU链表的头部。

shrink_active_list的整体效果图如下:

先对LRU ACTIVE链表做isolate操作,这部分操作会分离出来一部分页面,然后再对这些分离页面做进一步的判断,根据最近是否被referenced以及其它标志位做处理,基本上有四种去向:
1)rotate回原来的ACTIVE链表中;
2)处理成功移动到对应的UNACTIVE链表中;
3)不再使用返回Buddy系统;
4)如果出现了不可回收的情况(概率比较低),则放回LRU_UNEVICTABLE链表。

  • shrink_inactive_list
    内存回收的最后一步就是处理LRU_UNACTIVE链表了,该写回存储设备的写回存储设备,该写到Swap分区的写到Swap分区,最终就是释放处理。
    在提供最终效果图之前,先来分析一下shrink_page_list函数,它是shrink_inactive_list的核心。

从上图中可以看出,shrink_page_list函数执行完毕后,页面要不就是rotate回原来的LRU链表中了,要不就是进行回收并最终返回了Buddy System了。

所以,最终的shrink_inactive_list的效果如下图:

页面回收的模块还是挺复杂的,还有很多内容没有深入细扣,比如页面反向映射,memcg内存控制组等。
前前后后看了半个月时间的代码,就此收工。

下一个专题要开始看看SLUB内存分配器了,待续。

原文地址:https://www.cnblogs.com/LoyenWang/p/11827153.html

时间: 2024-10-13 08:31:03

【原创】(十)Linux内存管理 - zoned page frame allocator - 5的相关文章

【原创】(七)Linux内存管理 - zoned page frame allocator - 2

背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本:4.14 ARM64处理器,Contex-A53,双核 使用工具:Source Insight 3.5, Visio 1. 概述 本文将分析Buddy System. Buddy System伙伴系统,是通过将物理内存划分为页面来进行管理的系统,支持连续的物理页面分配和释放.此外,使用与碎片相关的算

【原创】(九)Linux内存管理 - zoned page frame allocator - 4

背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本:4.14 ARM64处理器,Contex-A53,双核 使用工具:Source Insight 3.5, Visio 1. 概述 本文将描述memory compaction,内存碎片整理技术. 内存碎片分为内碎片和外碎片: 内碎片:内存页里边的碎片: 外碎片:内存页之间的碎片,可能会造成连续物理页

【原创】(十四)Linux内存管理之page fault处理

背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本:4.14 ARM64处理器,Contex-A53,双核 使用工具:Source Insight 3.5, Visio 1. 概述 上篇文章分析到malloc/mmap函数中,内核实现只是在进程的地址空间建立好了vma区域,并没有实际的虚拟地址到物理地址的映射操作.这部分就是在Page Fault异常

Linux内存管理6---伙伴算法与slab

1.前言 本文所述关于内存管理的系列文章主要是对陈莉君老师所讲述的内存管理知识讲座的整理. 本讲座主要分三个主题展开对内存管理进行讲解:内存管理的硬件基础.虚拟地址空间的管理.物理地址空间的管理. 本文将主要以X86架构为例来介绍伙伴算法和slab分配 2.伙伴算法概述 块链表 Linux的伙伴算法将所有的空闲页面分成MAX_ORDER+1(MAX_ORDER默认大小为11)个块链表 每个链表中的一个节点指向一个含有2的幂次个页面的块,即页块或简称块 伙伴 大小相同.物理地址连续的两个页块称为伙

伙伴系统之伙伴系统概述--Linux内存管理(十四)

日期 内核版本 架构 作者 GitHub CSDN 2016-09-02 Linux-4.7 X86 & arm gatieme LinuxDeviceDrivers Linux内存管理 1 前景回顾 1.1 Linux内存管理的层次结构 Linux把物理内存划分为三个层次来管理 层次 描述 存储节点(Node) CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 管理区(Zone)

伙伴系统之伙伴系统概述--Linux内存管理(十五)

在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法. Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, 后来Knuth又进行了更深刻的描述. 伙伴系统是一个结合了2的方幂个分配器和空闲缓冲区合并计技术的内存分配方案, 其基本思想很简单. 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另

[转帖]Linux分页机制之分页机制的演变--Linux内存管理(七)

Linux分页机制之分页机制的演变--Linux内存管理(七) 2016年09月01日 20:01:31 JeanCheng 阅读数:4543 https://blog.csdn.net/gatieme/article/details/52402967 ~ 版权声明:本文为博主原创文章 && 转载请著名出处 @ http://blog.csdn.net/gatieme https://blog.csdn.net/gatieme/article/details/52402967 日期 内核版

启动期间的内存管理之初始化过程概述----Linux内存管理(九)

日期 内核版本 架构 作者 GitHub CSDN 2016-06-14 Linux-4.7 X86 & arm gatieme LinuxDeviceDrivers Linux内存管理 在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后内核才能检测到可用内存和寄存器. 而我们今天要讲的boot阶段就是系统初始化阶段使用的内存分配器. 1 前景回顾 1.1

linux内存管理-内核用户空间 【转】

转自:http://blog.chinaunix.net/uid-25909619-id-4491362.html 1,linux内存管理中几个重要的结构体和数组 page unsigned long flags 一组标志,也对页框所在的管理区进行编号 atomic_t _count 该页被引用的次数 atomic_t _mapcount 页框中页表项数目,如果没有则为-1 struct list_head lru 管理page忙碌/空闲链表(inactive_list/active_list)