这一章在做什么

回答一个问题:应用等文件系统等了多久,等在了哪里。文件系统拿内存做缓存(page cache),再加上预读和写回,把存储设备的慢挡在应用外面,所以磁盘上的数字常常和应用的感受对不上。这一章教你从应用这一层量延迟:看完整的分布而不是平均值,揪出慢的那几次,分清是缓存没命中、同步写在等盘,还是后台的写回和别的进程。工具从 free、strace 一直到 ext4dist、ext4slower。

一次 read() 走过的路,和可以量延迟的地方 应用(你的服务) 系统调用 read() / write() VFS 文件系统(ext4、XFS…) 存储(eMMC、SSD、机械盘) 未命中:等盘,0.1–10 ms page cache(内存) 命中:微秒级就返回 ① 应用自己计时 ② strace -T(开销大) ③ VFS:bpftrace ④ ext4dist / ext4slower iostat:物理 IO,第 9 章 ①–④ 量的是逻辑 IO,越往上越接近应用的感受;最下面的物理 IO 不一定有应用在等。
本站画的示意。page cache 在 VFS 和文件系统的读写路径上被查询,这里画在旁边是为了看清「命中就不往下走」。

什么时候翻这一章

page cache 的回收、换页和 dirty 系列参数在第 7 章(内存);盘本身的延迟、利用率、iostat 在第 9 章。一条常见的排查路线是:先在这一章确认应用确实在等文件系统,再往第 9 章走。

先用中文过一遍

page cache:命中和未命中差两三个数量级

Linux 把读过的文件内容留在内存里,叫页缓存(page cache)。下次再读同一块,内核在内存里就找到了,几微秒返回,这是命中(cache hit);找不到才去存储设备读,要等几百微秒到几毫秒,这是未命中(cache miss),读上来的数据顺手放进缓存(§8.2.2)。缓存会慢慢长到占满闲着的内存,这是故意的:内存闲着也是闲着,应用要用时内核再从缓存里回收(§8.3.2)。所以 free 里 free 一栏很小、buff/cache 很大,一般不用担心,看 available 就行。

两种结果的耗时差了两三个数量级,混在一起就是一个双峰分布(bimodal distribution):左边一个峰是命中,右边一个峰是未命中。这时平均值落在两峰之间的低谷,几乎没有哪一次请求真的花了那么久;中位数也一样,命中率高时它只反映命中那一峰(§8.6.18)。书里因此坚持看完整的分布(直方图、热图),或者把慢的操作逐条列出来(§8.5.2)。命中率 99% 时,平均值几乎全由命中决定,那 1% 的慢请求被淹没在里面,而它们恰恰是用户在抱怨的。

随机读 4000 次,文件一半在缓存里(本站在 WSL2 上实测) 0 2 4 8 16 32 64 128 256 512 每次读的耗时,微秒(对数刻度,每格翻一倍) 命中:内存里拿到 未命中:等盘 平均 74 µs 32–64 µs:0 次 p50 是 5 µs,p99 是 245 µs;平均值哪一峰也不代表。数据见下面「动手」的第二个实验。
本站在 WSL2 上用自己写的小程序跑出来的。WSL2 的虚拟盘背后还有 Windows 的缓存,未命中那一峰比真机的闪存、机械盘快得多;换成 eMMC 或机械盘,右峰会往右挪好几格,平均值被拉得更离谱。

写回和同步写:write() 返回不等于落盘

写的一侧默认是写回缓存(write-back caching):write() 把数据拷进内核的页缓存、把这些页标成脏页(dirty page),就返回了,应用只花了拷内存的时间。真正写到盘上是之后的事,由内核的 flusher 线程在后台做:脏页放了一段时间(默认约 30 秒),脏页总量超过阈值,或者有人调了 sync、fsync,都会触发写回(§8.4.3)。代价是可靠性:掉电时,还没写下去的脏页就丢了(§8.3.6)。

不能丢数据的程序用同步写(synchronous write):文件用 O_SYNC / O_DSYNC 打开,每次 write() 都等数据和必要的元数据落盘才返回;或者照常写,到一个检查点再调一次 fsync(),把之前的写一起提交(§8.3.7)。后者通常快得多,因为一批写只等一次盘。嵌入式板子上这件事特别要紧:慢的闪存上每等一次都可能是毫秒级,一个每条日志都 fsync 的线程,吞吐就被钉死在每秒几百条甚至更少。

同样一次 write(),应用要等多久 普通写 拷 返回,应用继续干活 flusher 线程写回 触发:脏页过期(约 30 秒)、脏页太多、sync / fsync 同步写 拷 阻塞:等数据和元数据落盘 才返回 盘上:写数据 + 日志 普通写:快,掉电丢最近的脏页。同步写:返回了就在盘上,但每次都付一整次存储延迟。
本站画的示意,长度不按比例。本站在 WSL2 上实测:4 KB 普通写每次约 10 微秒,加 oflag=dsync 后每次约 1.6 毫秒,差了一百多倍(见下面「动手」)。

预读和直接 IO:两种绕开等待的办法

顺序读一个大文件,按说每一块第一次读都该未命中。实际上很快,因为文件系统发现偏移量一个接一个,就提前把后面的几块读进缓存,这叫预取(prefetch),Linux 上一般叫预读(read-ahead)(§8.3.4)。猜对了,应用的读全是命中;猜错了,白读的数据占着缓存、占着盘的带宽。随机读没法预读,这也是随机读比顺序读慢得多的原因之一(§8.3.3)。

另一头是直接 IO(direct I/O):用 O_DIRECT 打开文件,读写都绕过页缓存(§8.3.8)。数据库这类自己管缓存的程序用它,免得同一份数据在两处各缓存一遍。它还常被拿来做测试,量的是盘而不是缓存。注意它不是同步写:O_DIRECT 不承诺 O_SYNC 那样的落盘保证,而且对缓冲区地址、IO 大小有对齐要求,不对齐可能直接返回 EINVAL。

逻辑 IO 和物理 IO:应用看到的不是盘看到的

