Skip to content

第六十三章:匿名之页

元婴中期

涉及内核源码:

林小源在内景中走着,脚下的页表地面突然消失了。

不是走到边缘的那种消失——而是一种更微妙的感觉。他明明踩在实地上,但地面是空的。没有数据,没有内容,什么都没有。

"你踩到匿名页了。"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 = 1000mapcount = 1000flags = 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,永远比内存慢。

他琢磨了一下,忽然明白——匿名页的"无名"是一种负担——没有归属,就没有退路。


c
/*
 * 匿名页的特点:
 * 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");

道藏笔记

内核启示

匿名页是没有关联文件的内存页面——堆、栈、 匿名映射都是。它们的内容首次访问时全零,由 分配。有意思的是,只读访问不会真正分配物理页——内核直接映射到一个全局的零页,几千个进程共享同一块全零的物理内存。只有写入时才触发 COW,分配真正属于进程自己的页面。

零页是个精妙的设计:零是唯一可以被无限共享的值,何必为每个进程分配一块独立的零呢?引用计数可能上万,但内容永远不变。

匿名页和文件页最大的区别在于回收:文件页有磁盘上的文件做后援,clean 的可以直接丢弃,下次从文件重新读取就行。匿名页没有这个退路——回收时必须写入 swap 分区,代价更高。所以内核会优先回收文件页,实在不够了才动匿名页。匿名页是"无名之辈"——没有归属,但有自己的价值。


破关试炼

匿名之页之试

malloc 背后那些没有文件来源、第一次访问才分配的内存页,本章称为什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道