Skip to content

第六十四章:文件之页

元婴中期

涉及内核源码:

林小源在内景中走了很久,终于走出了匿名页的领地。

眼前是一片截然不同的景象。那些悬浮的页面不再是光秃秃的空白——每一块都有一根细线连接着远处的某个方向,像脐带一样,把页面和磁盘上的文件绑在一起。

"文件页。"mm_struct 说,"和匿名页不同,文件页有归属。每一块文件页都对应磁盘上某个文件的某个位置。"

林小源走近一块文件页,伸手触碰。信息涌入脑海——这块页面属于 test.txt 的第 0 页,内容是文件的前 4KB,状态是

"这些文件页存在哪里?"

"page cache。"一个声音从脚下传来。不是 mm_struct——是一个更温和、更沉稳的声音。

林小源低头,看到地面下浮现出一个身影。它不像 do_anonymous_page 那样模糊——它的轮廓清晰,身上覆盖着一层淡金色的光,像一尊佛像。

"我是 。"那个身影说,"每个文件都有一个 结构,管理这个文件在内存中的所有缓存页面。你看到的那些细线——都连在我身上。"

林小源环顾四周。那些文件页确实都连向同一个方向——address_space 的所在地。每一块页面都通过一根细线和它相连,像一张巨大的蛛网。

"当进程调用 read() 时,"address_space 的声音平稳而清晰,"内核先在我的缓存里找——如果找到了,直接返回数据,不需要访问磁盘。这叫 page cache 命中。"

"如果没找到呢?"

"那就从磁盘读取,把数据存入我的缓存,然后返回。下次再读同一块,就命中了。"

林小源看着那些文件页。有的页面散发着温暖的光——那是最近被访问过的。有的页面暗淡冰冷——那是很久没被读取的。

"page cache 就是文件的内存副本。"他喃喃自语。

"不。"address_space 纠正他,"page cache 是文件的加速器。热数据在内存中,冷数据在磁盘上。你读得越频繁的文件,缓存命中率越高,IO 就越少。"

林小源注意到有些文件页的颜色不对。

大多数文件页是淡蓝色的——clean,和磁盘上的内容一致。但有几块页面是暗红色的,表面有细微的裂纹,像被什么东西灼烧过。

"那些是 dirty 页面。"address_space 说。

林小源走近一块暗红色的页面。它属于 test.txt 的第 2 页,内容被修改过了。那根连接磁盘的细线在闪烁——不是正常的淡金色,而是暗红色。

"进程调用 write() 时,"address_space 说,"内核把数据写入 page cache,标记页面为 dirty。但不会立即写入磁盘。"

"为什么不立即写?"

"因为太慢了。"address_space 的声音带了一丝无奈,"磁盘 IO 是整个系统最慢的操作之一。如果每次 write() 都要等磁盘写完,进程会饿死。所以内核采用延迟写入——dirty 页面先留在内存中,由后台线程在合适的时候批量写入磁盘。"

林小源看到远处有几个暗淡的身影在移动——后台刷写线程,pdflush,它们在 dirty 页面之间穿梭,把数据写入磁盘,然后把页面标记为 clean。

"那如果系统崩溃了呢?"林小源问。

address_space 沉默了片刻:"dirty 页面会丢失。"

林小源心里一紧。

"所以有 。"address_space 说,"如果你真的需要确保数据写入磁盘,就调用 ——它会强制把 dirty 页面写入磁盘,等 IO 完成后才返回。但大多数应用不需要这么精确——延迟写入的效率更高。"

林小源看着那些暗红色的 dirty 页面。它们像定时炸弹——只要还在内存中,就有丢失的风险。但正是这种风险,换来了更高的写入效率。

"写入是延迟的——他忽然明白了这一点。"

"延迟是一种策略。"address_space 说,"安全和效率,永远是天平的两端。"

林小源在内景中走了更远,渐渐注意到了一个更大的画面。

那些文件页——无论是 clean 还是 dirty——都占用着物理内存。当内存紧张时,内核需要回收一些页面。

"文件页可以被回收。"mm_struct 的声音重新出现,"但不是所有文件页都一样。"

林小源看到一块 clean 的文件页被内核轻轻一推,页面碎裂成光点消散了。那根连接磁盘的细线也断了。

"clean 文件页可以直接丢弃。"mm_struct 说,"因为它的内容和磁盘上一致——下次需要的时候,从磁盘重新读取就行。不需要 IO 写入,回收成本最低。"

然后他看到一块 dirty 的文件页被回收。这次不一样——内核先把数据写入磁盘,等 IO 完成后,才释放页面。

"dirty 文件页需要先写入磁盘。"mm_struct 说,"回收成本更高,但比匿名页便宜。"

"匿名页呢?"

"匿名页需要写入 swap——成本最高。所以内核会优先回收文件页,实在不够了才动匿名页。"

林小源看着那些文件页。它们像内存世界里的临时居民——来了又走,走了又来。只要文件还在磁盘上,文件页就永远可以被重建。

"page cache 是可回收的内存。"他喃喃自语。