应用发给文件系统的读写叫逻辑 IO(logical I/O),文件系统发给盘的叫物理 IO(physical I/O)。两者常常对不上(§8.3.12):有的缩水了(命中缓存就没有物理 IO,多次改同一块只写回一次),有的膨胀了(改几个字节要读写整块,再加一份日志),有的是间接的(预读、后台写回),有的跟你的应用根本无关(别的进程、备份、RAID 重建)。这就是书里不推荐只盯着磁盘排查的原因(§8.5.1):盘上 50 毫秒的写,可能没有任何请求在等;应用卡住的那次读,在盘上可能根本不存在(它在等一把锁,或者在等同一个文件上的 fsync)。

应用发出的(逻辑 IO) → 盘上看到的(物理 IO) 读 4 KB,页在缓存里 没有物理 IO(缩水) 顺序读 4 KB × 32 次 预读:几次大读,提前发出(间接) 日志文件追加 100 字节 稍后写回整页 4 KB,外加日志写(膨胀) 同一页 30 秒内改 10 次 只写回一次(写合并) (应用什么也没做) 备份、RAID 重建、别的进程(无关) 本站画的示意。数字是举例,实际大小取决于文件系统的块大小、预读窗口和日志模式。
本站画的示意。书里 §8.3.12 按「无关、间接、隐式、缩水、膨胀」五类列举,并用一个小写入一步步膨胀的例子说明;这里换成了写日志的场景。
英文中文一句话
page cache页缓存内核拿内存缓存文件内容,大小随空闲内存伸缩;free 里算在 buff/cache §8.4.3 术语页
cache hit ratio缓存命中率读请求里在缓存中找到的比例;理想情况接近 100% §8.6.12 术语页
prefetch / read-ahead预取 / 预读发现顺序读,就提前把后面的数据读进缓存 §8.3.4
write-back caching写回缓存写进内存就算完成,稍后异步写到盘上;快,但掉电会丢 §8.3.6 术语页
dirty page / flushing脏页 / 刷写改过还没写下去的页;flusher 线程负责把它们写回 §8.4.3
synchronous write同步写O_SYNC 打开或调 fsync,等数据落盘才返回 §8.3.7 术语页
direct I/O直接 IOO_DIRECT,绕过页缓存;不等于同步写 §8.3.8 术语页
logical / physical I/O逻辑 IO / 物理 IO应用发给文件系统的 / 文件系统发给盘的,常常对不上 §8.3.12 术语页
VFS虚拟文件系统内核里各种文件系统共用的一层接口,也是量延迟的一个统一位置 §8.4.2 术语页
dentry cache / inode cache目录项缓存 / inode 缓存缓存路径查找结果和文件属性,在 slabtop 里看得到 §8.4.3
journaling日志先把改动记进日志再落到原处,崩溃后能快速恢复;同步写入日志 §8.4.4
working set size (WSS)工作集大小实际被访问的数据量;装得进内存,测的就是缓存 §8.5.8 术语页
bimodal distribution双峰分布命中一个峰、未命中一个峰,平均值落在中间 §8.6.18 术语页
transaction cost事务里的文件系统耗时一个请求里阻塞在文件系统上的时间占比,决定优化它最多能快多少 §8.5.2

方法:按什么顺序查

书里建议先用这几种方法,顺序是:延迟分析、性能监控、负载特征、微基准测试、静态性能调优(§8.5)。核心是第一个:从应用这一侧量文件系统操作的延迟,而不是先看盘。落到一次排查上,本站整理成下面六步。前两步决定要不要继续查文件系统;要 root 的 BPF 工具从第二步开始出场,没有它们时每一步都给了退路。

  1. 文件系统占了请求时间的多少?

    在应用里给读、写、fsync 计时,或者对一个线程短时间跑 strace -c / strace -T 粗看。占比 = 一个请求里阻塞在文件系统上的时间 ÷ 请求总时间。占九成,消掉它最多能快近十倍;只占百分之一,就去查别处。§8.5.2

  2. 延迟的分布长什么样?

    ext4dist-bpfcc 10 1(要 root)按操作类型给出直方图。只有左边一个微秒级的峰,说明读几乎全命中,文件系统不是问题;右边有一个毫秒级的峰,再看它占多少。没有 BPF 时,在应用里自己按 2 的幂分桶计数,就是同一张图(见下面第二个实验)。§8.6.13§8.6.18

  3. 慢的那几次是什么操作、哪个文件、谁发的?

    ext4slower-bpfcc 1 列出超过 1 毫秒的操作。类型是 S(fsync)的多,是同步写在等盘;R 的多,是读未命中。只有 strace 时用 strace -T -e trace=read,write,fsync -p PID,但要记住它会把每次调用拖慢一大截。§8.6.14§8.6.7

  4. 施加的负载是什么样的?

    谁在读写哪些文件:opensnoop-bpfcc、filetop-bpfcc -C;读写比例、同步写有多少、顺序还是随机。/proc/PID/io 里 rchar 是读了多少(逻辑),read_bytes 是真正从存储层取了多少(物理),两者一比就知道命中了多少。常常能找到根本不该做的读写(反复打开同一个配置文件、调试日志没关)。§8.5.3§8.6.10§8.6.11

  5. 缓存够不够、写回压不压得住?

    free -m 看 buff/cache 和 available;cachestat-bpfcc 1 看命中率;grep -E 'Dirty|Writeback' /proc/meminfo 看脏页有多少。命中率低,先比一比工作集和可用内存:应用自己的内存占得太多,留给缓存的就少。§8.6.2§8.6.12§8.5.8

  6. 配置有没有问题?

    mount 看文件系统类型和挂载选项(relatime / noatime、有没有 sync);df -h 看是不是快满了;程序是 O_SYNC 逐条写还是攒一批 fsync。这些都排除了,延迟确实来自盘,就到第 9 章去看盘本身。§8.5.5§8.6.1§8.3.16

工具速查

