Skip to content

第九十四章:页缓存

斩灵期

涉及内核源码:

林小源走进一片广阔的平原。平原上铺满了整齐的方格,每个方格大小相同——4KB。无数光点在方格中闪烁,有的明亮,有的暗淡。

"这是什么地方?"林小源踩在一个方格上,感受到脚下的光点温热而活跃。

"页缓存。"一个沉稳的声音从平原深处传来。一个厚重的身影缓缓走来——它的身体由无数页面组成,每一页都在微微发光。"我是文件数据在内存中的家。"

"你就是页缓存?"林小源环顾四周,"这些方格……都是缓存的文件数据?"

"对。"页缓存的声音低沉而有力,"当进程读取文件时,内核先来找我。如果数据在我这里——缓存命中——直接返回,速度极快。如果不在——缓存未命中——才去磁盘读取,读完之后把数据存到我这里,下次再读就快了。"

林小源看到一条光线从远处飞来,落在一个方格上,方格瞬间变亮。"那是从磁盘读来的数据?"

"是的。"页缓存说,"每个方格是一个页面,4KB 大小。我用 xarray 组织这些页面——文件偏移 0 到 4KB 的数据在第一个方格,4KB 到 8KB 在第二个,以此类推。查找速度是 O(log n)。"

林小源在页缓存的平原上漫步,注意到一些方格上标着"脏"字。

"那些是什么?"他指着一个标红的方格问。

"脏页。"页缓存的声音变得谨慎,"进程写入文件时,数据先进入我这里——写入页缓存,速度很快。但数据还没有写入磁盘,所以我把这一页标记为'脏'。"

"那什么时候写入磁盘?"

"由 writeback 机制决定。"页缓存说,"内核有一个后台线程,定期检查脏页。当脏页数量超过阈值,或者距离上次写回超过一定时间,就会把脏页批量写入磁盘。"

林小源皱眉:"那如果突然断电呢?脏页里的数据不就丢了?"

页缓存沉默了一瞬:"这是缓存的风险。所以重要数据需要显式调用 ——强制把脏页写入磁盘。数据库就是这样做的。"

"那读取呢?"林小源问,"我听说内核会预读?"

"是的。"页缓存的语气缓和了一些,"当进程读取一个页面时,内核会猜测它可能还会读取相邻的页面,于是提前把后面的页面也读入缓存。这叫预读——减少未来的磁盘 I/O。"

林小源注意到平原的边缘在收缩——一些方格在变暗、消失。

"内存不够了?"他问。

"是的。"页缓存的声音有些疲惫,"我占用物理内存。当内存不足时,内核的页面回收算法会淘汰我不活跃的页面——那些长时间没被访问的缓存数据会被丢弃。"

"那你和进程争抢内存?"

"看起来是,但其实不是。"页缓存说,"页缓存使用的内存是可以回收的——当进程需要内存时,内核会优先回收我的页面。这就是为什么 命令显示的'空闲'内存很少——大部分被我占用了,但那些内存随时可以释放。"

林小源恍然大悟:"所以'空闲'不等于'可用'。"

"对。"页缓存说,"真正可用的内存是空闲内存加上可回收的缓存。 命令的 列才是真正的可用内存。"

平原上方浮现出一行光字:MemAvailable: 12288000 kB

"看到没?"页缓存说,"这才是你能用的内存。"

c
/*
 * 页缓存的工作原理:
 *
 * 1. 读取文件
 *    先检查页缓存
 *    命中: 直接返回(快)
 *    未命中: 从磁盘读取,缓存后返回(慢)
 *
 * 2. 写入文件
 *    写入页缓存(快)
 *    标记为"脏页"
 *    后续由 writeback 写入磁盘
 *
 * 3. 缓存淘汰
 *    内存不足时,淘汰不活跃的页面
 *    LRU 算法
 *
 * 页缓存的好处:
 *   - 减少磁盘 I/O
 *   - 加速读取
 *   - 合并写入
 *
 * 页缓存的组织:
 *   address_space 结构
 *   每个 inode 有一个 address_space
 *   用 radix tree (xarray) 组织
 */

printf("=== 页缓存 — 文件数据的内存缓存 ===\n\n");

printf("页缓存的工作原理:\n\n");

printf("1. 读取文件:\n");
printf("   进程: read(fd, buf, size)\n");
printf("   内核: 检查页缓存\n");
printf("   命中: 直接返回(快)\n");
printf("   未命中: 磁盘读取 → 缓存 → 返回(慢)\n\n");

printf("2. 写入文件:\n");
printf("   进程: write(fd, buf, size)\n");
printf("   内核: 写入页缓存(快)\n");
printf("   标记为"脏页"\n");
printf("   后续: writeback 写入磁盘\n\n");

printf("--- 页缓存的结构 ---\n");
printf("address_space:\n");
printf("  每个 inode 有一个\n");
printf("  用 xarray 组织页面\n");
printf("  页面按文件偏移索引\n\n");

printf("xarray (radix tree):\n");
printf("  [0] → page (偏移 0-4KB)\n");
printf("  [1] → page (偏移 4KB-8KB)\n");
printf("  [2] → NULL (未缓存)\n");
printf("  [3] → page (偏移 12KB-16KB)\n\n");

printf("--- 缓存命中率 ---\n");
printf("命中率 = 缓存命中次数 / 总访问次数\n\n");
printf("高命中率:\n");
printf("  大部分访问在缓存中\n");
printf("  性能好\n\n");
printf("低命中率:\n");
printf("  大部分访问需要磁盘 I/O\n");
printf("  性能差\n\n");

printf("--- 查看页缓存 ---\n");
printf("free -h:\n");
printf("  buff/cache: 2.1G\n\n");
printf("vmstat 1:\n");
printf("  bi: 块设备读入\n");
printf("  bo: 块设备写出\n\n");
printf("/proc/meminfo:\n");
printf("  Cached: 2145678 kB\n");

道藏笔记

内核启示

页缓存是文件数据在内存中的家。进程读文件时,内核先来页缓存找——找到了直接返回,速度极快;找不到才去磁盘读,读完存到页缓存里,下次再读就快了。

每个 inode 有一个 address_space 结构,用 xarray(以前叫 radix tree)组织页面。文件偏移 0 到 4KB 的数据在第一个页面,4KB 到 8KB 在第二个,查找速度是 O(log n)。

写入也先进页缓存——速度很快,但数据还没写到磁盘,所以页面被标记为"脏"。脏页什么时候写入磁盘,由 writeback 机制决定。如果突然断电,脏页里的数据就丢了,所以重要数据需要显式调用 fsync()。

页缓存占用的物理内存是可以回收的——当进程需要内存时,内核会优先回收不活跃的缓存页面。这就是为什么 命令显示的"空闲"内存很少——大部分被页缓存占了,但那些内存随时可以释放。 列才是真正的可用内存。


破关试炼

页缓存之试

页缓存会占用 buff/cache,但本章提醒看 free 命令时哪一列才接近真正可用内存?

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

以修仙之名,悟内核之道