"对。"mm_struct 说,"你看到的那些文件页,占用的内存随时可以被释放。它们是内存的缓冲区——有空间就多缓存一些,没空间就少缓存一些。不会影响系统的稳定性。"

林小源点了点头。他终于理解了 page cache 的本质——它不是进程"拥有"的内存,而是系统"借用"的内存。借来加速文件 IO,需要时随时归还。

说到底,page cache 是借来的内存——有借有还,再借不难。


c
/*
 * page cache 是文件内容在内存中的缓存。
 *
 * 关键数据结构:
 *   address_space — 文件的缓存空间
 *     - host       — 关联的 inode
 *     - i_pages    — 页面缓存(xarray)
 *
 * 文件读取流程:
 *   1. 进程调用 read()
 *   2. 内核查找 page cache
 *     - 命中 → 直接返回数据
 *     - 未命中 → 从磁盘读取,存入 page cache
 *   3. 返回数据给进程
 *
 * 文件写入流程:
 *   1. 进程调用 write()
 *   2. 内核写入 page cache
 *   3. 标记页面为 dirty
 *   4. 后台线程将 dirty 页面写入磁盘
 *   5. write() 返回
 */

struct page {
    int index;          /* 文件中的页索引 */
    int dirty;          /* 是否已修改 */
    int uptodate;       /* 数据是否最新 */
    char data[64];
};

struct address_space {
    const char *filename;
    struct page *pages[8];
    int nr_pages;
};

int page_cache_read(struct address_space *mapping, int index) {
    /* 查找 page cache */
    for (int i = 0; i < mapping->nr_pages; i++) {
        if (mapping->pages[i]->index == index) {
            printf("  [page cache] 命中: 页 %d\n", index);
            return i;
        }
    }
    return -1;
}

printf("=== page cache — 文件页的缓存 ===\n\n");

struct page pages[] = {
    { 0, 0, 1, "文件第 0 页的内容" },
    { 1, 0, 1, "文件第 1 页的内容" },
    { 2, 1, 1, "文件第 2 页(已修改)" },
};
struct address_space mapping = {
    .filename = "test.txt",
    .pages = { &pages[0], &pages[1], &pages[2] },
    .nr_pages = 3,
};

printf("page cache 中的页面:\n");
for (int i = 0; i < mapping.nr_pages; i++) {
    printf("  页 %d: %s%s\n",
           mapping.pages[i]->index,
           mapping.pages[i]->data,
           mapping.pages[i]->dirty ? " [dirty]" : "");
}

/* 读取文件 */
printf("\n--- 文件读取 ---\n");
int idx;

printf("读取页 0:\n");
idx = page_cache_read(&mapping, 0);
if (idx >= 0) printf("  数据: %s\n", mapping.pages[idx]->data);

printf("读取页 1:\n");
idx = page_cache_read(&mapping, 1);
if (idx >= 0) printf("  数据: %s\n", mapping.pages[idx]->data);

printf("读取页 3:\n");
idx = page_cache_read(&mapping, 3);
if (idx < 0) {
    printf("  [page cache] 未命中\n");
    printf("  从磁盘读取,存入 page cache\n");
}

printf("\n--- 文件写入 ---\n");
printf("写入页 1:\n");
printf("  1. 写入 page cache\n");
printf("  2. 标记页面为 dirty\n");
printf("  3. write() 返回(不等待磁盘写入)\n");
printf("  4. 后台线程将 dirty 页面写入磁盘\n\n");

printf("--- page cache 的优势 ---\n");
printf("1. 减少磁盘 I/O\n");
printf("   热数据在内存中,不需要访问磁盘\n\n");

printf("2. 共享缓存\n");
printf("   多个进程读取同一文件,共享 page cache\n\n");

printf("3. 写入合并\n");
printf("   多次写入合并为一次磁盘写入\n\n");

printf("4. 预读 (readahead)\n");
printf("   预测后续读取,提前加载\n");

道藏笔记

内核启示

page cache 是文件内容在内存中的缓存,每个文件都有一个 结构来管理它的缓存页面。读文件时内核先在 page cache 里找——命中就直接返回数据,没命中才从磁盘读取并存入缓存。下次再读同一块,就命中了。

写入更有意思:进程调用 write(),数据写入 page cache,页面标记为 dirty,然后 write() 就返回了——不等磁盘写完。dirty 页面由后台线程在合适的时候批量写入磁盘。这种延迟写入的策略让进程不用被磁盘 IO 拖慢,但代价是系统崩溃时 dirty 页面会丢失。需要确保数据落盘的话,得显式调用

page cache 的妙处在于它是"可回收"的内存:clean 页面随时可以丢弃(内容和磁盘一致),dirty 页面需要先写入磁盘再释放。多进程读同一个文件共享同一份 page cache,预读算法还能提前加载后续页面。page cache 是文件 I/O 的"加速器"——热数据在内存中。


破关试炼

文件之页之试

读写文件时先在内存里缓存文件内容、命中则避免磁盘 I/O 的机制是什么?

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

以修仙之名,悟内核之道