想知道敲什么看哪里节
缓存占了多少内存free -m
free -mw
buff/cache 大是正常的;available 才是应用还能拿到多少;-w 把 buffers 和 cache 分开§8.6.2
脏页和写回grep -E 'Dirty|Writeback' /proc/meminfo
vmstat 1
Dirty 长期很大、写回时 bo 一阵猛冲;vmstat 的 buff、cache 两列是缓存大小§8.6.4
挂载选项mount文件系统类型;relatime 还是 noatime;有没有 sync 这种让所有写都变同步的选项§8.6.1
单次系统调用多慢strace -T -e trace=read,write,fsync -p PID行尾尖括号里的秒数;它本身会拖慢被跟踪的进程,只短时间用§8.6.7
读了多少、走没走盘cat /proc/PID/iorchar 是逻辑读的字节,read_bytes 是从存储层取的;后者远小于前者说明大多命中§8.3.12
打开了哪些文件opensnoop-bpfcc -T
opensnoop-bpfcc -x
同一个文件被反复打开;-x 只看打开失败的,找缺失的配置和库§8.6.10
哪个文件读写最多filetop-bpfcc -C按读字节排序的文件列表;-C 不清屏,输出留在终端里§8.6.11
文件访问事件(不用 BPF)fatrace每行一个事件:O 打开、R 读、W 写、C 关闭;忙的系统上输出极多,开销不小§8.6.8
page cache 命中率cachestat-bpfcc 1HITRATIO 接近 100% 才好;DIRTIES 是写;它靠 kprobe,换内核版本可能失效§8.6.12
ext4 延迟分布ext4dist-bpfcc 10 1每种操作一张直方图;读出现两个峰,左边命中、右边未命中;XFS 用 xfsdist§8.6.13
慢操作逐条列出ext4slower-bpfcc 1默认阈值 10 毫秒,参数改阈值;T 列 R 读、W 写、O 打开、S 同步§8.6.14
目录项和 inode 缓存slabtop -o
sar -v 1
dentry、ext4_inode_cache 这几个 slab 的大小;sar -v 的 dentunusd§8.6.6

free、vmstat、slabtop 在 procps 包里,sar 在 sysstat,strace 和 fatrace 各是一个同名包。带 -bpfcc 后缀的是 Debian bpfcc-tools 包里的 BCC 工具,和 fatrace、slabtop 一样都要 root。工具名链到本站的工具卡:free、strace、fatrace、opensnoop、filetop、cachestat、ext4dist、ext4slower、slabtop。

动手:在自己的机器上试

page cache:读走缓存,写先进内存

用一个 128 MB 的文件看三件事:缓存怎么长出来、冷读和热读差多少、同步写比普通写慢多少。全程不需要 root:丢掉某个文件的缓存用的是 dd 的 iflag=nocache(底下是 posix_fadvise 的 DONTNEED),它丢不掉还没写下去的脏页,所以前面要先 sync 这个文件。

# 在自己的目录里做,文件总量控制在两三百 MB 以内
$ mkdir -p ~/fslab && cd ~/fslab
$ free -m
$ dd if=/dev/urandom of=f1 bs=1M count=128 status=none
$ free -m
# 先把 f1 落盘,再请内核把它的页从缓存里丢掉
$ sync f1
$ dd if=f1 iflag=nocache count=0 status=none
$ free -m
# 冷读、热读、绕过缓存各一次
$ dd if=f1 of=/dev/null bs=1M
$ dd if=f1 of=/dev/null bs=1M
$ dd if=f1 of=/dev/null bs=1M iflag=direct

本站在 WSL2 上看到的(free 只留了 buff/cache 一列,dd 只留最后一行):

buff/cache(MB)  写之前 1867   写完 f1 1998   丢掉 f1 的缓存后 1867

冷读  134217728 bytes copied, 0.142748 s, 940 MB/s
热读  134217728 bytes copied, 0.0316084 s, 4.2 GB/s
直接  134217728 bytes copied, 0.0531463 s, 2.5 GB/s
  1. 写完 f1,buff/cache 正好多了约 130 MB:刚写的数据留在了缓存里,free 一栏跟着少了。丢掉它的缓存,又回到原来的数。
  2. 热读比冷读快四倍多。这个差距在 WSL2 上偏小:虚拟盘背后还有 Windows 的文件缓存,冷读其实也是从宿主的内存里拿的;第二次重跑冷读,本站看到的是 1.6 GB/s。在真的 eMMC 或机械盘上,冷读会慢一两个数量级。
  3. 绕过缓存的直接读(iflag=direct)比冷读还快,也是同一个原因,外加它用 1 MB 的大块直接下发。这一组数在 WSL2 上只看相对关系,不代表真机。

再看写。下面三条命令都写 2000 个 4 KB 的块:

$ dd if=/dev/zero of=f2 bs=4k count=2000
# 每次写都等落盘(相当于 O_DSYNC)
$ dd if=/dev/zero of=f2 bs=4k count=2000 oflag=dsync
# 照常写,最后 fsync 一次
$ dd if=/dev/zero of=f2 bs=4k count=2000 conv=fsync
# 看脏页什么时候被写回:写 100 MB,每 5 秒看一次 Dirty
$ dd if=/dev/zero of=f3 bs=1M count=100 status=none
$ for i in $(seq 0 8); do grep '^Dirty:' /proc/meminfo; sleep 5; done
$ rm -f f1 f2 f3
普通写    8192000 bytes copied, 0.0206156 s, 397 MB/s
dsync     8192000 bytes copied, 3.19233 s, 2.6 MB/s
末尾 fsync 8192000 bytes copied, 0.0162681 s, 504 MB/s

Dirty(每 5 秒)  104700  103804  107076  107292  98696
                  99104  105156  9112  11608   (kB)
  1. 每次都等落盘,2000 次写花了 3.2 秒,平均每次 1.6 毫秒;普通写平均每次 10 微秒。同样的数据,差了一百五十倍。
  2. 最后统一 fsync 一次,和普通写差不多快:8 MB 一起交给盘,只等一次。这就是「攒一批再提交」的道理。
  3. 100 MB 脏页在内存里停了 30 多秒,然后一下子被写回。这和 vm.dirty_expire_centisecs 的默认值 3000(30 秒)对得上;本机读到 dirty_writeback_centisecs 是 1500(内核的默认值是 500,即 5 秒,本机为什么不同本站没有查),flusher 每 15 秒才醒来检查一次,过期的脏页要等到它下一次醒来,大概因此看到的是 35 秒左右;你的机器上可能不同。
