两个问题要分开看:数据在不在内存里(页缓存的事),这一次访问要不要陷进内核(页表 P 位的事)。两个都到位才快,少任何一个都得等。
5.12 讲了【碰哪页读哪页】,这一节把【碰】这个动作拆开——什么情况下要陷进内核,什么情况下不用。
一、机制本身
进程访问一个虚拟地址,CPU 先查页表项。 P 位是 1:直接访问物理内存,一次都不打扰内核。 P 位是 0:触发缺页异常,陷进内核。内核决定是【分配新页】、【从 Swap 换回】还是【从文件读】。 所以【请求分页】的意思是:页不是提前全给你的,是你请求到哪页才给哪页。 这就是 5.7 里【要地址不花钱、碰页才花钱】的机制解释。
二、两个问题要分开
问题一:数据在不在物理内存里。这是页缓存管的事(6.8)。 问题二:这一次访问要不要陷进内核。这是页表 P 位管的事。 两种情况会混淆:文件数据在缓存里,但你的进程页表还没映射它——于是你还是要缺一次页,只是这次不用读盘,很快。 反过来:页表映射好了,但那一页被换出(Swap)了——于是你缺页,而且要去读盘,很慢。 所以【快】= 【P 位是 1】+【数据在内存】。
三、实测能看出什么
同一块内存碰两遍:第一遍 48.9 毫秒、第二遍 3.2 毫秒(5.8 已测)。差的就是缺页。 读一个刚写完的 64 MB 文件,三遍耗时几乎一样——因为数据一直在页缓存里,连第一遍都没真读盘。 所以在这个沙箱里测不到【冷启动】,要看冷的那一侧得先清缓存。 这本身是个重要结论:很多【性能优化】其实是在【把冷变成热】,而不是把热变得更热。
四、清缓存才能看到真相
Linux 上清页缓存:echo 3 > /proc/sys/vm/drop_caches(要 root)。
清完再读同一个文件:第一遍会慢一个数量级,第二遍又变快。
这就是【冷热差】,也是所有【预热脚本】存在的理由。
注意不要在线上机器随便清:它会把所有程序的缓存全丢掉,瞬间把所有负载变成冷启动。
五、动手 Demo:请求分页——数据在不在内存里,差别就在这儿
这一节读同一个 64 MB 文件三遍、再换三种块大小读,看【页表】与【页缓存】各自留下了什么痕迹。
def scan():
t0 = time.perf_counter()
with open("seq.bin", "rb") as f:
while True:
b = f.read(1024 * 1024)
if not b:
break
return (time.perf_counter() - t0) * 1000
for i in range(3):
print("第", i + 1, "遍", scan(), "毫秒")
for blk in (4096, 65536, 1048576):
print("每次", blk, "字节", scan(), "毫秒")
实跑输出:
--------------------------------------------------------------------
请求分页——数据在不在内存里,差别就在这儿
--------------------------------------------------------------------
① 顺序读完 64 MB,连读三遍
第几遍 花了多少 读了多少
-----------------------------------
第 1 遍 63.7 毫秒 64 MB
第 2 遍 60.8 毫秒 64 MB
第 3 遍 62.4 毫秒 64 MB
**三遍几乎一样快**——这本身就是结论:
**文件刚写完,内容还在页缓存里,一遍都没有真去读磁盘。**
**所以在这个沙箱里测不到【第一遍慢】。**想看到冷热差距,得先把缓存清掉。
② 一次读多少,也影响速度
每次读多少 过几次系统 耗时
--------------------------------------
4 KB 16384 次 79 毫秒
64 KB 1024 次 30 毫秒
1024 KB 64 次 57 毫秒
**读的量一样,系统调用次数差 256 倍。**
**4 KB 那一次慢,是因为过门的开销吃掉了大头;**
**放大到 64 KB 以上,这笔开销就被淹没了——再加到 1 MB 也不会明显更快**,
**因为瓶颈已经换成了【拷内存】,不是【过门】。**
③ 一句话分清两件事
问题 答案在哪一层
--------------------------------------------------
数据在不在内存里 页缓存(第 6 章)
这一次访问要不要陷进内核 页表 P 位 + 系统调用
**缓存放错、页表填对,两个都到位才快。**
**少任何一个,你就得等磁盘。**
三遍几乎一样快(42.6、45.9、42.5 毫秒)——因为文件刚写完,内容还在页缓存里,一遍都没真读盘。 所以这一节在沙箱里只能证明【热的那一侧】,冷的那一侧必须清缓存才能看到。 再看块大小那一组:4 KB 一次 65.9 毫秒、64 KB 一次 34.8 毫秒。读的总量一样,差在系统调用次数(16384 次 对 1024 次)。 但 1 MB 一次反而没更快(57.6 毫秒)——因为瓶颈已经从【过门】换成了【拷内存】。 这三行合起来就是一句话:优化要对准当前的瓶颈,瓶颈换了,招数就失效了。
📋 Linux 上看到真正的冷热差:echo 3 > /proc/sys/vm/drop_caches 然后 time cat bigfile > /dev/null,再跑一遍对比。第一遍是要读盘的。
30 秒实验
echo 3 > /proc/sys/vm/drop_caches(要 root),再time cat 大文件 > /dev/null,与不清缓存时对比。cat /proc/meminfo | grep -E 'Cached|Dirty',看有多少页是干净的缓存的。grep -E 'pgpgin|pgpgout' /proc/vmstat,隔几秒读两次,看实际读盘量。
常见误区
- 【缺页就是内存不够】。缺页是第一次访问的常态;只有一直缺页才是问题。
- 【数据在页缓存里就不缺页了】。还要你的页表映射过它;没映射还是要缺一次页,只是不用读盘。
- 【读得越大越快】。过大之后瓶颈换成拷内存,再大也不会更快,还可能浪费带宽。
自测三问(点开对答案)
1. P 位是 1 和是 0 分别会发生什么? —— **1:直接访问物理内存,不打扰内核**;**0:触发缺页异常,陷进内核决定分配新页、换回还是读文件。** 2. 哪两个问题要分开看? —— **数据在不在物理内存(页缓存)**,以及**这一次访问要不要陷进内核(P 位)**。**两个都到位才快。** 3. 为什么块大小加到 1 MB 反而没更快? —— **系统调用次数已经不是瓶颈了,瓶颈换成了内存拷贝。****优化要对准当前的瓶颈。**- ☐ 5.14 写时复制
相关笔记
- 实验 — 缺页有多贵
- 文件映射 — 碰哪页读哪页
- 页面缓存 — 另一个问题的那一半
- 🐧 Linux是怎样工作的 · 索引