🐧 第七章 文件系统
📄 一句话概括

同样写 20 MB:磁盘不 fsync 10.88 毫秒、内存 8.88 毫秒、磁盘每次 fsync 26.03 毫秒。快的那两条干的是同一件事——一次内存拷贝。tmpfs 的特别之处是:它把【抄进缓存】当成了终点。

📝 本节和前面几节的关系

7.12 讲的是【放在盘上】的文件系统,这一节讲【不放在盘上】的那一种:数据只在内存里,掉电就没。

一、tmpfs 是什么

tmpfs 是一个【把内容放在内存与 Swap 里】的文件系统。它有完整的目录树、inode、权限——从接口上看和 ext4 没有任何区别。 区别只在【数据的去处】:ext4 的数据最终落在块设备上;tmpfs 的数据只在内存里,需要时被换出到 Swap。 所以它的两个性格:① 快——没有设备这一层,就是内存操作。② 危险——掉电就没,而且没有【已经落盘】这个状态。 mount -t tmpfs -o size=1G tmpfs /mnt/ram 就能造一个。不指定 size 的话,默认上限是物理内存的一半。 一句话:tmpfs 是【用文件接口包装起来的一块内存】。

二、快在哪、危险在哪

实测三个数:磁盘不 fsync 10.88 毫秒;内存 8.88 毫秒;磁盘每次 fsync 26.03 毫秒。 前两个在一个量级——因为它们干的是同一件事:一次内存拷贝。不 fsync 的磁盘写入,其实只是抄进了页缓存,根本没到盘上。 所以【写文件很快】这件事本身说明不了任何问题。它可能只是抄进了缓存。要确认真的落盘,只有 fsync 的返回能作数。 这就是 tmpfs 危险的地方:它把【抄进缓存】当成了终点,从来没有下一步。而普通文件至少还有一个【回写线程在后台慢慢写盘】。 反过来看,这也是 tmpfs 的好处:不需要回写,所以不会有【写放大】、不会有【突发的 IO 抖动】。

三、哪些目录本来就是 tmpfs

/tmp:临时文件。下次开机就该没了——所以放内存是合理的。 /run:进程号、锁文件、socket。重启后必须重新生成,所以【重启后 /run 里的东西消失】不是 bug,是设计。 /dev/shm:进程间共享内存。POSIX 共享内存就是靠这个目录实现的——两个进程 mmap 同一个文件,就共享了同一块内存。 这几个目录的共同点是:内容都是【运行期的状态】,本来就不该跨重启。把这种状态放进 tmpfs 是【设计对了】,不是【省事】。 而【把重要东西放在 /tmp 然后重启丢了】是用户自己的锅——/tmp 的语义就是【随时可以消失】。

四、tmpfs 与【文件被缓存】的区别

两者看着像(都占内存、都快),其实是两件事。 磁盘文件在缓存里是【副本】——盘上永远有一份真的。内存不够时,缓存可以被直接丢掉,因为盘上有。 tmpfs 里的东西【只有内存这一份】——内存不够时不能丢,只能换出到 Swap。所以它计入【内存已用】,而页缓存计入【可回收】。 这就是 free 里那几个数的关系(5.1 讲过):buff/cache 那部分是【可以随时丢的】;而 tmpfs 占的部分算在【已用】里。 所以【df -h /tmp 显示用了一半】和【free 显示内存紧张】可能是同一件事的两面。排查内存问题时,别忘了看一眼 /dev/shm 和 /tmp 有多大。

五、动手 Demo:基于内存的文件系统——把 /tmp 放进内存

这一节比较三件事:写 20 MB 到磁盘(不 fsync)、写 20 MB 到磁盘(每次 fsync)、写 20 MB 到预分配的 bytearray。

import os
import tempfile
import time

MB = 1024 * 1024
BUF = b"x" * MB
d = tempfile.mkdtemp(prefix="ch29c7_")
MEMBUF = bytearray(20 * MB)

def disk_write(sync):
    p = os.path.join(d, "w%d.bin" % (1 if sync else 0))
    with open(p, "wb") as g:
        for _ in range(20):
            g.write(BUF)
        g.flush()
        if sync:
            os.fsync(g.fileno())

def mem_write():
    for i in range(20):
        MEMBUF[i * MB:(i + 1) * MB] = BUF

for name, fn in (("磁盘 不fsync", lambda: disk_write(False)),
                 ("磁盘 fsync", lambda: disk_write(True)),
                 ("内存", mem_write)):
    t0 = time.perf_counter()
    fn()
    ms = (time.perf_counter() - t0) * 1000
    print(name, "%.2f 毫秒" % ms, "%.0f MB/秒" % (20 / (ms / 1000)))

实跑输出:

--------------------------------------------------------------------
基于内存的文件系统——把 /tmp 放进内存
--------------------------------------------------------------------
① 同样写 20 MB:磁盘 vs 内存

  写到哪                 20 MB 耗时     折算速度
  ---------------------------------------------------
  磁盘(不 fsync)       12.19 毫秒     1641 MB/秒
  磁盘(每次 fsync)     25.02 毫秒     800 MB/秒
  内存(bytearray)      13.48 毫秒     1484 MB/秒

  **三行里有两行在同一个量级,第三行明显慢一截。**
  **快的那两行——【写内存】与【不 fsync 的磁盘】——干的是同一件事:一次内存拷贝。**
  **慢的那一行(每次 fsync)才真的把数据交给了设备**,**多花 2.1 倍时间。**
  **所以【写文件很快】这件事本身说明不了任何问题。**
  **它可能只是抄进了页缓存,还没到盘上。**
  **要确认真的落盘,只有 fsync 的返回能作数。**
  **这也是 tmpfs 危险的地方:它把【抄进缓存】当成了终点,从来没有下一步。**

② tmpfs 与【普通文件放缓存里】的区别

  特性               磁盘上的文件           tmpfs 里的文件
  -------------------------------------------------------------
  掉电后还在吗       在(fsync 过就稳)     全没了
  占的是谁的空间     磁盘                   内存与 Swap
  会不会被回收       不会                   会:换出到 Swap
  容量上限           卷的大小               默认内存的一半
  fsync 有意义吗     有                     没意义,无事可做

  **tmpfs 与【磁盘文件被缓存】看着像,其实是两件事。**
  **磁盘文件在缓存里是【副本】,盘上永远有一份真的;**
  **tmpfs 里的东西【只有内存这一份】。**
  **所以 `df -h /tmp` 明明显示用了 50%,**
  **而 `free` 里那部分其实是【可以被换出的内存】。**

③ 哪些目录本来就是 tmpfs

  目录         里面放什么         为什么放内存
  -----------------------------------------------------
  /tmp         临时文件           下次开机就该没了
  /run         进程号、锁文件     重启后必须重新生成
  /dev/shm     进程间共享内存     本来就不该落盘

  **所以【重启后 /run 里的东西消失】不是 bug,是设计。**
  **而【/tmp 里放了重要东西然后重启丢了】是用户自己的锅。**

  📋 **Linux 上验证**:`df -h /tmp /run /dev/shm`,
  看 `Filesystem` 一列是不是写着 `tmpfs`;
  `mount | grep tmpfs` 看各自的大小上限。

三行里有两行在同一个量级,第三行明显慢一截。快的那两行——【写内存】与【不 fsync 的磁盘】——干的是同一件事:一次内存拷贝。慢的那一行(每次 fsync)才真的把数据交给了设备。 所以【写文件很快】说明不了任何问题,它可能只是抄进了页缓存,还没到盘上。 这也是 tmpfs 危险的地方:它把【抄进缓存】当成了终点,从来没有下一步。 注意这里的内存缓冲是【预分配】的——如果改成反复 +=,测出来的就是【拼接的代价】而不是【内存的带宽】(7.10 踩过这个坑)。

📋 Linux 上验证:df -h /tmp /run /dev/shm,Filesystem 一列写着 tmpfs;mount | grep tmpfs 看各自的大小上限。

30 秒实验

  1. df -h /tmp /dev/shm,看这两个各有多大。
  2. mount -t tmpfs -o size=100M tmpfs /mnt,然后 dd 写满它,看什么时候报错。
  3. 想一想:为什么把编译的临时目录放进 tmpfs 能加快编译?

常见误区

  1. 【tmpfs 就是页缓存】。页缓存是磁盘文件的副本,可以随时丢;tmpfs 只有内存这一份,丢了就没了。
  2. 【tmpfs 里的东西会被自动保存】。它不会。要保就得自己拷出来。
  3. 【/tmp 会定期清理所以不重要】。清理策略因发行版而异,有的重启才清;重要东西不该放 /tmp,这是设计前提。
自测三问(点开对答案) 1. 为什么【不 fsync 的磁盘写】和【写内存】差不多快? —— **因为不 fsync 时数据只是抄进了页缓存**,**还没到设备**——**它和写内存干的是同一件事。** 2. tmpfs 与页缓存的两个关键区别是什么? —— **① 页缓存是副本,盘上有真的;tmpfs 只有一份。****② 页缓存可以被直接丢弃,tmpfs 只能换出到 Swap。** 3. 为什么把编译临时目录放进 tmpfs 能加快编译? —— **因为编译器产生大量小的中间文件**,**省掉的是每次 `open`/`write` 的固定开销与回写**(7.12 那条台阶)。

相关笔记