WSL2
本站实测过上面每一条命令(Debian 13,coreutils 9.7),不需要 root。WSL2 的虚拟盘有宿主缓存垫着,冷读和同步写的绝对值都偏乐观,只看相对关系。
服务器
别在生产机的业务盘上跑写测试。要冷缓存可以用 root 写 /proc/sys/vm/drop_caches(会清掉整台机器的缓存,影响线上服务),只丢一个文件就用上面的 iflag=nocache。
ARM 板
busybox 有 free 和 dd,但 iflag=nocache、oflag=dsync 这些选项是 GNU dd 的,busybox 支持哪些看你的版本和裁剪;板子上有 root 时直接 echo 3 > /proc/sys/vm/drop_caches。/proc/meminfo 的 Dirty、Writeback 一般都在。在 eMMC、SD 卡上跑 dsync 那一条,每次写的耗时会明显比 WSL2 长,正好量一量你的板子同步写一次要多久;写测试会磨损闪存,量少一点。

从应用这一层量延迟:亲眼看见双峰

下面这个小程序随机读一个文件 4 KB 一块,给每次 pread 计时,最后按 2 的幂分桶打印。它就是方法第一步说的「在应用里计时」,也是没有 BPF 时 ext4dist 的替身。先猜:文件只有一半在缓存里时,平均值会是多少?

// lat.c:随机读一个文件 N 次(每次 4 KB),给每次 pread 计时,打印对数直方图
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>

static long long now_ns(void) {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec * 1000000000LL + ts.tv_nsec;
}

static int cmp(const void *a, const void *b) {
    long long x = *(const long long *)a, y = *(const long long *)b;
    return (x > y) - (x < y);
}

int main(int argc, char **argv) {
    if (argc < 3) { fprintf(stderr, "usage: %s FILE N\n", argv[0]); return 1; }
    int fd = open(argv[1], O_RDONLY);
    if (fd < 0) { perror("open"); return 1; }
    int n = atoi(argv[2]);
    struct stat st; fstat(fd, &st);
    long blocks = st.st_size / 4096;
    posix_fadvise(fd, 0, 0, POSIX_FADV_RANDOM);   // 关掉预读,每次未命中只读这一块
    static char buf[4096];
    long long *t = malloc(sizeof(long long) * n), sum = 0;
    int hist[32] = {0};
    srand(42);                                     // 固定种子:每次跑读的是同一串偏移
    for (int i = 0; i < n; i++) {
        off_t off = (off_t)(rand() % blocks) * 4096;
        long long t0 = now_ns();
        if (pread(fd, buf, sizeof buf, off) != sizeof buf) { perror("pread"); return 1; }
        t[i] = (now_ns() - t0) / 1000;             // 微秒
        sum += t[i];
        int b = 0; while (b < 31 && (2LL << b) <= t[i]) b++;   // 落在 [2^b, 2^(b+1)) 这一桶
        hist[b]++;
    }
    for (int b = 0; b < 32; b++)
        if (hist[b]) printf("%6lld -> %-6lld us : %d\n", b ? 1LL << b : 0, (2LL << b) - 1, hist[b]);
    qsort(t, n, sizeof *t, cmp);
    printf("avg %lld us, p50 %lld us, p99 %lld us\n", sum / n, t[n / 2], t[n * 99 / 100]);
    return 0;
}
$ gcc -O2 -Wall lat.c -o lat
$ dd if=/dev/urandom of=f1 bs=1M count=128 status=none
# 全冷:丢掉 f1 的缓存再读
$ sync f1; dd if=f1 iflag=nocache count=0 status=none
$ ./lat f1 4000
# 全热:同一串偏移再读一遍,刚才读过的块都在缓存里
$ ./lat f1 4000
# 一半热:丢掉缓存,只把前 64 MB 读进来,再随机读整个文件
$ dd if=f1 iflag=nocache count=0 status=none
$ dd if=f1 of=/dev/null bs=1M count=64 status=none
$ ./lat f1 4000

本站在 WSL2 上的结果。全冷是 avg 164 us, p50 160 us,全热是 avg 1 us, p50 1 us;一半热的完整输出:

     0 -> 1      us : 721
     2 -> 3      us : 1104
     4 -> 7      us : 216
     8 -> 15     us : 120
    16 -> 31     us : 17
    64 -> 127    us : 162
   128 -> 255    us : 1630
   256 -> 511    us : 25
   512 -> 1023   us : 3
  1024 -> 2047   us : 2
avg 74 us, p50 5 us, p99 245 us
  1. 两个峰:几微秒的是命中,一两百微秒的是未命中,32 到 63 微秒这一桶一次也没有。平均值 74 微秒正好落在空档附近,没有哪一次读真的花了这么久;p50 是 5 微秒,只说了命中那一峰。上面「先用中文过一遍」的直方图就是这组数。
  2. 全冷那次也有两百多次读很快:随机偏移会重复,第二次读到同一块就命中了。命中率不只看文件有多少在缓存里,还看访问模式。
  3. 想先确认缓存里到底有多少页,可以用 Linux 6.5 加的 cachestat 系统调用问内核,本站在 WSL2 的 6.6 内核上用 syscall(451, …) 调通了:只读前 64 MB 后报告 f1 有 16640 页(约 65 MB)在缓存里,多出来的一点是 dd 顺序读时的预读。f1 是自己建的文件;对别人的只读文件,打过 2025 年安全补丁的内核不给结果。

最后看 strace 自己的代价。把文件全部读进缓存,分别直接跑和在 strace 下跑:

