第六十三章:匿名之页
元婴中期涉及内核源码:
一
林小源在内景中走着,脚下的页表地面突然消失了。
不是走到边缘的那种消失——而是一种更微妙的感觉。他明明踩在实地上,但地面是空的。没有数据,没有内容,什么都没有。
"你踩到匿名页了。"mm_struct 说。
"匿名页?"
"没有关联任何文件的页面。"mm_struct 的声音平淡得像在念清单,"堆、栈、 匿名映射——这些都是匿名页。它们没有文件归属,没有名字,是内存世界里的无名之辈。"
林小源低头看脚下的地面。确实——这块页面没有连接任何文件,不像那些文件页,总有一根细线连向磁盘上的某个位置。它就是一块光秃秃的空白地。
"那它的内容是什么?"
"零。"mm_struct 说,"全零。 分配的页面,首次访问时内容全零。这是内核的承诺——你申请一块新内存,里面一定是干净的。"
一个声音从脚下的地面传来,轻得像风:"我是 。"
林小源低头,看到地面下浮现出一个模糊的身影。没有面孔,没有特征,像一团淡淡的雾气。
"你看起来……很普通。"林小源说。
"我就是普通的。"do_anonymous_page 的声音没有怨气,只有一种平静,"我没有文件做后援,没有名字做标记。我分配页面,清零,更新页表,然后消失。这就是我的全部。"
"那为什么还需要你?直接用文件页不就行了?"
"因为不是所有内存都需要关联文件。"do_anonymous_page 说,"你调用 时,堆上分配的内存——那是匿名页。你函数调用时,栈上创建的局部变量——那也是匿名页。你用 mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) 映射的内存——还是匿名页。"
林小源沉默了。他想起自己的修炼过程中,大量的内存操作都是在匿名页上完成的。
"你虽然无名,"他轻声说,"但无处不在。"
do_anonymous_page 没有回答,只是慢慢沉回地面之下。
二
林小源继续走着,忽然注意到远处有一块特别亮的地面。
那块地面和其他匿名页不同——它散发着柔和的白光,表面光滑如镜。更奇怪的是,他看到好几个进程的光团都踩在上面,各自读取着同样的内容。
"那是什么?"
"零页。"mm_struct 说,"一个特殊的全局物理页。内容全零,被所有只读的匿名页共享。"
林小源走近那块地面。它确实和其他匿名页不同——表面没有任何标记,纯粹的白,像一块永远不会被污染的雪地。他能感觉到脚下的温度——冰凉的,安静的,没有任何数据在流动。
"所有进程的只读匿名页都映射到这里?"
"对。"mm_struct 说,"当进程首次以只读方式访问匿名页时, 不会分配新页面——直接映射到零页就行。零页的引用计数可能有几千,甚至几万。"
林小源蹲下来,伸手触碰零页的表面。一股信息涌入脑海——refcount = 1000,mapcount = 1000,flags = PG_anonymous。内容全零,永远不变。
"这是一种极致的共享。"他喃喃自语。
"对。"mm_struct 说,"零是唯一可以被无限共享的值。所有进程读到的零都是一样的——何必为每个进程分配一块独立的零呢?"
"但如果进程要写入呢?"
"那就触发 COW。"mm_struct 说,"零页的 PTE 是只读的。写入时,内核分配一块新页面,内容清零,更新页表。从那一刻起,那个进程有了自己的匿名页,不再是零页的共享者。"
林小源站起来,看着那块安静的零页。它像一块基石,无数进程的匿名页都从它开始。只有当进程真正需要写入时,才会从它身上分离出去。
"零也是一种共享。"他轻声说。
三
林小源在内景中走得更远了,渐渐注意到一个问题。
那些匿名页——尤其是堆上的——没有连接任何文件。但文件页不同,每一块文件页都有一根细线连向磁盘上的某个位置。如果文件页被回收了,下次可以从文件重新读取。但匿名页呢?
"匿名页没有文件后援。"他说出了自己的担忧。
mm_struct 的声音变得严肃:"你发现了匿名页的致命弱点。"
林小源看到远处有一块匿名页正在变暗——内存紧张时,内核想要回收它。但它没有文件可以回退,丢弃它就意味着数据永久丢失。
"那怎么办?"
"swap。"mm_struct 说,"匿名页必须写入 swap 分区才能被回收。swap 是匿名页的后援——不是文件,但作用一样。把匿名页的内容写入 swap 空间,然后释放物理页面。下次访问时,再从 swap 读回来。"
林小源看到那块变暗的匿名页被一股力量吸走,消失在远处的一个黑洞里。那黑洞散发着冷冽的气息——swap 分区。
"但 swap 很慢。"mm_struct 补充道,"磁盘 IO 比内存访问慢几个数量级。所以内核会尽量先回收文件页——clean 的文件页可以直接丢弃,不需要 IO。只有在文件页不够用的时候,才会动匿名页。"
"所以匿名页是最后的防线?"
"最后的防线。"mm_struct 说,"也是最昂贵的防线。每回收一块匿名页,都意味着一次 swap 写入。如果系统频繁回收匿名页——那就是在用磁盘的速度做内存的事情。"
林小源看着那个黑洞,心里多了一层理解。匿名页虽然无名,但它们的代价比文件页高得多。没有文件后援,就只能依赖 swap。而 swap,永远比内存慢。
他琢磨了一下,忽然明白——匿名页的"无名"是一种负担——没有归属,就没有退路。
/*
* 匿名页的特点:
* 1. 没有关联文件
* 2. 内容全零(首次访问时)
* 3. 不能从文件恢复(需要 swap)
* 4. 使用 anon_vma 管理
*
* 匿名页的分配:
* do_anonymous_page()
* 1. 分配一个新的物理页
* 2. 清零
* 3. 更新页表
* 4. 返回
*
* 零页 (zero page):
* 一个特殊的全局物理页,内容全零
* 所有只读的匿名页都映射到零页
* 只有在写入时才分配真正的物理页(COW)
*/
struct page {
int refcount;
int mapcount; /* 映射计数 */
unsigned long flags;
char data[64];
};
#define PG_anonymous (1 << 0)
#define PG_swapcache (1 << 1)
#define PG_lru (1 << 2)
printf("=== 匿名页 — 无名之页 ===\n\n");
/* 零页 */
struct page zero_page = {
.refcount = 1000, /* 被大量进程共享 */
.mapcount = 1000,
.flags = PG_anonymous,
};
for (int i = 0; i < 64; i++) zero_page.data[i] = 0;
printf("零页:\n");
printf(" refcount = %d(被大量进程共享)\n", zero_page.refcount);
printf(" 内容全零\n\n");
/* 进程首次访问匿名页 */
printf("进程首次访问匿名页:\n");
printf(" 1. 触发页 fault\n");
printf(" 2. do_anonymous_page() 被调用\n");
printf(" 3. 只读访问 → 映射到零页\n");
printf(" 4. 写入访问 → COW,分配新页面\n\n");
/* COW 后的页面 */
struct page new_page = {
.refcount = 1,
.mapcount = 1,
.flags = PG_anonymous,
};
sprintf(new_page.data, "进程写入的数据");
printf("COW 后的新页面:\n");
printf(" refcount = %d(独占)\n", new_page.refcount);
printf(" 内容: %s\n", new_page.data);
printf("\n--- 匿名页 vs 文件页 ---\n");
printf("匿名页:\n");
printf(" - 没有关联文件\n");
printf(" - 内容全零(首次访问)\n");
printf(" - 回收时需要写入 swap\n");
printf(" - 使用 anon_vma 管理\n\n");
printf("文件页:\n");
printf(" - 关联一个文件\n");
printf(" - 内容来自文件\n");
printf(" - 回收时可以直接丢弃(未修改)\n");
printf(" - 使用 address_space 管理\n");
printf("\n--- 匿名页的生命周期 ---\n");
printf("1. 分配: do_anonymous_page()\n");
printf("2. 使用: 进程读写\n");
printf("3. 回收: 写入 swap 或丢弃\n");
printf("4. 释放: 引用计数降为 0\n");#include <stdio.h>
/*
* 匿名页的特点:
* 1. 没有关联文件
* 2. 内容全零(首次访问时)
* 3. 不能从文件恢复(需要 swap)
* 4. 使用 anon_vma 管理
*
* 匿名页的分配:
* do_anonymous_page()
* 1. 分配一个新的物理页
* 2. 清零
* 3. 更新页表
* 4. 返回
*
* 零页 (zero page):
* 一个特殊的全局物理页,内容全零
* 所有只读的匿名页都映射到零页
* 只有在写入时才分配真正的物理页(COW)
*/
struct page {
int refcount;
int mapcount; /* 映射计数 */
unsigned long flags;
char data[64];
};
#define PG_anonymous (1 << 0)
#define PG_swapcache (1 << 1)
#define PG_lru (1 << 2)
int main() {
printf("=== 匿名页 — 无名之页 ===\n\n");
/* 零页 */
struct page zero_page = {
.refcount = 1000, /* 被大量进程共享 */
.mapcount = 1000,
.flags = PG_anonymous,
};
for (int i = 0; i < 64; i++) zero_page.data[i] = 0;
printf("零页:\n");
printf(" refcount = %d(被大量进程共享)\n", zero_page.refcount);
printf(" 内容全零\n\n");
/* 进程首次访问匿名页 */
printf("进程首次访问匿名页:\n");
printf(" 1. 触发页 fault\n");
printf(" 2. do_anonymous_page() 被调用\n");
printf(" 3. 只读访问 → 映射到零页\n");
printf(" 4. 写入访问 → COW,分配新页面\n\n");
/* COW 后的页面 */
struct page new_page = {
.refcount = 1,
.mapcount = 1,
.flags = PG_anonymous,
};
sprintf(new_page.data, "进程写入的数据");
printf("COW 后的新页面:\n");
printf(" refcount = %d(独占)\n", new_page.refcount);
printf(" 内容: %s\n", new_page.data);
printf("\n--- 匿名页 vs 文件页 ---\n");
printf("匿名页:\n");
printf(" - 没有关联文件\n");
printf(" - 内容全零(首次访问)\n");
printf(" - 回收时需要写入 swap\n");
printf(" - 使用 anon_vma 管理\n\n");
printf("文件页:\n");
printf(" - 关联一个文件\n");
printf(" - 内容来自文件\n");
printf(" - 回收时可以直接丢弃(未修改)\n");
printf(" - 使用 address_space 管理\n");
printf("\n--- 匿名页的生命周期 ---\n");
printf("1. 分配: do_anonymous_page()\n");
printf("2. 使用: 进程读写\n");
printf("3. 回收: 写入 swap 或丢弃\n");
printf("4. 释放: 引用计数降为 0\n");
return 0;
}道藏笔记
内核启示
匿名页是没有关联文件的内存页面——堆、栈、 匿名映射都是。它们的内容首次访问时全零,由 分配。有意思的是,只读访问不会真正分配物理页——内核直接映射到一个全局的零页,几千个进程共享同一块全零的物理内存。只有写入时才触发 COW,分配真正属于进程自己的页面。
零页是个精妙的设计:零是唯一可以被无限共享的值,何必为每个进程分配一块独立的零呢?引用计数可能上万,但内容永远不变。
匿名页和文件页最大的区别在于回收:文件页有磁盘上的文件做后援,clean 的可以直接丢弃,下次从文件重新读取就行。匿名页没有这个退路——回收时必须写入 swap 分区,代价更高。所以内核会优先回收文件页,实在不够了才动匿名页。匿名页是"无名之辈"——没有归属,但有自己的价值。
匿名之页之试
malloc 背后那些没有文件来源、第一次访问才分配的内存页,本章称为什么?