第10章:页面回收
基于 Linux 6.12.38 源码
10.1 页面回收概述
10.1.1 为什么需要页面回收
当系统内存不足时,内核需要回收一些页面以满足新的分配请求。
回收触发条件:
- 水位低于 low watermark
- 分配高阶页面失败
- 用户空间请求更多内存
- 系统进入低内存状态
10.1.2 回收策略
Linux 使用多种策略回收页面:
- LRU 算法: 最近最少使用算法
- 多代 LRU (Multi-Gen LRU): Linux 6.x 默认
- 交换 (Swap): 将页面换出到磁盘
- 直接回收: 同步回收页面
- Slab 收缩: 回收 Slab 缓存
10.2 LRU 链表
10.2.1 LRU 类型定义
位置: include/linux/mmzone.h 和 include/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
| void lru_cache_add(struct page *page); void lru_cache_add_inactive_or_unevictable(struct page *page, struct lruvec *lruvec);
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
| 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; }
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) { shrink_slab(gfp_mask, order);
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;
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
| 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) { 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
| #define MAX_NR_GENS 3
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]; };
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
| 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
| 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
| int register_shrinker(struct shrinker *shrinker);
void unregister_shrinker(struct 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
| 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, };
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
|
10.9.2 /proc/meminfo
1 2 3 4 5 6 7 8 9 10 11
| cat /proc/meminfo
|
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 的页面回收:
- 回收概述: 水位低于 low 时触发回收
- LRU 链表: 活跃/非活跃,匿名/文件页分类
- 页面状态: 新页面 → INACTIVE → ACTIVE → 空闲
- kswapd: 异步回收内核线程
- 直接回收: 同步回收,分配失败时触发
- 回收算法: scan_control,LRU 扫描
- MGLRU: 多代 LRU,更好的页面老化
- 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数据结构) │ │ │ └────────────────────────────────────────────────────────────┘
|
关键回收路径:
异步回收: 水位不足 → wakeup_kswapd() → LRU扫描 → swap/释放 → 水位恢复
- 相关章节: ch02 (watermark), ch12 (swap机制)
直接回收: alloc_pages失败 → do_try_to_free_pages() → 同步回收 → 重试分配
压缩配合: 高阶分配失败 → wakeup_kcompactd() → 页面迁移 → 碎片整理
OOM处理: 回收失败 → OOM触发 → memcg检查 → OOM Killer
- 相关章节: ch14 (memcg, OOM控制)
下一章将介绍内存映射。