🐧 第五章 内存管理
📄 一句话概括

两个问题要分开看:数据在不在内存里(页缓存的事),这一次访问要不要陷进内核(页表 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 秒实验

  1. echo 3 > /proc/sys/vm/drop_caches(要 root),再 time cat 大文件 > /dev/null,与不清缓存时对比。
  2. cat /proc/meminfo | grep -E 'Cached|Dirty',看有多少页是干净的缓存的。
  3. grep -E 'pgpgin|pgpgout' /proc/vmstat,隔几秒读两次,看实际读盘量。

常见误区

  1. 【缺页就是内存不够】。缺页是第一次访问的常态;只有一直缺页才是问题。
  2. 【数据在页缓存里就不缺页了】。还要你的页表映射过它;没映射还是要缺一次页,只是不用读盘。
  3. 【读得越大越快】。过大之后瓶颈换成拷内存,再大也不会更快,还可能浪费带宽。
自测三问(点开对答案) 1. P 位是 1 和是 0 分别会发生什么? —— **1:直接访问物理内存,不打扰内核**;**0:触发缺页异常,陷进内核决定分配新页、换回还是读文件。** 2. 哪两个问题要分开看? —— **数据在不在物理内存(页缓存)**,以及**这一次访问要不要陷进内核(P 位)**。**两个都到位才快。** 3. 为什么块大小加到 1 MB 反而没更快? —— **系统调用次数已经不是瓶颈了,瓶颈换成了内存拷贝。****优化要对准当前的瓶颈。**

相关笔记