第六十五章:页面回收
元婴中期涉及内核源码:
一
林小源在内景中感觉到了一股压力。
不是物理上的压力——是一种更深层的紧张感,像整个空间在收缩。那些悬浮的 VMA 大陆在微微颤抖,页表地面发出低沉的嗡鸣声。
"内存紧张了。"mm_struct 的声音变得紧迫,"内核需要回收页面。"
林小源环顾四周。远处出现了一个巨大的身影——比 mm_struct 更大,更沉重,像一座移动的山。它的身上覆盖着无数闪烁的光点,每一个光点都是一块物理页面。
"我是 。"那个身影的声音低沉如雷,"内存回收的执行者。当空闲页面不足时,我就醒来。"
林小源看到 shrink_node 身上有两条巨大的链表在流动——一条散发着温暖的金色光芒,另一条是冰冷的银色。
"active_list 和 inactive_list。"shrink_node 说,"所有物理页面都在这两条链表上。最近被访问的页面在 active_list,很久没被访问的页面在 inactive_list。"
"怎么区分'最近'和'很久'?"
"PTE 的 A 位。"shrink_node 说,"硬件在页面被访问时自动设置 A 位。我定期扫描——A 位为 1 的页面,说明最近被访问过,留在 active_list。A 位为 0 的页面,说明很久没被访问,移到 inactive_list。"
林小源看到 shrink_node 伸出巨大的手掌,从 active_list 中抓起一块页面,仔细检查它的 A 位。A 位是 0——它把那块页面扔到了 inactive_list 中。
"这就是页面老化。"shrink_node 说,"时间是最好的裁判。被遗忘的页面,终将被淘汰。"
二
林小源看着 shrink_node 在两条链表之间忙碌着。
每过一段时间,它就会从 active_list 中检查一批页面——A 位为 0 的扔到 inactive_list,然后清除所有页面的 A 位。这样,下一次扫描时,只有真正被访问过的页面才会保留 A 位为 1。
"如果一个页面在 A 位被清除后又被访问了呢?"林小源问。
"硬件会重新设置 A 位。"shrink_node 说,"下次我扫描时,它就留在 active_list。这就是 LRU 的精髓——用硬件的访问记录来判断页面的热度。"
林小源看到 shrink_node 开始从 inactive_list 中挑选页面回收。但不是所有页面都一样——它有严格的优先级。
"先回收 clean 文件页。"shrink_node 说,抓起一块淡蓝色的页面。那块页面的 D 位是 0,和磁盘上的内容一致。shrink_node 轻轻一捏,页面碎裂成光点消散了。
"clean 文件页可以直接丢弃。"它说,"不需要 IO,回收成本最低。"
然后它抓起一块暗红色的页面——dirty 文件页,D 位是 1。这次它没有直接捏碎,而是先把页面的内容写入远处的磁盘,等 IO 完成后才释放。
"dirty 文件页需要先写入磁盘。"shrink_node 说,"回收成本更高,但还能承受。"
最后,它看向一块匿名页——没有文件连接,表面是空白的。它犹豫了一下。
"匿名页没有文件后援。"shrink_node 的声音变得沉重,"回收匿名页需要写入 swap——这是最昂贵的操作。磁盘 IO 比内存访问慢几个数量级。"
林小源看到那块匿名页被 shrink_node 送往远处的 swap 黑洞。整个过程缓慢而沉重。
"所以优先级是:clean 文件页、dirty 文件页、匿名页。"林小源说。
"对。"shrink_node 说,"回收也有成本。能免费回收的,绝不多花一分钱。"
三
林小源在内景中走了更远,渐渐看到了页面回收的全貌。
那些被回收的页面并不是真的消失了——它们只是从物理内存中释放了。文件页的内容还在磁盘上,匿名页的内容在 swap 中。当进程再次访问这些页面时,会触发页 fault,内核重新分配物理页面,把数据读回来。
"回收不是毁灭。"mm_struct 说,"回收是转移。把数据从内存转移到磁盘,腾出空间给更需要的页面。"
林小源看到远处有一些页面在 active_list 和 inactive_list 之间反复跳动——它们时而被访问,时而被遗忘,像潮汐一样涨落。
"这些是热点数据。"mm_struct 说,"频繁被访问,永远不会被回收。它们是系统性能的关键——如果热点数据被回收了,系统会频繁触发 page fault,性能急剧下降。"
"所以 LRU 的目标是保护热点数据?"
"对。"mm_struct 说,"LRU 不是最优算法——最优算法需要预知未来。但 LRU 是最实用的算法——它用历史访问模式来预测未来。大多数情况下,最近被访问的页面,未来也更可能被访问。"
林小源看着那两条流动的链表。active_list 像一条温暖的河流,承载着系统的热点数据。inactive_list 像一条冰冷的溪流,承载着即将被淘汰的页面。shrink_node 站在两条河的交汇处,不断地筛选、回收、释放。
"页面回收是内存管理的最后防线。"他喃喃自语。
"不。"mm_struct 纠正他,"页面回收是内存管理的呼吸。有呼有吸,内存才能流动。只分配不回收,系统会窒息。"
他忽然明白过来——页面回收不只是淘汰——它是内存的呼吸。
/*
* 页面回收使用 LRU(Least Recently Used)算法。
*
* 内核维护两个链表:
* active_list — 活跃页面(最近被访问)
* inactive_list — 不活跃页面(很久没被访问)
*
* 页面在两个链表之间移动:
* 访问页面 → 移到 active_list
* 长时间未访问 → 移到 inactive_list
* 回收时从 inactive_list 中选择页面
*
* PTE 的 A (Accessed) 位:
* 硬件在页面被访问时设置
* 内核定期检查并清除
* 如果清除后再次被访问,页面仍在 active_list
*
* 回收策略:
* 1. 优先回收 clean 文件页(直接丢弃)
* 2. 其次回收 dirty 文件页(先写入磁盘)
* 3. 最后回收匿名页(写入 swap)
*/
struct page {
int index;
int accessed; /* A 位 */
int dirty; /* D 位 */
int type; /* 0=anonymous, 1=file */
char data[32];
};
struct lru_list {
struct page *pages[8];
int count;
const char *name;
};
void lru_add(struct lru_list *lru, struct page *page) {
lru->pages[lru->count++] = page;
}
struct page *lru_reclaim(struct lru_list *lru) {
if (lru->count == 0) return NULL;
return lru->pages[--lru->count];
}
printf("=== 页面回收 — LRU 算法 ===\n\n");
struct lru_list active = { .count = 0, .name = "active_list" };
struct lru_list inactive = { .count = 0, .name = "inactive_list" };
/* 模拟一些页面 */
struct page pages[] = {
{ 0, 1, 0, 1, "热数据(经常访问)" },
{ 1, 0, 0, 1, "冷数据(很久没访问)" },
{ 2, 1, 1, 0, "匿名页(已修改)" },
{ 3, 0, 0, 1, "文件页(clean)" },
{ 4, 0, 1, 0, "匿名页(dirty)" },
};
/* 初始分配到 active_list */
for (int i = 0; i < 5; i++)
lru_add(&active, &pages[i]);
printf("active_list 中的页面:\n");
for (int i = 0; i < active.count; i++)
printf(" 页 %d: %s\n", active.pages[i]->index, active.pages[i]->data);
/* 模拟页面老化 */
printf("\n--- 页面老化 ---\n");
printf("冷数据长时间未访问 → 移到 inactive_list\n\n");
/* 移动冷数据到 inactive_list */
inactive.pages[inactive.count++] = active.pages[1];
active.pages[1] = active.pages[active.count - 1];
active.count--;
printf("active_list:\n");
for (int i = 0; i < active.count; i++)
printf(" 页 %d: %s\n", active.pages[i]->index, active.pages[i]->data);
printf("inactive_list:\n");
for (int i = 0; i < inactive.count; i++)
printf(" 页 %d: %s\n", inactive.pages[i]->index, inactive.pages[i]->data);
printf("\n--- 回收策略 ---\n");
printf("1. 优先回收 clean 文件页(直接丢弃)\n");
printf(" 页 3: clean 文件页 → 直接丢弃\n\n");
printf("2. 其次回收 dirty 文件页(先写入磁盘)\n");
printf(" 需要等待 I/O 完成\n\n");
printf("3. 最后回收匿名页(写入 swap)\n");
printf(" 需要 swap 空间\n");#include <stdio.h>
#include <string.h>
/*
* 页面回收使用 LRU(Least Recently Used)算法。
*
* 内核维护两个链表:
* active_list — 活跃页面(最近被访问)
* inactive_list — 不活跃页面(很久没被访问)
*
* 页面在两个链表之间移动:
* 访问页面 → 移到 active_list
* 长时间未访问 → 移到 inactive_list
* 回收时从 inactive_list 中选择页面
*
* PTE 的 A (Accessed) 位:
* 硬件在页面被访问时设置
* 内核定期检查并清除
* 如果清除后再次被访问,页面仍在 active_list
*
* 回收策略:
* 1. 优先回收 clean 文件页(直接丢弃)
* 2. 其次回收 dirty 文件页(先写入磁盘)
* 3. 最后回收匿名页(写入 swap)
*/
struct page {
int index;
int accessed; /* A 位 */
int dirty; /* D 位 */
int type; /* 0=anonymous, 1=file */
char data[32];
};
struct lru_list {
struct page *pages[8];
int count;
const char *name;
};
void lru_add(struct lru_list *lru, struct page *page) {
lru->pages[lru->count++] = page;
}
struct page *lru_reclaim(struct lru_list *lru) {
if (lru->count == 0) return NULL;
return lru->pages[--lru->count];
}
int main() {
printf("=== 页面回收 — LRU 算法 ===\n\n");
struct lru_list active = { .count = 0, .name = "active_list" };
struct lru_list inactive = { .count = 0, .name = "inactive_list" };
/* 模拟一些页面 */
struct page pages[] = {
{ 0, 1, 0, 1, "热数据(经常访问)" },
{ 1, 0, 0, 1, "冷数据(很久没访问)" },
{ 2, 1, 1, 0, "匿名页(已修改)" },
{ 3, 0, 0, 1, "文件页(clean)" },
{ 4, 0, 1, 0, "匿名页(dirty)" },
};
/* 初始分配到 active_list */
for (int i = 0; i < 5; i++)
lru_add(&active, &pages[i]);
printf("active_list 中的页面:\n");
for (int i = 0; i < active.count; i++)
printf(" 页 %d: %s\n", active.pages[i]->index, active.pages[i]->data);
/* 模拟页面老化 */
printf("\n--- 页面老化 ---\n");
printf("冷数据长时间未访问 → 移到 inactive_list\n\n");
/* 移动冷数据到 inactive_list */
inactive.pages[inactive.count++] = active.pages[1];
active.pages[1] = active.pages[active.count - 1];
active.count--;
printf("active_list:\n");
for (int i = 0; i < active.count; i++)
printf(" 页 %d: %s\n", active.pages[i]->index, active.pages[i]->data);
printf("inactive_list:\n");
for (int i = 0; i < inactive.count; i++)
printf(" 页 %d: %s\n", inactive.pages[i]->index, inactive.pages[i]->data);
printf("\n--- 回收策略 ---\n");
printf("1. 优先回收 clean 文件页(直接丢弃)\n");
printf(" 页 3: clean 文件页 → 直接丢弃\n\n");
printf("2. 其次回收 dirty 文件页(先写入磁盘)\n");
printf(" 需要等待 I/O 完成\n\n");
printf("3. 最后回收匿名页(写入 swap)\n");
printf(" 需要 swap 空间\n");
return 0;
}道藏笔记
内核启示
页面回收是内存管理的核心机制之一。
LRU 算法:
- — 活跃页面
inactive_list— 不活跃页面- 页面在两个链表之间移动
- 回收时从
inactive_list中选择
PTE 的 A(Accessed)位:
- 硬件在页面被访问时设置
- 内核定期检查并清除
- 用于判断页面是否活跃
回收策略的优先级:
- clean 文件页 — 直接丢弃
- dirty 文件页 — 先写入磁盘
- 匿名页 — 写入 swap
页面回收是"淘汰赛"——最不活跃的页面被淘汰。
页面回收之试
页面回收会优先盯住长期不用的页,本章中不活跃页面所在的链表叫什么?