Linux内核分析之内存管理-10

韩乔落

第10章:页面回收

基于 Linux 6.12.38 源码


10.1 页面回收概述

10.1.1 为什么需要页面回收

当系统内存不足时,内核需要回收一些页面以满足新的分配请求。

回收触发条件:

  • 水位低于 low watermark
  • 分配高阶页面失败
  • 用户空间请求更多内存
  • 系统进入低内存状态

10.1.2 回收策略

Linux 使用多种策略回收页面:

  1. LRU 算法: 最近最少使用算法
  2. 多代 LRU (Multi-Gen LRU): Linux 6.x 默认
  3. 交换 (Swap): 将页面换出到磁盘
  4. 直接回收: 同步回收页面
  5. Slab 收缩: 回收 Slab 缓存

10.2 LRU 链表

10.2.1 LRU 类型定义

位置: include/linux/mmzone.hinclude/linux/mm_inline.h

1
2
3
4
5
6
7
8
9
10
11
enum lru_list {
LRU_INACTIVE_ANON = 0, /* 非活跃匿名页 */
LRU_ACTIVE_ANON, /* 活跃匿名页 */
LRU_INACTIVE_FILE, /* 非活跃文件页 */
LRU_ACTIVE_FILE, /* 活跃文件页 */
LRU_UNEVICTABLE, /* 不可驱逐页 */
NR_LRU_LISTS
};

#define for_each_lru(lru) for (lru = 0; lru < NR_LRU_LISTS; lru++)
#define for_each_evictable_lru(lru) for (lru = 0; lru <= LRU_ACTIVE_FILE; lru++)

10.2.2 LRU 链表结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌──────────────────────────────────────────────────────────────┐
│ LRU 链表 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 活跃匿名页: [A] ←→ [A] ←→ [A] ←→ [A] ←→ ... │
│ 非活跃匿名页: [I] ←→ [I] ←→ [I] ←→ [I] ←→ ... │
│ 活跃文件页: [F] ←→ [F] ←→ [F] ←→ [F] ←→ ... │
│ 非活跃文件页: [F] ←→ [F] ←→ [F] ←→ [F] ←→ ... │
│ 不可驱逐: [U] ←→ [U] ←→ [U] ←→ [U] ←→ ... │
│ │
└──────────────────────────────────────────────────────────────┘

A = Active (最近被访问)
I = Inactive (最近未被访问)
F = File page (文件映射)
U = Unevictable (mlock, swap cache 等)

10.2.3 LRU 链表操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 添加页面到 LRU */
void lru_cache_add(struct page *page);
void lru_cache_add_inactive_or_unevictable(struct page *page,
struct lruvec *lruvec);

/* 从 LRU 删除 */
void lru_cache_del(struct page *page);

/* 激活页面 */
void mark_page_accessed(struct page *page);

/* 标记为活跃 */
void lru_cache_add_active(struct page *page);

/* 标记为非活跃 */
void lru_cache_add_inactive(struct page *page);

10.3 页面状态转换

10.3.1 状态机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌──────────────────────────────────────────────────────────────────┐
│ │
│ ┌──────────┐ 新分配 ┌────────────┐ │
│ │ 新页面 │ ─────────────→ │ INACTIVE │ │
│ │ │ │ (ANON/FILE) │ │
│ └──────────┘ └─────────────┘ │
│ │ │
│ │ 被引用 │
│ ▼ │
│ ┌────────────┐ │
│ │ ACTIVE │ │
│ │ (ANON/FILE) │ │
│ └─────────────┘ │
│ │ │
│ │ 时间流逝 │
│ ▼ │
│ ┌────────────┐ │
│ │ INACTIVE │ │
│ └─────────────┘ │
│ │ │
│ │ 释放 │
│ ▼ │
│ ┌────────────┐ │
│ │ 空闲 │ │
│ │ Free │ │
│ └────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘

10.3.2 状态转换操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 激活页面 */
static inline void mark_page_accessed(struct page *page)
{
if (!PageActive(page) && !PageUnevictable(page) && PageLRU(page))
activate_page(page);
}

