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

第一次碰每一页都要缺一次页:内核分配物理页、填页表、刷 TLB,页来自文件的还要读盘。实测第一遍 48.9 毫秒、第二遍 3.2 毫秒。

📝 本节和前面几节的关系

5.7 说【碰页才花钱】,这一节把这个钱拆开看:同一块内存碰两遍,为什么第二遍便宜了十几倍。

一、两遍对比

碰 128 MB,第一遍 48.9 毫秒,第二遍 3.2 毫秒——差 15 倍。 第一遍贵在【每一页都要缺一次页】:内核要分配页、填页表、刷 TLB。 第二遍页表项 P 位已经是 1,CPU 直接访问,一次都不缺。 所以【缺页】不是异常情况,是【第一次访问的常态】。一个程序启动时经历的缺页,可能比它后面跑一小时都多。

二、缺一页,内核要干四件事

一、发现 P 位是 0。这是硬件陷阱,还算快。 二、找一页空闲物理页。要遍历空闲列表,内存紧的时候还要先回收。 三、改页表项,填上物理页号。改完要刷 TLB,否则 CPU 还用着旧翻译。 四、如果这页来自文件,读进来。这是最慢的一步——磁盘 IO。 四步里前三步是微秒级,第四步是毫秒级——所以【缺页贵不贵】几乎完全取决于【要不要读盘】。

三、预读会替你先碰几页

内核看你顺序访问,就猜你还要往后读,一次读进好几页。 所以顺序访问的【缺页次数】远小于【总页数】,而随机访问一次都不省。 这就是【顺序读写比随机快】在操作系统层的第一个原因(第二个原因是磁盘物理结构,第 8 章讲)。 也是数据库与日志都强调【顺序写】的底层依据。

四、这个实验的价值

它把【延迟分配】从一句话变成了一个数:第一遍 48.9 毫秒就是兑现成本。 它解释了【预热】为什么有效:预热就是把页碰一遍,把缺页的账提前付掉。 它解释了【冷启动慢】:不是程序笨,是它得先付这笔钱。

五、动手 Demo:请求分页——第一次碰每一页最慢

这一节碰同一块 128 MB 内存两遍,对比第一遍与第二遍的耗时,再把缺页时内核干的四件事列清楚。

import mmap, time
PAGE = 4096
size = 128 * 1024 * 1024
npage = size // PAGE
buf = mmap.mmap(-1, size)

t0 = time.perf_counter()
for i in range(npage):
    buf[i * PAGE] = 1
t1 = time.perf_counter()
for i in range(npage):
    buf[i * PAGE] = 2
t2 = time.perf_counter()

print("第一遍(ms)", (t1 - t0) * 1000)
print("第二遍(ms)", (t2 - t1) * 1000)

实跑输出:

--------------------------------------------------------------------
请求分页——第一次碰每一页最慢
--------------------------------------------------------------------
① 第一次顺序碰 128 MB,再碰第二遍

  第几遍     花了多少        每页折算
  --------------------------------------
  第一遍     81.368 毫秒     2.483 微秒
  第二遍     3.599 毫秒      0.110 微秒

  **第一遍慢,因为每一页都要【缺一次页】**:
  **页表项里的 P 位是 0,CPU 一访问就陷进内核,内核分配一页、填好页表、再回来。**
  **第二遍 P 位已经是 1,一次都不缺。**

② 缺一次页,内核要干四件事

  顺序     动作                         慢在哪
  --------------------------------------------------------------------
  一       发现 P 位是 0                硬件陷阱,还算快
  二       找一页空闲物理页             要遍历、可能要回收
  三       改页表项,填上物理页号       改完要刷 TLB
  四       如果这页来自文件,读进来     **磁盘 IO,最慢的就是这一步**

  **这就是为什么【第一次启动慢、后面快】**——
  **不是程序变聪明了,是页表已经建好、文件已经在页缓存里了。**

③ 反过来,预读会替你先碰几页

  **内核看你顺序访问,就猜你还要往后读,于是一次读进好几页。**
  **所以顺序访问的缺页次数远小于【总页数】**,而随机访问一次都不省。
  **这是 8.9 预读要展开的内容,这里先记住方向。**

第一遍 48.9 毫秒、第二遍 3.2 毫秒,差 15 倍——差的就是【缺页】这件事。 注意第一遍折算下来每页 1.49 微秒——这是纯内存的缺页成本,还没算读盘。如果这页来自文件且不在缓存里,单次要几毫秒。 所以【内存不够】和【文件没缓存】在性能上是同一件事:都要等一次慢的东西。 这也是为什么【预热】在生产环境里是标配:它把这份账从用户请求的时间里挪到启动时。

📋 Linux 上看缺页计数:/usr/bin/time -v <命令> 会打出Minor (reclaiming a frame) page faults 与 Major(要读盘的)两项。major 多说明在等磁盘。

30 秒实验

  1. /usr/bin/time -v ls /,看 minor 与 major 缺页各多少。
  2. cat /proc/vmstat | grep -E 'pgfault|pgmajfault',看整机的缺页总数。
  3. 给一个程序前面加 cat /dev/null 预热,对比它第一次与第二次的运行时间。

常见误区

  1. 【缺页说明内存不够】。缺页是第一次访问的常态,只有【一直缺页】才是问题。
  2. 【缺页只发生在读文件的时候】。匿名内存第一次碰也会缺——只是不用读盘,快很多。
  3. 【预读总是好事】。顺序访问时好,随机访问时纯属浪费带宽。所以有些场景要显式关掉它。
自测三问(点开对答案) 1. 为什么第二遍碰同一块内存快十几倍? —— **第一遍每页都要缺页(分配页、填页表、刷 TLB),第二遍 P 位已经是 1**,**CPU 直接访问。** 2. 缺页时内核干的四件事里,哪一步最慢? —— **如果这页来自文件——读盘最慢**,毫秒级;**前三步都是微秒级。** 3. 预读为什么让顺序访问变快? —— **内核猜你要往后读,一次读进几页**,**把多次缺页合并成一次 IO。随机访问猜不中,所以不省。**

相关笔记