$ dd if=f1 of=/dev/null bs=1M status=none
$ ./lat f1 4000 | tail -1
$ strace -o /dev/null -e trace=pread64 ./lat f1 4000 | tail -1
$ rm -f f1
直接跑      avg 0 us, p50 1 us, p99 1 us
strace 下   avg 141 us, p50 133 us, p99 219 us
  1. 同样是全命中的读,在 strace 下每次从 1 微秒变成 130 多微秒,比 WSL2 上的未命中还慢。strace 用 ptrace 在每次系统调用进出时把进程停下来,程序自己量到的时间也被拉长了(§8.6.7)。
  2. 所以 strace -T 看到的绝对值不可信,尤其是本来很快的调用;它适合短时间看「调了什么、哪几次特别慢」。量延迟要用开销低的办法:应用自己计时,或者 BPF 工具。
WSL2
本站实测过:gcc 14、内核 6.6;strace 用的是 Debian 的 strace 6.13(装 strace 包)。跟踪自己启动的程序不需要 root。未命中那一峰的位置是 WSL2 虚拟盘的,不代表真机。
服务器
有 root 就直接 ext4dist-bpfcc 10 1、ext4slower-bpfcc 1 看真实服务(本站没有 root,没有实测)。别对线上的高频进程长时间挂 strace。
ARM 板
lat.c 只用 POSIX 接口,交叉编译就能跑,最适合 BPF 用不了的板子;读的文件放在 eMMC 或 SD 卡上,未命中那一峰会比 WSL2 靠右得多。strace 很多 BSP 里有(Buildroot、Yocto 都能选上),开销问题一样。cachestat 系统调用要内核 6.5 以上,看你的 BSP 内核版本。

动手:模拟器

先猜:命中率 90%,命中 2 微秒,未命中 1 毫秒(慢一点的闪存),平均延迟是多少?有多少次读的耗时接近这个平均值?再把命中率调到 99%,平均值降了多少,那 1% 的慢请求还在不在?

模拟器

缓存命中率与延迟分布

每次「读」先按命中率决定命中还是未命中,再按各自的延迟抽一个耗时(围绕设定值上下浮动)。下面是对数刻度的直方图(柱高按次数的平方根画,矮的峰也看得见):浅色是命中,橙色是未命中;实线是样本的平均值,虚线是按公式算的平均值。延迟的预设是示意值,不是哪块盘的实测。

命中未命中柱高:次数的平方根实线:样本平均虚线:公式平均 = 命中率 × 命中延迟 + (1 − 命中率) × 未命中延迟
还没有样本。点「读 1000 次」开始。
0
读的次数
—
实际命中率
—
平均(公式 / 样本)
—
p50 / p99
—
落在平均值 ±30% 内的读
  1. 默认设置读 1000 次:平均值落在哪?「落在平均值 ±30% 内的读」是多少?再把未命中换成机械盘,平均值翻了几倍,两个峰本身变了吗?
  2. 命中率从 90% 调到 99%、再到 99.9%:p50 几乎不动,平均值一路下降。到 99.9% 时读 1000 次,p99 落在哪一峰?这就是为什么高命中率时平均值和中位数都看不见那几次慢读,要看 p99、p99.9 或者逐条列慢操作。
  3. 把命中延迟从 2 微秒改成 10 微秒(比如每次读还要多走一层加密或校验):命中率 99% 时,它对平均值的影响和「未命中从 1 毫秒变成 8 毫秒」比,哪个大?

成书之后

查询某个文件的缓存状态,有了正式的系统调用

书里
cachestat 工具靠 kprobe 跟踪内核函数,换内核版本就可能失效;作者希望将来有跟踪点或 /proc 统计,让它变稳定(§8.6.12)。
今天
Linux 6.5(2023 年)加入了 cachestat() 系统调用:给一个文件和一段范围,返回其中在缓存里的页数、脏页数、正在写回的页数、被逐出的页数和最近被逐出的页数,不需要 root。2025 年初修了一个安全问题(CVE-2025-21691)之后,打过补丁的内核要求调用者是文件的属主或有写权限才给出结果,和 mincore 的规矩一样。它回答的是「这个文件有多少在缓存里」,不是全系统的命中率,所以不能完全替代 cachestat 工具。本站在 WSL2 的 6.6 内核上用 syscall(451, …) 调用过。
核对于 2026-10。出处:Phoronix《Linux 6.5 Brings New cachestat Syscall For Querying Page Cache Stats Of A File》;man7.org 上的 cachestat(2) 手册页;NVD 的 CVE-2025-21691 条目。

BPF 里能拿到文件路径了:bpf_d_path

书里
跟踪 read(2) 只拿得到文件描述符,分不清是文件、套接字还是 /proc;作者提到一个即将到来的 BPF 辅助函数,名字写作 get_fd_path(),能把描述符换成路径(§8.6.15)。
今天
最后进入内核的名字是 bpf_d_path(),随 Linux 5.10 合入,作用是由内核里的路径结构得到完整路径。你的内核和 BPF 工具都够新时,按路径区分文件系统读写方便多了;内核是 5.10 以前的(不少板子的 BSP 内核还很老),还得用书里说的其他办法。
核对于 2026-10。出处:Linux 5.10 合并窗口的网络子系统合并说明(networking changes for the 5.10 merge window),其中列出了 bpf_d_path() 辅助函数。

分节地图