/* 标记为非活跃 */
static inline void lru_cache_add_inactive(struct page *page)
{
if (!PageUnevictable(page))
lru_cache_add(page);
}

/* 标记为不可驱逐 */
void mark_page_unevictable(struct page *page);
void clear_page_unevictable(struct page *page);

10.4 kswapd 内核线程

10.4.1 kswapd 概述

kswapd 是每个内存节点的内核换页线程,负责异步回收页面。

10.4.2 kswapd 主循环

位置: mm/vmscan.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static int kswapd(void *p)
{
struct pglist_data *pgdat = p;
struct task_struct *tsk = current;

/* 设置调度策略 */
tsk->flags |= PF_MEMALLOC | PF_KSWAPD;
set_freezable();

while (!kthread_should_stop()) {
/* 等待需要回收 */
wait_event_freezable(kswapd_wait,
kswapd_run(pgdat));

/* 执行页面回收 */
kswapd_run(pgdat);

/* 平衡节点 */
balance_pgdat(pgdat, order, highest_zoneidx);
}

return 0;
}

10.4.3 回收触发条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 检查是否需要唤醒 kswapd */
static inline bool pgdat_watermark_ok(struct pglist_data *pgdat,
int classzone_idx)
{
int i;

for (i = 0; i <= classzone_idx; i++) {
struct zone *zone = pgdat->node_zones + i;
if (!zone_watermark_ok(zone, order, 0, classzone_idx))
return false;
}
return true;
}

/* 唤醒 kswapd */
static void wakeup_kswapd(struct zone *zone, unsigned int order,
enum zone_type highest_zoneidx)
{
if (!pgdat_watermark_ok(zone->zone_pgdat, highest_zoneidx)) {
if (waitqueue_active(&pgdat->kswapd_wait))
wake_up_interruptible(&pgdat->kswapd_wait);
}
}

10.4.4 kswapd 工作流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌──────────────────────────────────────────────────────────────┐
│ kswapd 工作流程 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 1. 等待唤醒 (kswapd_wait) │
│ │
│ 2. 检查水位 (pgdat_watermark_ok) │
│ - 如果水位 OK,继续等待 │
│ - 如果水位不足,开始回收 │
│ │
│ 3. 执行回收 (balance_pgdat) │
│ - 扫描 LRU 链表 │
│ - 回收非活跃页面 │
│ - 将匿名页换出到 swap │
│ - 释放文件页 │
│ │
│ 4. 检查是否达到水位 │
│ - 如果达到 high watermark,停止回收 │
│ - 如果未达到,继续回收 │
│ │
│ 5. 返回等待状态 │
│ │
└──────────────────────────────────────────────────────────────┘

10.5 直接回收

10.5.1 直接回收触发

当 kswapd 无法快速满足分配请求时,会触发直接回收。

10.5.2 do_try_to_free_pages

位置: mm/vmscan.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
unsigned long do_try_to_free_pages(struct zonelist *zonelist,
int order,
gfp_t gfp_mask)
{
struct scan_control sc = {
.gfp_mask = gfp_mask,
.order = order,
.may_writepage = !laptop_mode,
.nr_to_reclaim = SWAP_CLUSTER_MAX,
.may_unmap = 1,
.may_swap = 1,
};

/* 执行回收扫描 */
while (!sc.proactive) {
/* 收缩 slab */
shrink_slab(gfp_mask, order);

/* 收缩 LRU */
shrink_zones(zonelist, &sc);

/* 检查是否完成 */
if (sc.nr_reclaimed >= sc.nr_to_reclaim)
break;
}

return sc.nr_reclaimed;
}

10.5.3 直接回收流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
alloc_pages() 失败


┌──────────────────────────────────────┐
│ 1. 唤醒 kswapd │
│ - 异步回收 │
└──────────────────────────────────────┘

仍然不够

┌──────────────────────────────────────┐
│ 2. 直接回收 (do_try_to_free_pages) │
│ - 同步回收页面 │
└──────────────────────────────────────┘


┌──────────────────────────────────────┐
│ 3. 收缩 slab │
│ - shrink_slab │
└──────────────────────────────────────┘


