第九十四章:页缓存
斩灵期涉及内核源码:
一
林小源走进一片广阔的平原。平原上铺满了整齐的方格,每个方格大小相同——4KB。无数光点在方格中闪烁,有的明亮,有的暗淡。
"这是什么地方?"林小源踩在一个方格上,感受到脚下的光点温热而活跃。
"页缓存。"一个沉稳的声音从平原深处传来。一个厚重的身影缓缓走来——它的身体由无数页面组成,每一页都在微微发光。"我是文件数据在内存中的家。"
"你就是页缓存?"林小源环顾四周,"这些方格……都是缓存的文件数据?"
"对。"页缓存的声音低沉而有力,"当进程读取文件时,内核先来找我。如果数据在我这里——缓存命中——直接返回,速度极快。如果不在——缓存未命中——才去磁盘读取,读完之后把数据存到我这里,下次再读就快了。"
林小源看到一条光线从远处飞来,落在一个方格上,方格瞬间变亮。"那是从磁盘读来的数据?"
"是的。"页缓存说,"每个方格是一个页面,4KB 大小。我用 xarray 组织这些页面——文件偏移 0 到 4KB 的数据在第一个方格,4KB 到 8KB 在第二个,以此类推。查找速度是 O(log n)。"
二
林小源在页缓存的平原上漫步,注意到一些方格上标着"脏"字。
"那些是什么?"他指着一个标红的方格问。
"脏页。"页缓存的声音变得谨慎,"进程写入文件时,数据先进入我这里——写入页缓存,速度很快。但数据还没有写入磁盘,所以我把这一页标记为'脏'。"
"那什么时候写入磁盘?"
"由 writeback 机制决定。"页缓存说,"内核有一个后台线程,定期检查脏页。当脏页数量超过阈值,或者距离上次写回超过一定时间,就会把脏页批量写入磁盘。"
林小源皱眉:"那如果突然断电呢?脏页里的数据不就丢了?"
页缓存沉默了一瞬:"这是缓存的风险。所以重要数据需要显式调用 ——强制把脏页写入磁盘。数据库就是这样做的。"
"那读取呢?"林小源问,"我听说内核会预读?"
"是的。"页缓存的语气缓和了一些,"当进程读取一个页面时,内核会猜测它可能还会读取相邻的页面,于是提前把后面的页面也读入缓存。这叫预读——减少未来的磁盘 I/O。"
三
林小源注意到平原的边缘在收缩——一些方格在变暗、消失。
"内存不够了?"他问。
"是的。"页缓存的声音有些疲惫,"我占用物理内存。当内存不足时,内核的页面回收算法会淘汰我不活跃的页面——那些长时间没被访问的缓存数据会被丢弃。"
"那你和进程争抢内存?"
"看起来是,但其实不是。"页缓存说,"页缓存使用的内存是可以回收的——当进程需要内存时,内核会优先回收我的页面。这就是为什么 命令显示的'空闲'内存很少——大部分被我占用了,但那些内存随时可以释放。"
林小源恍然大悟:"所以'空闲'不等于'可用'。"
"对。"页缓存说,"真正可用的内存是空闲内存加上可回收的缓存。 命令的 列才是真正的可用内存。"
平原上方浮现出一行光字:MemAvailable: 12288000 kB。
"看到没?"页缓存说,"这才是你能用的内存。"
/*
* 页缓存的工作原理:
*
* 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");#include <stdio.h>
/*
* 页缓存的工作原理:
*
* 1. 读取文件
* 先检查页缓存
* 命中: 直接返回(快)
* 未命中: 从磁盘读取,缓存后返回(慢)
*
* 2. 写入文件
* 写入页缓存(快)
* 标记为"脏页"
* 后续由 writeback 写入磁盘
*
* 3. 缓存淘汰
* 内存不足时,淘汰不活跃的页面
* LRU 算法
*
* 页缓存的好处:
* - 减少磁盘 I/O
* - 加速读取
* - 合并写入
*
* 页缓存的组织:
* address_space 结构
* 每个 inode 有一个 address_space
* 用 radix tree (xarray) 组织
*/
int main() {
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");
return 0;
}道藏笔记
内核启示
页缓存是文件数据在内存中的家。进程读文件时,内核先来页缓存找——找到了直接返回,速度极快;找不到才去磁盘读,读完存到页缓存里,下次再读就快了。
每个 inode 有一个 address_space 结构,用 xarray(以前叫 radix tree)组织页面。文件偏移 0 到 4KB 的数据在第一个页面,4KB 到 8KB 在第二个,查找速度是 O(log n)。
写入也先进页缓存——速度很快,但数据还没写到磁盘,所以页面被标记为"脏"。脏页什么时候写入磁盘,由 writeback 机制决定。如果突然断电,脏页里的数据就丢了,所以重要数据需要显式调用 fsync()。
页缓存占用的物理内存是可以回收的——当进程需要内存时,内核会优先回收不活跃的缓存页面。这就是为什么 命令显示的"空闲"内存很少——大部分被页缓存占了,但那些内存随时可以释放。 列才是真正的可用内存。
页缓存之试
页缓存会占用 buff/cache,但本章提醒看 free 命令时哪一列才接近真正可用内存?