第一次碰每一页都要缺一次页:内核分配物理页、填页表、刷 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 秒实验
/usr/bin/time -v ls /,看 minor 与 major 缺页各多少。cat /proc/vmstat | grep -E 'pgfault|pgmajfault',看整机的缺页总数。- 给一个程序前面加
cat /dev/null预热,对比它第一次与第二次的运行时间。
常见误区
- 【缺页说明内存不够】。缺页是第一次访问的常态,只有【一直缺页】才是问题。
- 【缺页只发生在读文件的时候】。匿名内存第一次碰也会缺——只是不用读盘,快很多。
- 【预读总是好事】。顺序访问时好,随机访问时纯属浪费带宽。所以有些场景要显式关掉它。
自测三问(点开对答案)
1. 为什么第二遍碰同一块内存快十几倍? —— **第一遍每页都要缺页(分配页、填页表、刷 TLB),第二遍 P 位已经是 1**,**CPU 直接访问。** 2. 缺页时内核干的四件事里,哪一步最慢? —— **如果这页来自文件——读盘最慢**,毫秒级;**前三步都是微秒级。** 3. 预读为什么让顺序访问变快? —— **内核猜你要往后读,一次读进几页**,**把多次缺页合并成一次 IO。随机访问猜不中,所以不省。**- ☐ 5.9 利用上层进行内存分配
相关笔记
- 为进程分配内存 — 碰页才花钱
- 请求分页 — 这套机制的正式名字
- 页面缓存 — 页来自文件时钱花在哪
- 🐧 Linux是怎样工作的 · 索引