┌──────────────────────────────────────┐
│ 4. 收缩 LRU │
│ - shrink_zones │
│ - shrink_node │
└──────────────────────────────────────┘


┌──────────────────────────────────────┐
│ 5. 再次尝试分配 │
└──────────────────────────────────────┘

10.6 页面回收算法

10.6.1 scan_control 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct scan_control {
/* 扫描目标 */
unsigned long nr_to_reclaim; /* 目标回收页面数 */
gfp_t gfp_mask; /* GFP 标志 */

/* 扫描统计 */
unsigned long nr_scanned; /* 已扫描页面数 */
unsigned long nr_reclaimed; /* 已回收页面数 */

/* 扫描选项 */
unsigned int order; /* 分配阶数 */
int may_writepage; /* 是否写回脏页 */
int may_unmap; /* 是否取消映射 */
int may_swap; /* 是否交换 */
int proactive; /* 主动回收 */
};

10.6.2 LRU 扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* 扫描 LRU 链表 */
static void shrink_lruvec(struct lruvec *lruvec, struct scan_control *sc)
{
unsigned long nr_to_scan;
enum lru_list lru;

/* 计算扫描数量 */
while (sc->nr_reclaimed < sc->nr_to_reclaim) {
/* 扫描每个 LRU */
for_each_evictable_lru(lru) {
/* 获取扫描数量 */
nr_to_scan = get_nr_to_scan(lruvec, lru, sc);

/* 扫描页面 */
while (nr_to_scan-- > 0) {
/* 获取页面 */
struct page *page = isolate_lru_page(lruvec, lru);

/* 检查是否可以回收 */
if (!page)
continue;

/* 尝试回收 */
if (!page_reclaimable(page))
putback_lru_page(page);
else
reclaim_page(page, sc);
}
}

/* 检查是否完成 */
if (sc->nr_reclaimed >= sc->nr_to_reclaim)
break;
}
}

10.7 多代 LRU (Multi-Gen LRU)

10.7.1 MGLRU 概述

Linux 6.x 引入了多代 LRU (Multi-Gen LRU,MGLRU),替代了传统的 LRU 算法。

MGLRU 优势:

  • 更好的页面老化机制
  • 减少误杀活跃页面
  • 更好的性能
  • 支持多代页面跟踪

10.7.2 MGLRU 数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* MGLRU 配置 */
#define MAX_NR_GENS 3 /* 最大代数 */

/* MGLRU 状态 */
struct lru_gen_mm_walk {
struct mem_cgroup *memcg;
lru_gen_fn_t fn;
unsigned long bitmap;
unsigned long max_seq;
unsigned long start_seq;
int batch_size;
int nr_pages[MAX_NR_GENS][ANON_AND_FILE];
};

/* MGLRU 统计 */
struct lru_gen_memcg {
struct list_head list;
unsigned long timestamp;
int nr_nodes;
struct lru_gen_mm_walk nodes[];
};

10.7.3 MGLRU 初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 初始化 MGLRU */
static inline void lru_gen_init_lruvec(struct lruvec *lruvec)
{
int i, j;

for (i = 0; i < MIN_NR_GENS + 1; i++) {
for (j = 0; j < ANON_AND_FILE; j++) {
struct lru_gen_struct *lrugen = &lruvec->lrugen[i][j];
lrugen->max_seq = 0;
lrugen->min_seq = 0;
}
}
}

10.8 Slab 收缩

10.8.1 shrinker 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* shrinker 结构 */
struct shrinker {
unsigned long (*count_objects)(struct shrinker *,
struct shrink_control *sc);
unsigned long (*scan_objects)(struct shrinker *,
struct shrink_control *sc);

int seeks; /* 寻找次数 */
long batch; /* 批量大小 */
unsigned long flags;

/* 注册 */
struct list_head list;
void *private_data;
};

10.8.2 注册 shrinker

1
2
3
4
5
6
7
8
9
/* 注册 shrinker */
int register_shrinker(struct shrinker *shrinker);

/* 注销 shrinker */
void unregister_shrinker(struct shrinker *shrinker);

/* 注册动态 shrinker */
int prealloc_shrinker(struct shrinker *shrinker);
void free_prealloced_shrinker(struct shrinker *shrinker);

