写 20000 次 1 字节:不带缓冲 79.74 毫秒,带 1 MB 缓冲 2.74 毫秒——差 29 倍。写的内容一模一样,差的只是【过不过内核】。
6.9 讲了 fsync 那一道关,这一节往回退一层:在到达内核之前,程序自己还能攒一道。
一、三层缓冲
第一层:程序自己的缓冲(C 的 stdio、Python 的文件对象)。攒够了一定大小或手动 flush 才交给内核。 第二层:内核的页缓存。写进去就算【别人能看到了】,但还没落盘。 第三层:设备自己的写缓存。在硬盘电路板上,几 MB,到了就写,这一层断电就丢。 三层叠起来:你写 1 个字节,可能要过很久才真到盘上。所以要分清【谁能看到】与【断电丢不丢】是两件事。
二、实测:29 倍
写 20000 次 1 字节。 不带缓冲(每次直接过内核):79.74 毫秒,每次 3.99 微秒。 带 1 MB 缓冲:2.74 毫秒,每次 0.14 微秒。 差 29 倍。写的内容完全一样,差的只是【过不过内核】。 这 3.99 微秒就是【一次系统调用 + 一次拷贝】的价格,跟 6.9 里 fsync 的 3 毫秒是两个量级的问题——一个是过门,一个是等设备。
三、什么时候必须关缓冲
一、要立刻被别人读到:flush 就够(进页缓存,别人就能读),不需要 fsync。 二、要断电不丢:必须 fsync(催到介质)。 三、你自己已经攒了一大批:直接写,别再套一层缓冲——否则数据会被拷两次(一次进你的缓冲,一次进页缓存)。 第四条容易被忽略:大块写时关掉缓冲反而更快,因为省掉了一次内存拷贝。
四、日志为什么要【追加 + 缓冲 + fsync】
追加:天生顺序,不用跳着写(6.12 会讲随机写多贵)。 缓冲:把多次小写攒成一次大写,省掉过门。 fsync:在【我说提交成功】之前,逼它落盘。 三件事合起来,就是【日志(WAL)】的写法——它同时拿到了【顺序】、【批量】、【可靠】三个好处。 所以几乎所有数据库、消息队列都用这一套。
五、动手 Demo:缓冲区缓存——带缓冲和不带缓冲差多少
这一节写 20000 次 1 字节,一次不带缓冲(每次直接过内核),一次带 1 MB 缓冲,把【过门】的价格量出来。
n = 20000
with open(p, "wb", buffering=0) as f:
t0 = time.perf_counter()
for _ in range(n):
f.write(b"A")
print("不带缓冲", (time.perf_counter() - t0) * 1000)
with open(p, "wb", buffering=1024 * 1024) as f:
t0 = time.perf_counter()
for _ in range(n):
f.write(b"A")
print("带 1MB 缓冲", (time.perf_counter() - t0) * 1000)
实跑输出:
--------------------------------------------------------------------
缓冲区缓存——带缓冲和不带缓冲差多少
--------------------------------------------------------------------
① 写 20000 次 1 字节,两种写法
写法 总耗时 每次折算
--------------------------------------------------
不带缓冲(每次过门) 81.66 毫秒 4.08 微秒
带 1 MB 缓冲 2.76 毫秒 0.14 微秒
**差了 30 倍。**
**写的内容一模一样,差的只是【过不过内核】。**
② 缓冲区缓存是【谁】的缓冲
这一层 在哪 攒多久
-------------------------------------------------------
程序自己的缓冲 你的进程里 到大小或手动 flush
内核的页缓存 内核里 等回写线程来收
磁盘自己的缓存 硬盘电路板上 到几 MB 就写
**三层缓冲叠起来**:**你写 1 个字节,可能要过很久才真到盘上。**
**所以【写完就能被别的进程读到】靠的是第二层(页缓存),不是第一层。**
**而【断电不丢】必须靠 fsync 一直催到第三层。**
③ 什么时候该关掉缓冲
场景 该怎么做
---------------------------------------------
要立刻被别人读到 flush(进页缓存就行)
要断电不丢 fsync(催到介质)
自己已经攒了一大批 直接写,别再套一层
79.74 毫秒对 2.74 毫秒——差 29 倍。 每次折算:3.99 微秒对 0.14 微秒。3.99 微秒就是【一次系统调用 + 一次拷贝】的价格。 注意这里连 fsync 都没做——只是到内核为止。如果再加上 fsync,就是 3 毫秒那一档了(6.9)。 所以性能优化要分清层次:过门是微秒级,等设备是毫秒级。先解决毫秒级的,再管微秒级的。 这也解释了【C 语言里 printf 要不要加换行】这种老话题——行缓冲与全缓冲的差别,就是同一个道理。
📋 Linux 上看到底写没写下去:strace -e trace=write,fsync -c <命令>——看 write 被调了几次、每次多少字节。
30 秒实验
- 把缓冲改成 4 KB 与 1 MB,看耗时怎么变。
strace -c -e trace=write ls,看一个普通命令调了几次 write。- 把上例的 1 字节改成 4 KB、写 100 次,看带不带缓冲还差多少(差距应该大幅缩小)。
常见误区
- 【缓冲就是 fsync】。缓冲只是攒起来;fsync 是逼它落盘,两件事不同层。
- 【缓冲一定更快】。大块写的时候它反而多一次拷贝,所以有些场景要显式关掉。
- 【flush 之后断电就不丢了】。flush 只到页缓存,断电照样丢。
自测三问(点开对答案)
1. 三层缓冲分别在哪? —— **程序自己的缓冲 → 内核页缓存 → 设备写缓存。****越往下越慢,也越【丢不了】。** 2. 为什么带缓冲快 29 倍? —— **不带缓冲时每次写都是一次系统调用**(约 4 微秒);**带缓冲把 20000 次合并成几次。** 3. 什么时候应该关掉缓冲? —— **你自己已经攒了一大批、一次大块写的时候**——**关掉能省一次内存拷贝。**- ☐ 6.11 读取文件的实验
相关笔记
- 同步写入 — 下一道关:等设备
- 读取文件的实验 — 读的那一侧
- C标准库 — 同一个道理在标准库里的样子
- 🐧 Linux是怎样工作的 · 索引