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

简介: 【原创】(十)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:统计回收了的页面总数;

【原创】(十)Linux内存管理 - zoned page frame allocator - 5(2)https://developer.aliyun.com/article/1543823

相关文章
|
8天前
|
监控 Linux 数据处理
探索Linux中的`lsmem`命令:深入了解系统内存布局
`lsmem`是Linux命令,用于显示系统内存布局和大小,帮助管理员和开发者理解内存使用情况。它提供详细输出,包括内存块的大小、范围、类型和关联,支持多种格式展示,如树状图。命令参数如`-h`显示帮助,`-t`以树形展示,`--human-readable`使大小更易读。需root权限运行,常与`free`、`vmstat`等工具结合使用,用于监控和优化内存。注意不同发行版可能存在兼容性差异。
|
8天前
|
Java Linux PHP
【应急响应】后门攻击检测指南&Rookit&内存马&权限维持&WIN&Linux
【应急响应】后门攻击检测指南&Rookit&内存马&权限维持&WIN&Linux
|
1天前
|
存储 机器学习/深度学习 Linux
程序员必知:关于Linux内存寻址与页表处理的一些细节
程序员必知:关于Linux内存寻址与页表处理的一些细节
|
1天前
|
Linux
【linux】共享内存
【linux】共享内存
5 0
|
2天前
|
Linux 芯片
一篇文章讲明白Linux内核态和用户态共享内存方式通信
一篇文章讲明白Linux内核态和用户态共享内存方式通信
|
6天前
|
存储 缓存 Linux
【原创】(十)Linux内存管理 - zoned page frame allocator - 5(2)
【原创】(十)Linux内存管理 - zoned page frame allocator - 5
|
11天前
|
消息中间件 存储 Kafka
实时计算 Flink版产品使用问题之 从Kafka读取数据,并与两个仅在任务启动时读取一次的维度表进行内连接(inner join)时,如果没有匹配到的数据会被直接丢弃还是会被存储在内存中
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
3天前
|
存储 Java C++
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据,如局部变量和操作数;本地方法栈支持native方法;堆存放所有线程的对象实例,由垃圾回收管理;方法区(在Java 8后变为元空间)存储类信息和常量;运行时常量池是方法区一部分,保存符号引用和常量;直接内存非JVM规范定义,手动管理,通过Buffer类使用。Java 8后,永久代被元空间取代,G1成为默认GC。
11 2
|
7天前
|
存储
数据在内存中的存储(2)
数据在内存中的存储(2)
21 5
|
7天前
|
存储 小程序 编译器
数据在内存中的存储(1)
数据在内存中的存储(1)
25 5