第六十四章:文件之页
元婴中期涉及内核源码:
一
林小源在内景中走了很久,终于走出了匿名页的领地。
眼前是一片截然不同的景象。那些悬浮的页面不再是光秃秃的空白——每一块都有一根细线连接着远处的某个方向,像脐带一样,把页面和磁盘上的文件绑在一起。
"文件页。"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 是借来的内存——有借有还,再借不难。
/*
* 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");#include <stdio.h>
#include <string.h>
/*
* 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;
}
int main() {
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");
return 0;
}道藏笔记
内核启示
page cache 是文件内容在内存中的缓存,每个文件都有一个 结构来管理它的缓存页面。读文件时内核先在 page cache 里找——命中就直接返回数据,没命中才从磁盘读取并存入缓存。下次再读同一块,就命中了。
写入更有意思:进程调用 write(),数据写入 page cache,页面标记为 dirty,然后 write() 就返回了——不等磁盘写完。dirty 页面由后台线程在合适的时候批量写入磁盘。这种延迟写入的策略让进程不用被磁盘 IO 拖慢,但代价是系统崩溃时 dirty 页面会丢失。需要确保数据落盘的话,得显式调用 。
page cache 的妙处在于它是"可回收"的内存:clean 页面随时可以丢弃(内容和磁盘一致),dirty 页面需要先写入磁盘再释放。多进程读同一个文件共享同一份 page cache,预读算法还能提前加载后续页面。page cache 是文件 I/O 的"加速器"——热数据在内存中。
文件之页之试
读写文件时先在内存里缓存文件内容、命中则避免磁盘 I/O 的机制是什么?