这一章还是那个套路:背景(§8.1–8.3)、架构(§8.4)、方法(§8.5)、工具(§8.6)、实验和调优(§8.7–8.8)。上手要读的是概念里讲缓存、预读、写回、同步写、逻辑与物理 IO 的几节,架构里的 Linux 缓存,方法里的延迟分析和微基准测试,工具里的 free 和三个 BPF 工具,最后是讲双峰的可视化。各种文件系统的细节、ZFS 的调优用到再查或者跳过。

  1. File system performance often matters more…约 300 词开篇为什么文件系统比盘更贴近应用;学习目标和六个部分。
  2. Terminology约 300 词术语本章的 IO 只算直接读写的操作,open、close 不算。
  3. Models约 100 词模型一句引子。
  4. File System Interfaces约 100 词文件系统的接口把文件系统当黑盒,只量各种操作的延迟。
  5. File System Cache约 100 词文件系统缓存命中、未命中、预热;写也会先缓冲。
  6. Second-Level Cache约 100 词二级缓存闪存做的第二级缓存,主要是 ZFS 的做法,一般用不到。
  7. Concepts约 100 词概念一句引子。
  8. File System Latency约 300 词文件系统延迟首要指标;哪些情况下应用并不直接等它。
  9. Caching约 200 词缓存缓存吃掉空闲内存为什么是正常的;几种缓存。
  10. Random vs. Sequential I/O约 200 词随机与顺序 IO按偏移量区分;碎片让顺序读变成随机读。
  11. Prefetch约 300 词预取顺序读怎么被提前读进缓存;猜错的代价。
  12. Read-Ahead约 100 词预读Linux 的叫法和 readahead 系统调用。
  13. Write-Back Caching约 200 词写回缓存写进内存就返回,稍后再刷;掉电的风险。
  14. Synchronous Writes约 200 词同步写逐次同步写和用 fsync 成批提交的区别。
  15. Raw and Direct I/O约 200 词裸 IO 与直接 IO绕过文件系统和只绕过缓存;对齐和 EINVAL。
  16. Non-Blocking I/O约 200 词非阻塞 IOO_NONBLOCK、aio 和 io_uring;普通文件上未必生效。
  17. Memory-Mapped Files约 200 词内存映射文件mmap 省的是系统调用,省不掉等盘。
  18. Metadata约 200 词元数据逻辑元数据和物理元数据;stat 密集的负载。
  19. Logical vs. Physical I/O约 700 词逻辑 IO 与物理 IO应用的 IO 和盘的 IO 为什么对不上,五类原因。
  20. Operations Are Not Equal约 200 词操作各不相同只看每秒操作数说明不了什么。
  21. Special File Systems约 100 词特殊文件系统tmpfs、/proc、/sys 这些不落盘的文件系统。
  22. Access Timestamps约 100 词访问时间戳读文件也会引起写;怎么关掉见调优。
  23. Capacity约 100 词容量文件系统快满时为什么变慢。
  24. Architecture约 100 词架构这一部分的引子。
  25. File System I/O Stack约 100 词文件系统 IO 栈从系统调用到设备的几条路径。
  26. VFS约 100 词VFS各文件系统共用的接口,也是统一的观测点。
  27. File System Caches约 900 词文件系统的几种缓存buffer cache、page cache、dentry、inode 缓存;脏页何时写回。
  28. File System Features约 500 词文件系统特性块与 extent、日志、写时复制、擦洗。
  29. File System Types约 2000 词文件系统类型FFS 的历史,ext3、ext4、XFS、ZFS、btrfs 的性能特性。选文件系统时查。
  30. Volumes and Pools约 400 词卷与存储池LVM 和存储池;RAID 重建会拖慢很久。
  31. Methodology约 100 词方法方法总表和作者建议的起手顺序。
  32. Disk Analysis约 100 词只看磁盘只盯着盘的老办法为什么不够了。
  33. Latency Analysis约 600 词延迟分析在哪一层量、怎么呈现;请求里文件系统耗时的占比。
  34. Workload Characterization约 600 词负载特征描述文件系统负载的几项属性和一张问题清单。
  35. Performance Monitoring约 200 词性能监控长期盯操作速率和延迟两个指标。
  36. Static Performance Tuning约 200 词静态性能调优一张检查配置的问题清单。
  37. Cache Tuning约 100 词缓存调优几种缓存都可以查看、往往可以调。
  38. Workload Separation约 100 词负载分离日志和数据放在不同的盘上。
  39. Micro-Benchmarking约 400 词微基准测试工作集大小决定测的是缓存还是盘。
  40. Observability Tools约 100 词观测工具工具总表:传统工具和 BPF 工具。
  41. mount约 100 词mount挂载选项;relatime 怎么减少时间戳写入。
  42. free约 100 词freebuff/cache 和 available 两列怎么读。
  43. top约 100 词top头部也有缓存大小。
  44. vmstat约 100 词vmstatbuff 和 cache 两列。
  45. sar约 100 词sar-v 看目录项和 inode,-r 看缓存大小。
  46. slabtop约 100 词slabtop哪些 slab 是文件系统的缓存。
  47. strace约 200 词strace在系统调用层计时;开销大,数字会被拉长。
  48. fatrace约 100 词fatrace基于 fanotify 的文件访问事件;忙时输出太多。
  49. LatencyTOP约 100 词LatencyTOP作者说它久未更新、要少见的内核选项,用 BPF 工具代替。
  50. opensnoop约 200 词opensnoop跟踪文件打开,找配置、日志和缺失的文件。
  51. filetop约 200 词filetop按文件排的 top,找意外忙碌的文件。
  52. cachestat约 200 词cachestatpage cache 命中率;命中率低时调什么。
  53. ext4dist (xfs, zfs, btrfs, nfs)约 200 词ext4dist 等延迟分布工具按操作类型画延迟直方图,读出双峰。
  54. ext4slower (xfs, zfs, btrfs, nfs)约 200 词ext4slower 等慢操作工具逐条列出超过阈值的操作,看出写后同步的模式。
  55. bpftrace约 800 词bpftrace单行命令、跟踪 read 的坑、VFS 延迟按类型拆分。要自己写工具时查。
  56. Other Tools约 200 词其他工具别处讲过的十几个 BPF 工具,df、inotify,ZFS 的工具。
  57. Visualizations约 400 词可视化文件系统延迟为什么是双峰,热图怎么读。
  58. Experimentation约 100 词实验测试时让 iostat 一直开着,确认盘上有没有动静。
  59. Ad Hoc约 100 词临时测试用 dd 粗测顺序读写;写的数字是进内存的速度。
  60. Micro-Benchmark Tools约 500 词微基准测试工具Bonnie、fio、FileBench;作者推荐 fio。
  61. Cache Flushing约 100 词清空缓存drop_caches 的三个值,测试前让缓存回到冷状态。
  62. Tuning约 100 词调优最大的收益来自去掉不必要的工作。
  63. Application Calls约 200 词应用的调用fsync 成批提交;posix_fadvise 和 madvise 给缓存提示。
  64. ext4约 300 词ext4挂载选项、tune2fs、/sys/fs/ext4 和 e2fsck;noatime。
  65. ZFS约 200 词ZFSZFS 的属性调优;不用 ZFS 的读者可以跳过。
  66. Exercises约 300 词习题习题;动手部分本页已经给了替代。