10.8.3 收缩 Slab

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 收缩 slab 缓存 */
unsigned long shrink_slab(gfp_t gfp_mask, int order)
{
struct shrink_control sc = {
.gfp_mask = gfp_mask,
.nr_to_scan = 0,
.nr_scanned = 0,
.nr_reclaimed = 0,
};

/* 遍历所有 shrinker */
do_shrinker_register(&sc);

return sc.nr_reclaimed;
}

10.9 回收统计

10.9.1 /proc/vmstat

1
2
3
4
5
6
# 查看页面回收统计
cat /proc/vmstat | grep -i reclaim

# 输出
# nr_reclaimed: 12345678 /* 总回收页面数 */
# nr_slab_reclaimable: 12345 /* 可回收 slab 对象 */

10.9.2 /proc/meminfo

1
2
3
4
5
6
7
8
9
10
11
# 查看内存信息
cat /proc/meminfo

# 输出
# Active: 12345678 kB /* 活跃页面 */
# Inactive: 12345678 kB /* 非活跃页面 */
# Active(anon): 1234567 kB /* 活跃匿名页 */
# Inactive(anon): 1234567 kB /* 非活跃匿名页 */
# Active(file): 1234567 kB /* 活跃文件页 */
# Inactive(file): 1234567 kB /* 非活跃文件页 */
# Unevictable: 12345 kB /* 不可驱逐页 */


10.10 内存回收协同机制

10.10.1 回收协同工作流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
┌─────────────────────────────────────────────────────────────────────┐
│ 内存回收协同机制 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 触发条件: free_pages < low_wmark │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ 分配请求 │ │
│ │ alloc_pages(order=2) │ │
│ └──────────────────────────────┬─────────────────────────────┘ │
│ │ │
│ ▼ │
│ 检查水位 (zone_watermark_ok) │
│ │ │
│ ┌──────────────────┼──────────────────┐ │
│ │ │ │ │
│ 水位OK 水位<low 水位<min │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 直接返回 ┌──────────────────┐ OOM Killer │
│ │ 异步回收 │ │ │
│ │ wakeup_kswapd() │ │ │
│ │ 继续尝试分配 │ │ │
│ └────────┬─────────┘ │ │
│ │ │ │
│ │ 仍不足? │ │
│ ▼ ▼ │
│ ┌──────────────────┐ 触发OOM │
│ │ 同步回收 │ │
│ │ do_try_to_free │ │
│ │ _pages() │ │
│ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────┐ │
│ │ 回收协同工作流 │ │
│ ├───────────────────────────────────┤ │
│ │ │ │
│ │ ┌─────────┐ ┌─────────┐ │ │
│ │ │ kswapd │ │kcompactd│ │ │
│ │ │ 后台 │ │ 后台 │ │ │
│ │ │ 回收 │ │ 压缩 │ │ │
│ │ └────┬────┘ └────┬────┘ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ LRU扫描 (shrink_lruvec) │ │ │
│ │ │ ├─ 匿名页 → swap │ │ │
│ │ │ ├─ 文件页 → 释放 │ │ │
│ │ │ └─ Slab → shrink_slab │ │ │
│ │ └─────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ 内存压缩 (compact_zone)│ │ │
│ │ │ └─ 迁移可移动页面 │ │ │
│ │ │ 整理内存碎片 │ │ │
│ │ └─────────────────────────┘ │ │
│ │ │ │
│ └───────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 达到水位要求? │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ │ │ │ │
│ YES NO 继续尝试 │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 分配成功 分配失败 触发OOM │
│ │
│ 相关章节: │
│ ───────── │
│ • ch02 - 水位管理 (watermark) │
│ • ch03 - 页面分配 (快速/慢速路径) │
│ • ch10 - 页面回收 (kswapd, 直接回收) │
│ • ch13 - 内存压缩 (kcompactd, page migration) │
│ • ch14 - memcg (内存限制与 OOM) │
│ │
└─────────────────────────────────────────────────────────────────────┘

