LRU链表介绍

本文详细介绍了Linux内存管理中的LRU(Last Recently Used)链表,包括LRU链表组织、LRU Cache、LRU移动操作及回收策略。文章深入探讨了LRU链表的加入、更新、回收过程,如page加入LRU的操作,以及LRU回收涉及的Swappiness参数、反向映射和代码实现。此外,还提到了其他内存回收方式,如Shrinker、内存整理(Compact)和KSM内存合并。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文章目录

1. 简介

Buddy 的内存分配和释放算法还是比较简单明晰的。分配的时候优先找 order 相等的空闲内存链表,找不到的话就去找 order 更大的空闲内存链表;释放的话先释放到对应 order 的空闲内存链表,然后尝试和 buddy 内存进行合并,尽量合并成更大的空闲内存块。

但是内存管理加入 缺页(PageFault) 和 回收(Reclaim) 以后,情况就变得异常复杂了。Linux 为了用同样多的内存养活更多的进程和服务操碎了心:

  • PageFault:对新创建的进程,除了内核态的内存不得不马上分配,对用户态的内存严格遵循 lazy 的延后分配策略,对私有数据设置 COW(Copy On Write) 策略只有在更改的时候才会触发 PageFault 重新分配物理页面,对新的文件映射也只是简单的分配VMA只有在实际访问的时候才会触发 PageFault 分配物理页面。
  • Reclaim:在内存不够用的情况下,Linux 尝试回收一些内存。对于从文件映射的内存 (FileMap),因为文件中有备份所以先丢弃掉内存,需要访问时再重新触发 PageFault 从文件中加载;对于没有文件映射的内存 (AnonMap),可以先把它交换到 Swap 分区上去,需要访问时再重新触发 PageFault 从 Swap 中加载。

本篇文章就来详细的分析上述的 缺页(PageFault) 和 回收(Reclaim) 过程。

2. LRU 组织

用户态的内存(FileMap + AnonMap)的内存回收的主要来源。为了尽量减少内存回收锁引起的震荡,刚刚回收的内存马上又被访问需要重新分配。Linux 设计了 LRU (Last Recent Use) 链表,来优先回收最长时间没有访问的内存。

2.1 LRU 链表

在这里插入图片描述
设计了 5 组 LRU 链表:

enum lru_list {
	LRU_INACTIVE_ANON = LRU_BASE,							// inactive 匿名
	LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,				// active 匿名
	LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,				// inactive 文件
	LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,		// active 文件
	LRU_UNEVICTABLE,										// 不可回收内存
	NR_LRU_LISTS
};

在 4.8 版本以前是每个 zone 拥有独立的 lru 链表,在 4.8 版本以后改成了每个 node 一个 lru 链表:

typedef struct pglist_data {
    struct lruvec lruvec; 
    ... 
}
struct lruvec {
    struct list_head lists[NR_LRU_LISTS]; // lru 链表
    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
};

2.2 LRU Cache

为了减少多个CPU在操作LRU链表时的拿锁冲突,系统设计了 PerCPU 的 lru cache。每个 cache 能容纳 14 个 page,一共定义了以下几类 cache:

static DEFINE_PER_CPU(struct pagevec, lru_add_pvec);				// 将不处于lru链表的新页放入到lru链表中
static DEFINE_PER_CPU(struct pagevec, lru_rotate_pvecs);			// 将非活动lru链表中的页移动到非活动lru链表尾部
static DEFINE_PER_CPU(struct pagevec, lru_deactivate_file_pvecs);	// 将处于活动lru链表的页移动到非活动lru链表
static DEFINE_PER_CPU(struct pagevec, lru_lazyfree_pvecs);
#ifdef CONFIG_SMP
static DEFINE_PER_CPU(struct pagevec, activate_page_pvecs);			// 将处于非活动lru链表的页移动到活动lru链表
#endif
/* 14 pointers + two long’s align the pagevec structure to a power of two */
#define PAGEVEC_SIZE 14

struct pagevec {
    unsigned long nr;
    bool percpu_pvec_drained;
    struct page *pages[PAGEVEC_SIZE];
};

2.3 LRU 移动操作

page 可以加入到 lru 链表,并且根据条件在 active/inactive 链表间移动。

2.3.1 page 加入 LRU

关于 lru 的操作,其中最重要的是把 新分配的 page 加入到 lru 中。这部分工作一般由 do_page_fault() 来处理。

do_page_fault() 分配内存 page,以及把新 page 加入到 lru 的典型场景如下:

场景 入口函数 LRU函数调用关系 条件 LRU链表 LRU list page flags
匿名内存第一次发生缺页 do_anonymous_page() lru_cache_add_active_or_unevictable() → lru_cache_add() → __lru_cache_add() → __pagevec_lru_add() → pagevec_lru_move_fn() → __pagevec_lru_add_fn() !(vma->vm_flags & VM_LOCKED) 匿名 active lru pglist_data->lruvec.lists[LRU_ACTIVE_ANON] PG_swapbacked + PG_active + PG_lru
lru_cache_add_active_or_unevictable() → add_page_to_unevictable_list() (vma->vm_flags & VM_LOCKED) 不可回收 lru pglist_data->lruvec.lists[LRU_UNEVICTABLE] PG_swapbacked + PG_unevictable + PG_lru
匿名内存被swap出去后发生缺页 do_swap_page() lru_cache_add_active_or_unevictable()
私有文件内存写操作缺页 do_cow_fault() finish_fault() → alloc_set_pte() → lru_cache_add_active_or_unevictable() !(vma->vm_flags & VM_LOCKED) 匿名 active lru pglist_data->lruvec.lists[LRU_ACTIVE_ANON] PG_swapbacked + PG_active + PG_lru
finish_fault() → alloc_set_pte() → lru_cache_add_active_or_unevictable() (vma->vm_flags & VM_LOCKED) 不可回收 lru pglist_data->lruvec.lists[LRU_UNEVICTABLE] PG_swapbacked + PG_unevictable + PG_lru
私有内存写操作缺页 do_wp_page() wp_page_copy() → lru_cache_add_active_or_unevictable()
文件内存读操作缺页 do_read_fault() pagecache_get_page() → add_to_page_cache_lru() → lru_cache_add() - 文件 lru pglist_data->lruvec.lists[LRU_INACTIVE_FILE / LRU_ACTIVE_FILE] PG_active + PG_lru
共享文件内存写操作缺页 do_shared_fault() -

其中核心部分的代码分析如下:

static void __lru_cache_add(struct page *page)
{
	struct pagevec *pvec = &get_cpu_var(lru_add_pvec);
get_page(page); /* (1) 首先把 page 加入到 lru cache 中 */ if (!pagevec_add(pvec, page) || PageCompound(page)) /* (2) 如果 lru cache 空间已满,把page加入到各自对应的 lru 链表中 */ __pagevec_lru_add(pvec); put_cpu_var(lru_add_pvec); 

}

void __pagevec_lru_add(struct pagevec pvec)
{
/
 (2.1) 遍历 lru cache 中的 page,根据 page 的标志把 page 加入到不同类型的 lru 链表 */
pagevec_lru_move_fn(pvec, __pagevec_lru_add_fn, NULL);
}

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);
/
 (2.1.1) 根据 page 中的标志,获取到 page 想要加入的 lru 类型 */
enum lru_list lru = page_lru(page);

VM_BUG_ON_PAGE(PageLRU(page), page);

/* (2.1.2) 加入 lru 链表的 page 设置 PG_lru 标志 */
SetPageLRU(page);
/* (2.1.3) 加入 lru 链表 */
add_page_to_lru_list(page, lruvec, lru);
update_page_reclaim_stat(lruvec, file, active);
trace_mm_lru_insertion(page, lru);

}

static __always_inline enum lru_list page_lru(struct page *page)
{
enum lru_list lru;

/* (2.1.1.1) page 标志设置了 PG_unevictable,lru = LRU_UNEVICTABLE */
if (PageUnevictable(page))
	lru = LRU_UNEVICTABLE;
/* (2.1.1.2) page 标志设置:
	PG_swapbacked,lru = LRU_INACTIVE_ANON
	PG_swapbacked +  PG_active,lru = LRU_ACTIVE_ANON
	,lru = LRU_INACTIVE_FILE
	PG_active,lru = LRU_ACTIVE_FILE
 */
else {
	lru = page_lru_base_type(page);
	if (PageActive(page))
		lru += LRU_ACTIVE;
}
return lru;

}

static inline enum lru_list page_lru_base_type(struct page *page)
{
if (page_is_file_cache(page))
return LRU_INACTIVE_FILE;
return LRU_INACTIVE_ANON;
}

static inline int page_is_file_cache(struct page *page)
{
return !PageSwapBacked(page);
}

2.3.2 其他 LRU 移动操作

action function
将处于非活动链表中的页移动到非活动链表尾部 rotate_reclaimable_page() → pagevec_move_tail() → pagevec_move_tail_fn()
将活动lru链表中的页加入到非活动lru链表中 deactivate_page() → lru_deactivate_file_fn()
将非活动lru链表的页加入到活动lru链表 activate_page() → __activate_page()

3. LRU 回收

使用 LRU 回收内存的大概流程如下所示:

在这里插入图片描述

3.1 LRU 更新

为了减少对性能的影响,系统把加入到 LRU 的内存 page 分为 active 和 inactive,从 inactive 链表中回收内存。page 近期被访问过即 active,近期没有被访问即 inactive。

系统并不会设计一个定时器,而是通过判断两次扫描之间 PTE 中的 Accessed bit 没有被置位,从而来判断对应 page 有没有被访问过。每次扫描完会清理 Accessed bit:
在这里插入图片描述

一个 page 可能会被多个 vma 锁映射,系统通过 反向映射 找到所有 vma ,并统计 有多少个 vma 的 pte 被访问过 accessed。

page_referenced()
  • 1、在 active 链表回收扫描函数 shrink_active_list() 中的处理:
shrink_node() → shrink_node_memcg() → shrink_list() → shrink_active_list():

shrink_active_list()
{

/* (1) page 对于的任一 vma 的 pte Accessed bit 有被置位,且是代码段