词与短语

只收先读和用到再查两档的节。斜体那一行是原句里连续的几个词,在电子书里搜它就能找到那一句。

§0 开篇

blind spot
… file system performance as a blind spot.
盲区
过去的工具只盯着磁盘,文件系统这一层没人量。
rule out
… makes it possible to rule out file systems …
排除(嫌疑)
不是「画线」。量清楚了,就能确定问题不在文件系统和盘上,转去查别处。

§8.2.2 File System Cache

warm up
… populating the cache (warming it up).
预热(缓存)
不是加热。缓存从空到装满常用数据的过程;反义是 cold cache。

§8.3.1 File System Latency

directly and proportionally
… file system latency directly and proportionally affects …
直接地、按比例地(影响)
应用线程阻塞着等,文件系统慢多少,请求就慢多少。
look alarming
… this looks alarming; however, no application …
看着吓人

§8.3.4 Prefetch

keep ahead of
… the disks keep ahead of application requests …
跑在……前头
盘读在应用要之前,应用每次来要,数据都已经在缓存里了。
pollute the cache
… not need, polluting the cache and consuming …
污染缓存
没用的数据占了缓存,把有用的挤出去。

§8.3.6 Write-Back Caching

in jeopardy
… the business may be in jeopardy.
岌岌可危

§8.3.9 Non-Blocking I/O

honor
… which may honor non-blocking only for …
遵守,支持(某个标志)
熟词生义:honor a flag 是「照这个标志办」,不是「尊敬」。

§8.3.10 Memory-Mapped Files

accomplish very little
… with mmap(2) may accomplish very little, …
几乎没用
往轻里说。前一句作者说常见人不分析就上 mmap;瓶颈在等盘时,省掉系统调用无济于事。

§8.3.12 Logical vs. Physical I/O

counterintuitive
Although it may seem counterintuitive, I/O requested …
违反直觉的
inflated / deflated
… implicit, inflated, or deflated as compared …
(IO)膨胀了 / 缩水了
本义是充气、放气。这里指到了盘上,IO 比应用发出的多了或少了。
in concert
… how these factors can occur in concert, …
同时起作用
不是「在音乐会上」。几种因素叠在一起发生。

§8.3.13 Operations Are Not Equal

can’t tell much
You can’t tell much about a workload …
看不出多少东西
tell 在这里是「判断出」,不是「告诉」。

§8.4.3 File System Caches

address
These problems have largely been addressed by …
解决,处理(问题)
技术文章里 address a problem 极常见,和「地址」「演讲」都无关。
revert to
… RCU-walk reverts to the slower reference-count walk …
退回到(慢的办法)

§8.4.5 File System Types

resistance
… there is still resistance due to the …
(来自社区的)抵触

§8.4.6 Volumes and Pools

resilvering
… Also called resilvering, this is when …
(RAID)重建
本义是给镜子重新镀银。镜像盘坏了换新盘、把数据同步过去,借用了这个说法。

§8.5.1 Disk Analysis

conveniently
… you have conveniently focused on the expected …
图省事地
带点讽刺:只看盘,正好看的是自己预想的地方,省事,但可能找错。
miss entire classes of issues
… confusing and misses entire classes of issues …
漏掉整整几类问题
miss 是「漏掉」,不是「想念」。

§8.5.2 Latency Analysis

isolated instances
… there are isolated instances of high latency …
零星的几次
isolated 不是「被隔离的」,是「孤立出现的、个别的」。
steer
… as it can steer the performance investigation …
把(排查)引向

§8.5.7 Workload Separation

separate spindles
… known as using “separate spindles,” since …
各用各的盘
spindle 是机械盘的主轴,代指一块盘。说法来自机械盘时代。

§8.5.8 Micro-Benchmarking

imply
… and may imply a disk benchmark …
让人以为(测的是盘)
工具号称测盘,文件却小到全在缓存里。

§8.6.7 strace

skewed by observer effect
… latency can be skewed by observer effect.
被观测者效应扭曲
观测工具自己的开销改变了量到的数,第二个实验里就看得到。

§8.6.8 fatrace

alleviate somewhat
This may be alleviated somewhat by filtering …
多少缓解一点

§8.6.12 cachestat

Its best use today may be
Its best use today may be simply …
眼下它最大的用处,也许只是……
作者自嘲:这个工具不稳定,最大的意义可能是证明这种工具做得出来。

§8.6.18 Visualizations

cloud
… appearing as a “cloud,” which is likely …
(热图上的)一团
不是云计算。热图上方散开的一大片,像一团云。

§8.7 Experimentation

which may mean not at all
… which may mean not at all.
也就是可能根本不该有(磁盘 IO)
接在「确认负载按预期到达盘上」之后:工作集装得进缓存时,「按预期」就是盘上一点动静都没有。

§8.8 Tuning

tuning wins
… can lead to the highest tuning wins …
调优的收益
win 在性能领域常指一次改进带来的收益,不是「赢」。

诊断题:换成你来查

场景和输出都是本站编的。先自己判断,再点选项看解释。

采集程序跟不上,最该改什么?§8.3.7

场景:一块 ARM 板上的采集程序每收到一条数据就往 eMMC 上的日志文件写一行,文件是 O_SYNC 打开的。数据一密集,采集线程就积压。你用 strace 看了一会儿:

write(5, ..., 96) = 96 <0.011820>
write(5, ..., 112) = 112 <0.009347>
write(5, ..., 88) = 88 <0.014105>

每次写一百来字节却要等十毫秒左右,时间全花在等闪存落盘上:线程每秒最多写一百条上下。攒一批再 fsync,一批只等一次盘,这是书里说的成批提交(§8.3.7、§8.8.1)。mmap 省掉的是系统调用的开销,等盘的时间一点没少(§8.3.10);O_SYNC 的写根本不经过脏页的积累,调 dirty 参数没有用;换大容量的盘也不改变每次同步写的延迟。strace 本身也会拉长每次调用,但这里的十毫秒远大于它的开销。

同事要加定时任务清缓存,该不该?§8.3.2

场景:一台 16 GB 的服务器跑你的 C++ 文件服务,同事看到下面的 free 输出,担心内存快用完了,想每小时执行一次 echo 3 > /proc/sys/vm/drop_caches。

        total   used   free  shared  buff/cache  available
Mem:    15890   3490    410      25       12360      12400

page cache 会长到占满空闲内存,这是设计如此(§8.3.2)。判断还剩多少看 available:它算上了能马上回收的缓存(§8.6.2)。定时清缓存,等于每小时把这台文件服务的命中率打回零,接下来一阵子的读全要等盘,延迟分布右边那一峰会突然变高。drop_caches 适合做基准测试前让缓存回到冷状态(§8.7.3),不是日常维护手段。

平均读延迟 0.85 ms,这说明什么?§8.6.18

场景:服务每个请求读一小块文件,监控上读延迟的平均值是 0.85 毫秒,团队觉得「盘有点慢」。你用 ext4dist 看了 10 秒的读:

usecs              count
0 -> 1              8120
2 -> 3              1530
4 -> 7               210
(中间几桶都是 0)
2048 -> 4095         400
4096 -> 8191         900
8192 -> 16383        250

左峰 9860 次在几微秒内,右峰 1550 次在 2 到 16 毫秒:典型的命中加未命中(§8.6.13)。没有一次读真的花了 0.85 毫秒。两条路:提高命中率(工作集和可用内存比一比,看应用是不是把内存占得太多),或者降低未命中的代价(这才轮到盘,到第 9 章查)。下一步用 ext4slower-bpfcc 1 看慢的是哪些文件、哪些偏移,有没有本来可以顺序读的随机读。

盘每半分钟忙一阵,要不要处理?§8.3.1

场景:iostat 显示数据盘每隔三十来秒写延迟就冲到 80 毫秒、利用率接近 100%,持续一两秒。服务的 p99 延迟一直平稳,ext4slower-bpfcc 10 跑了几分钟没有输出。

time       w/s   w_await   %util
10:00:01    12      0.9      1.1
10:00:31   840     82.4     96.0
10:00:32    15      1.2      1.5

书里专门拿这种情况举例:后台刷写在磁盘统计上看着吓人,可是没有应用在等它(§8.3.1、§8.3.12)。应用这一层没有超过 10 毫秒的操作,说明请求没被它拖累。把过期时间调短会让写更碎、更频繁,还少了同一页多次修改只写一次的好处;改 O_DIRECT 是大改,而且让每次写都直接等盘。只有当读请求排在写回后面、ext4slower 开始出现慢读时,才需要动手。

这个写速度可信吗?§8.7.1

场景:一块 1 GB 内存的 ARM 板,厂家说 eMMC 顺序写大约 40 MB/s。你在板子上测了一下:

$ dd if=/dev/zero of=test bs=1M count=200
209715200 bytes (210 MB, 200 MiB) copied, 1.1 s, 191 MB/s

默认的写是写回缓存,dd 报告的是把数据拷进内存的速度(§8.7.1)。工作集比内存小时,测的是缓存而不是盘(§8.5.8)。conv=fsync 让 dd 在结束前 fsync 一次,时间里就包括了真正写到 eMMC 的部分;测的同时另开一个终端看 /proc/meminfo 的 Dirty,或者有 iostat 就开着,确认数据确实到了盘上。换 urandom 会把生成随机数的 CPU 开销混进来,eMMC 一般也不压缩数据。

值得停下来的句子

For this reason, representing the distribution as a single value—such as a mean, mode, or median—is misleading.— §8.6.18。本站译:因此,用一个数——平均值、众数或中位数——来代表这个分布,会误导人。

这句话前面是理由:文件系统延迟本来就是两个峰,一个是缓存命中,一个是去存储设备。监控面板上最常见的恰恰是一个平均值。读者自己的服务大概率也是这样:缓存命中率九成以上,平均值被几微秒的命中拉得很好看,用户抱怨的却是那几次几十毫秒的读。看到「平均延迟正常」时,先问一句分布是什么样的,上面的模拟器把这件事演了一遍。

From the disk device-level statistics, this looks alarming; however, no application is waiting on these to complete.— §8.3.1。本站译:从磁盘设备层的统计看,这很吓人;可是并没有哪个应用在等它们完成。

「这」指后台刷写已写数据时突发的高延迟磁盘 IO。它说的是这一章的立足点:衡量好坏的是应用等了多久,不是盘有多忙。反过来也成立:盘很闲,应用也可能在等文件系统(等锁、等同一个文件上的 fsync)。所以排查从应用那一层的延迟开始,确认确实在等,再往下走到盘。

带走的几条

一句话:从应用这一层量文件系统延迟、看整个分布,分清命中、未命中和同步写,确认应用真在等了,再去看盘。

先算占比

一个请求里等文件系统的时间占多少。占九成,值得深挖;占百分之一,去查别处。

§8.5.2
看分布,不看平均

命中和未命中差两三个数量级,平均值落在两峰之间。用直方图、p99 或逐条的慢操作(ext4dist、ext4slower)。

§8.6.18
buff/cache 大是好事

闲内存拿去做缓存,应用要用时会被收回。看 available,别定时 drop_caches。

§8.3.2
write() 返回不等于落盘

普通写只是进了内存,脏页默认放约 30 秒才写回,掉电会丢。要保证落盘用 fsync 或 O_SYNC,代价是每次都等盘。

§8.3.6
板子上同步写要攒批

慢闪存上每次 fsync 都可能是毫秒级。逐条同步写会钉死吞吐,攒一批再 fsync,批的大小按能丢多少定。

§8.3.7
盘忙不等于应用在等

后台写回、预读、别的进程都会让盘忙。逻辑 IO 和物理 IO 对不上是常态,以应用这一层为准。

§8.3.12
测试先想清楚测的是什么

文件比内存小,测的是缓存。测盘要用 direct IO 或把 fsync 算进去,同时看着盘上有没有 IO。

§8.5.8