10.10.2 回收组件协作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
┌────────────────────────────────────────────────────────────┐
│ 回收组件协作图 │
├────────────────────────────────────────────────────────────┤
│ │
│ 分配请求 (alloc_pages) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 检查水位 (watermark) │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ├──► 水位 < min ──► OOM Killer │ │
│ │ (ch14:memcg) │
│ │ │
│ ├──► 水位 < low ──► wakeup_kswapd() │
│ │ │ │
│ │ ▼ │
│ │ ┌────────────────┐ │
│ │ │ kswapd │ │
│ │ │ 异步回收 │ │
│ │ └───────┬────────┘ │
│ │ │ │
│ │ ├──► LRU扫描 │
│ │ │ (匿名→swap) │
│ │ │ (文件→释放) │
│ │ │ │
│ │ └───► 唤醒 kcompactd │
│ │ (ch13) │
│ │ │
│ └──► 仍然不足 ──► do_try_to_free_pages() │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ 直接回收 │ │
│ │ (同步阻塞) │ │
│ └───────┬───────┘ │
│ │ │
│ ├──► shrink_lruvec │
│ │ (回收页面) │
│ │ │
│ ├──► shrink_slab │
│ │ (回收缓存) │
│ │ │
│ └──► compact_zone │
│ (内存压缩) │
│ │
└────────────────────────────────────────────────────────────┘

10.11 本章小结与跨章节关联

10.11.1 本章要点

本章介绍了 Linux 6.12 的页面回收:

  1. 回收概述: 水位低于 low 时触发回收
  2. LRU 链表: 活跃/非活跃,匿名/文件页分类
  3. 页面状态: 新页面 → INACTIVE → ACTIVE → 空闲
  4. kswapd: 异步回收内核线程
  5. 直接回收: 同步回收,分配失败时触发
  6. 回收算法: scan_control,LRU 扫描
  7. MGLRU: 多代 LRU,更好的页面老化
  8. Slab 收缩: shrinker 接口

10.11.2 与其他章节的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌────────────────────────────────────────────────────────────┐
│ 本章内容 (页面回收) │
├────────────────────────────────────────────────────────────┤
│ │
│ watermark检查 ──▶ ch02:Zone水位 │
│ (min/low/high/promo) │
│ │
│ kswapd唤醒 ────▶ ch02:pglist_data (kswapd_wait/kswapd) │
│ │
│ LRU扫描 ───────▶ ch02:Page状态 (PG_active/PG_lru) │
│ │
│ shrink_lruvec ──▶ ch02:lruvec (LRU向量管理) │
│ │
│ 匿名页回收 ────▶ ch12:Swap (换出到磁盘) │
│ │
│ 文件页回收 ────▶ ch12:页缓存 (释放干净页面) │
│ │
│ shrink_slab ───▶ ch08:Slab分配器 (收缩slab缓存) │
│ │
│ 直接回收 ──────▶ ch07:alloc_pages (分配失败触发) │
│ │
│ kcompactd ─────▶ ch13:内存压缩 (整理碎片) │
│ │
│ OOM Killer ────▶ ch14:memcg (内存限制触发) │
│ │
│ MGLRU ─────────▶ ch02:lruvec (多代LRU数据结构) │
│ │
└────────────────────────────────────────────────────────────┘

关键回收路径:

  1. 异步回收: 水位不足wakeup_kswapd()LRU扫描swap/释放水位恢复

    • 相关章节: ch02 (watermark), ch12 (swap机制)
  2. 直接回收: alloc_pages失败do_try_to_free_pages()同步回收重试分配

    • 相关章节: ch07 (页面分配)
  3. 压缩配合: 高阶分配失败wakeup_kcompactd()页面迁移碎片整理

    • 相关章节: ch13 (内存压缩)
  4. OOM处理: 回收失败OOM触发memcg检查OOM Killer

    • 相关章节: ch14 (memcg, OOM控制)

下一章将介绍内存映射。

  • Title: Linux内核分析之内存管理-10
  • Author: 韩乔落
  • Created at : 2026-01-20 21:39:32
  • Updated at : 2026-02-24 14:05:23
  • Link: https://jelasin.github.io/2026/01/20/Linux内核分析之内存管理-10/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments