读 200 个 4 KB 的小文件要 19.62 毫秒,读 1 个 800 KB 的大文件只要 0.38 毫秒——差 52 倍。数据量一样,差的全是 200 次【打开、关闭】的过门开销。
6.7 讲的是 CPU 里的缓存,这一节跳一级:内核把文件内容也缓存起来——这是【读文件看起来很快】的真正原因。
一、页缓存是什么
你 read 一个文件,内核不是每次都去磁盘:它把文件按页读进内存,下次直接从内存给你。 这一份副本叫【页缓存】。它属于内核,不属于任何进程——所有读同一个文件的进程共享它。 所以【第二次读同一个文件快得不像话】——不是磁盘变快了,是根本不读磁盘。 这也解释了 5.13 那个现象:文件刚写完就在页缓存里,第一遍都不慢。
二、四个缓存叠在一起
页缓存:文件的内容,按页存。这是最值钱的一个——数据量最大、命中收益最高。
目录项缓存(dentry):路径到 inode 的对应关系。所以【打开过的路径】再打开很快。
inode 缓存:文件的元数据(大小、权限、块位置)。所以 ls -l 很快。
缓冲区缓存:块设备层面的块。现代内核里它已经和页缓存合并了,但概念还在。
四个叠起来,才是【读文件很快】的全部原因——你以为在读磁盘,其实在读内存。
三、实测:小文件多的代价
200 个 4 KB 的文件,一共 800 KB,连读三遍。 三遍几乎一样快(19.62/17.29/24.95 毫秒)——说明早在缓存里了。 但把它合成 1 个 800 KB 的文件,只要 0.38 毫秒——差 52 倍。 差的全是 200 次 open/close 的过门开销:每次都要查目录项、建文件对象、更新时间戳。 所以【打开文件比读文件贵得多】。这就是为什么程序喜欢【一个文件装很多条】(日志、数据库、tar 包都是这个道理)。
四、页缓存是【借】不是【占】
它算在【可用内存】里(5.1 讲过的 Buffers/Cached),随时能让出来。
所以【缓存占了很多内存】不等于【内存不够】。
反过来说:刚重启的机器读文件慢,跑一会儿就快了——不是磁盘变快,是缓存建起来了。
这也给了两条实用建议:重要服务启动后先【预热】一遍数据;不要在生产机上随手清缓存(drop_caches 会把所有负载变成冷启动)。
五、动手 Demo:页面缓存——文件读过的内容,内核替你留着
这一节造 200 个 4 KB 的小文件连读三遍,再把同样的 800 KB 合成一个文件读一遍,看【打开】这件事到底有多贵。
nfile, blk = 200, 4 * 1024
for i in range(nfile):
with open(os.path.join(d, "f%03d.bin" % i), "wb") as f:
f.write(bytes([i % 251]) * blk)
def scan_all():
tot = 0
for i in range(nfile):
with open(os.path.join(d, "f%03d.bin" % i), "rb") as f:
tot += len(f.read())
return tot
for i in range(3):
t0 = time.perf_counter()
scan_all()
print("第", i + 1, "遍", (time.perf_counter() - t0) * 1000)
实跑输出:
--------------------------------------------------------------------
页面缓存——文件读过的内容,内核替你留着
--------------------------------------------------------------------
① 200 个 4 KB 的小文件,连读三遍
第几遍 耗时 读了多少
------------------------------------
第 1 遍 24.22 毫秒 800 KB
第 2 遍 17.77 毫秒 800 KB
第 3 遍 22.63 毫秒 800 KB
三遍几乎一样快(24.22/17.77/22.63 毫秒)**——这本身就是结论**:
**文件刚写完,内容与目录项都在页缓存里,一遍都没真去磁盘。**
② 同样 800 KB,拆成 200 个文件 vs 合成一个文件
读法 打开几次 耗时
-------------------------------------------------
200 个 4 KB 的小文件 200 次 24.22 毫秒
1 个 800 KB 的大文件 1 次 0.34 毫秒
**数据量一样,慢 72 倍**——**差的全是 200 次【打开、关闭】的过门开销。**
**所以【打开文件】比【读文件】贵得多。**这就是为什么程序喜欢【一个文件装很多条】。
③ 页缓存管的是什么
缓存 缓存的对象 谁受益
--------------------------------------------------------
页缓存 文件的内容(按页) 所有读文件的进程
目录项缓存 路径到 inode 的对应 打开同一个文件
inode 缓存 文件的元数据 stat、ls 之类
缓冲区缓存 块设备的块 文件系统自己
**四个缓存叠在一起,才是【读文件很快】的全部原因。**
**你以为在读磁盘,其实大部分时候在读内存。**
④ 页缓存是【借】不是【占】
**它算在【可用内存】里,随时能让出来**(5.1 讲过的 Buffers/Cached)。
**所以【缓存占了很多内存】不等于【内存不够】。**
**反过来说**:**一台刚重启的机器读文件慢,跑一会儿就快了**——
**不是磁盘变快了,是缓存建起来了。**
三遍 19.62/17.29/24.95 毫秒——没有【第一遍慢】,因为文件刚写完还在缓存里。 同样 800 KB,合成一个文件只要 0.38 毫秒——差 52 倍。 所以这 20 毫秒里,绝大部分不是【读数据】,是【打开文件】。800 KB 数据本身只要 0.38 毫秒。 这个数字很有用:它就是【小文件为什么慢】的定量答案。一个目录塞十万个小文件,光是打开就要好几秒。 工程对策:合并、批量、打包——tar、日志滚动、LSM 树的 SSTable 都是这么来的。
📋 Linux 上看页缓存占了多少:free -h 的 buff/cache 那一列;细看:cat /proc/meminfo | grep -E 'Cached|Dirty|Writeback'。
30 秒实验
free -h,看 buff/cache 这一列有多大。cat /proc/meminfo | grep -E '^Cached|^Dirty',看缓存总量与还没写回的脏页。- 造 1000 个 1 KB 的小文件,量【列出目录】与【逐个读】各多久。
常见误区
- 【读文件就是从磁盘读】。大部分时候是从页缓存读,磁盘只在第一次参与。
- 【小文件慢是因为数据量小所以效率低】。恰恰相反——慢是因为【打开次数多】,与数据量无关。
- 【缓存占内存要清理】。它是借的,随时能让;清了只会让下一次读变慢。
自测三问(点开对答案)
1. 页缓存属于谁?为什么所有进程都能受益? —— **属于内核,不属于任何进程**,**所以读同一个文件的进程共享同一份缓存。** 2. 为什么 200 个小文件比 1 个大文件慢 52 倍? —— **数据量一样,差的是 200 次 open/close 的过门开销**——**查目录项、建文件对象、更新时间戳。** 3. 为什么不能说【缓存占内存是不好的】? —— **页缓存是借的,随时能让出来**;**判断内存够不够看 MemAvailable,不看 MemFree。**- ☐ 6.9 同步写入
相关笔记
- 请求分页 — 文件页与 P 位的关系
- 同步写入 — 缓存什么时候必须落地
- 读取文件的实验 — 顺序与随机的差距
- 🐧 Linux是怎样工作的 · 索引