第 13 章 文件 I/O 缓冲 (File I/O Buffering)

      +

      核心结论

      • 两层缓冲:内核 buffer cache(page cache)+ stdio 缓冲;两者都会缓冲数据;理解它们是优化 I/O 性能的关键。

      • Buffer Cache 行为read/write 不直接操作磁盘——数据先进入内核 page cache;内核异步刷盘;多次小写合并为大写。

      • Buffer Cache 大小影响性能:大 buffer 减少系统调用次数,性能显著提升;1 字节 vs 4096 字节差 50 倍(§13.1 表 13-1)。

      • stdio 缓冲setvbuf 控制缓冲模式——_IONBF(无缓冲)、_IOLBF(行缓冲,终端默认)、_IOFBF(全缓冲,磁盘文件默认);BUFSIZ 8192 字节。

      • fflush:强制 stdio 输出缓冲写入内核;fflush(NULL) 刷新所有流;输入流调用则丢弃缓冲。

      • 同步 I/Ofsync(fd) 强制数据落盘;fdatasync 只同步数据不同步元数据;sync() 同步所有;O_SYNC/O_DSYNC/O_SYNC flag 控制写入语义。

      • 直接 I/OO_DIRECT 绕过 buffer cache,直接 DMA 到用户缓冲区;适合数据库等需要完全控制 I/O 的程序;要求对齐(通常 512 字节)和 size 限制。

      本章主旨

      本章是第 4-5 章文件 I/O 的延伸。核心:理解内核 buffer cache + stdio 缓冲的双层结构;理解为何「小写不如大写快」「O_SYNC 影响性能」「何时需要直接 I/O」。本章不展开内存映射 I/O(详见第 49 章)、异步 I/O(详见第 63 章)、IO 调度(详见第 14 章)。

      一、核心概念

      本章围绕 6 个核心概念展开:从「内核缓冲」到「stdio 缓冲」再到「同步与直接 I/O」。

      概念 定义 + 重要性 实现提示

      Buffer Cache (Page Cache)

      内核维护的数据缓存;read/write 不直接操作磁盘;内核异步刷盘;Linux 2.4+ 与 mmap 文件共享同一 page cache。

      §13.1;性能关键——大 buffer 减少 syscall 50 倍。

      stdio 缓冲

      C 标准库的 I/O 缓冲;默认全缓冲(磁盘文件)、行缓冲(终端)、无缓冲(stderr);setvbuf 改变。

      §13.2;BUFSIZ 8192 字节;fflush 强制刷出。

      Buffer 大小与性能

      大 buffer 显著提升 I/O 性能;4096 字节接近最优;过小(1 字节)性能极差(50-100 倍差距)。

      §13.1 表 13-1;4096 字节是常见文件系统块大小。

      同步 I/O

      fsync/fdatasync 强制数据落盘;O_SYNC/O_DSYNC flag 让每次 write 都同步;数据库需要。

      §13.3;fsync 慢(毫秒级);fdatasync 略快。

      直接 I/O (O_DIRECT)

      绕过 buffer cache;用户缓冲区直接 DMA 到设备;要求对齐(512/4096 字节)和 size 匹配;数据库常用。

      §13.6;绕过 OS 缓存——应用程序需自己管理缓存。

      性能权衡

      缓冲提升吞吐但增加延迟(崩溃时数据可能丢失);同步保证安全但慢;直接 I/O 提供控制但责任大。

      §13.1-§13.6;根据场景选择:日志 → fsync;批量处理 → 大 buffer;数据库 → O_DIRECT。

      二、详细笔记

      13.1 内核 Buffer Cache

      What:内核为文件 I/O 维护的内存缓存(page cache);read/write 不直接操作磁盘;数据先进入 cache,再异步刷盘。

      Why:理解 buffer cache 是理解「为什么 write 立即返回」「为什么 read 第二次快」「为什么崩溃时数据可能丢失」的关键。

      How

      操作 内核动作 性能影响

      read(fd, buf, n)

      先查 page cache;命中则拷贝到用户 buf;未命中则读磁盘到 cache,再拷贝

      命中是 μs 级;未命中是 ms 级

      write(fd, buf, n)

      数据从用户 buf 拷贝到 page cache;返回;内核异步刷盘

      write 立即返回;崩溃时未刷盘数据丢失

      读命中

      第二次读同一文件 → 从 cache 返回

      极快

      写合并

      多次小 write → 内核合并为大块异步写

      减少磁盘 I/O 次数

      Buffer Cache 行为细节(§13.1):

      • Linux 2.4+:不再有独立的「buffer cache」——文件 I/O 缓存合并到 page cache,与 mmap 文件共享。

      • 大小无固定上限——按需扩展;受可用内存限制。

      • 内存紧张时——内核刷脏页到磁盘,释放 cache 页。

      性能数据(§13.1 表 13-1,100MB 文件复制):

      BUF_SIZE Elapsed (秒)

      System CPU (秒)

      1

      107.43

      99.12

      16

      7.50

      6.63

      256

      2.06

      1.65

      4096

      2.05

      0.38

      65536

      2.06

      0.32

      观察:

      • 1 字节 → 65536 字节,性能提升 50 倍。

      • 4096 字节接近最优(与文件系统块大小匹配)。

      • 超过 4096 字节后——性能提升微小(主要瓶颈是磁盘)。

      When

      • 大数据复制——用 4KB+ buffer。

      • 频繁小写——合并为大块 write。

      • 实时性要求高——O_SYNCfsync

      Example

      // 摘自《The Linux Programming Interface》第 4 章 fileio/copy.c
      // 推荐 buffer 大小
      #define BUF_SIZE 4096  // 一次读 4KB
      
      char buf[BUF_SIZE];
      ssize_t numRead;
      while ((numRead = read(inputFd, buf, BUF_SIZE)) > 0)
          if (write(outputFd, buf, numRead) != numRead)
              fatal("couldn't write whole buffer");

      13.2 stdio 缓冲

      What:C 标准库为 FILE* 流提供的缓冲层;缓冲模式由 setvbuf 设置;fflush 强制刷出。

      Why:stdio 缓冲让 fprintf 等高级 I/O 与裸 read/write 一样高效;理解何时该 fflush 是写交互式程序的关键。

      How

      // 摘自《The Linux Programming Interface》第 13 章
      #include <stdio.h>
      
      int setvbuf(FILE *stream, char *buf, int mode, size_t size);
      // mode: _IONBF / _IOLBF / _IOFBF
      // 必须在任何其他 stdio 操作前调用
      
      void setbuf(FILE *stream, char *buf);
      // 简化版:buf == NULL → 无缓冲;否则全缓冲
      
      void setbuffer(FILE *stream, char *buf, size_t size);
      // 类似 setbuf 但指定 size
      
      int fflush(FILE *stream);
      // 强制刷出;stream == NULL 刷所有

      缓冲模式:

      模式 行为

      默认场景

      _IONBF

      无缓冲;每次 stdio 调用立即 read/write

      stderr

      _IOLBF

      行缓冲;遇 \n 刷出;输入按行

      终端(stdin/stdout)

      _IOFBF

      全缓冲;缓冲区满或显式 fflush 才刷出

      磁盘文件

      BUFSIZ:默认 buffer 大小;glibc 8192 字节。

      When

      • 交互式提示——printf("Enter: "); 后立即 fflush(stdout)(行缓冲到终端会自动刷)。

      • 关键日志——每次写后 fflush 或用 setvbuf 设为无缓冲。

      • 性能——大块用全缓冲;小块用无缓冲避免缓冲开销。

      Example

      // 摘自《The Linux Programming Interface》第 13 章
      // 配置 stdout 为无缓冲(错误日志)
      if (setvbuf(stdout, NULL, _IONBF, 0) != 0)
          errExit("setvbuf");
      
      // 自定义缓冲区
      static char buf[4096];
      if (setvbuf(fp, buf, _IOFBF, sizeof(buf)) != 0)
          errExit("setvbuf");

      13.3 同步 I/O 完成

      What:SUSv3 定义「同步 I/O 完成」——保证数据已被传输(或诊断为失败);区分「data integrity」(数据完整性)和「file integrity」(文件完整性,包括元数据)。

      Why:数据库、日志等需要崩溃时数据不丢——必须同步到磁盘。

      How

      类型 含义

      synchronized I/O data integrity

      read:数据已被读;pending write 已落盘

      write:数据和检索数据所需元数据已落盘

      synchronized I/O file integrity

      read:同上

      write:数据 + 所有元数据(atime/mtime 等)已落盘

      相关调用(§13.3):

      // 摘自《The Linux Programming Interface》第 13 章
      #include <unistd.h>
      
      int fsync(int fd);
      // 强制 fd 的所有数据 + 元数据落盘;慢(毫秒级)
      // 成功返回 0
      
      int fdatasync(int fd);
      // 只强制数据落盘;跳过非必要元数据(如 atime)
      // 略快于 fsync
      
      void sync(void);
      // 同步所有文件的 buffer cache;不等待

      O_SYNC flag(§13.3):

      // 摘自《The Linux Programming Interface》第 4 章
      // 每次 write 都同步落盘
      int fd = open("log", O_WRONLY | O_CREAT | O_APPEND | O_SYNC, 0644);
      // 每次 write 阻塞直到数据落盘;慢但安全
      
      // O_DSYNC 类似但只同步数据,不同步元数据(更快)
      int fd = open("log", O_WRONLY | O_CREAT | O_APPEND | O_DSYNC, 0644);

      When

      • 关键日志——O_DSYNC 或定期 fsync

      • 数据库事务提交——fsync 保证持久性。

      • 普通配置文件——退出前 fsync

      • 临时文件——不需要 fsync(崩溃丢失可接受)。

      Example

      // 摘自《The Linux Programming Interface》第 13 章
      // 关键日志
      int fd = open("important.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
      write(fd, "critical event\n", 16);
      fsync(fd);  // 确保已落盘
      close(fd);

      13.4 直接 I/O (O_DIRECT)

      WhatO_DIRECT flag 让 read/write 绕过内核 buffer cache;用户缓冲区直接 DMA 到磁盘(需要硬件支持)。

      Why:数据库等需要完全控制缓存——避免「双缓存」(OS 缓存 + 应用缓存)浪费内存。

      How

      // 摘自《The Linux Programming Interface》第 13 章
      #define _GNU_SOURCE
      #include <fcntl.h>
      
      int fd = open("file", O_WRONLY | O_DIRECT | O_SYNC, 0644);
      // 绕过 buffer cache
      // 要求:
      //   1. 用户缓冲区按设备块大小对齐(512/4096 字节)
      //   2. write size 是块大小整数倍
      //   3. 文件 offset 是块大小整数倍

      约束(§13.6):

      • 缓冲区对齐:posix_memalign(&buf, 512, size)aligned_alloc(512, size)

      • size 必须是块大小整数倍——否则 EINVAL

      • offset 必须是块大小整数倍——否则 EINVAL

      • 不支持 mmap。

      When

      • 数据库(PostgreSQL、MySQL InnoDB 等)——管理自己的缓存。

      • 高性能科学计算——避免数据复制。

      • 不适合普通应用——OS 缓冲已经很快。

      Example

      // 摘自《The Linux Programming Interface》第 13 章
      #define _GNU_SOURCE
      #include <fcntl.h>
      
      // 4KB 对齐的缓冲区
      void *buf;
      if (posix_memalign(&buf, 4096, 4096) != 0) errExit("posix_memalign");
      memset(buf, 'A', 4096);
      
      int fd = open("data.bin", O_WRONLY | O_CREAT | O_TRUNC | O_DIRECT, 0644);
      if (fd == -1) errExit("open");
      
      if (write(fd, buf, 4096) != 4096) errExit("write");
      fsync(fd);
      close(fd);
      free(buf);

      13.5 系统调用开销

      What:每次系统调用都有开销(用户态↔内核态切换、参数检查、数据拷贝);系统调用比普通函数调用慢一个数量级。

      Why:理解开销能解释「为什么大 buffer 性能好」「为什么 syscall 密集型应用考虑 vDSO/io_uring」。

      How

      开销构成(§3.1):

      • CPU 模式切换(保存/恢复寄存器):约 0.1-0.3 μs。

      • 参数检查:地址验证等。

      • 数据拷贝:用户态↔内核态缓冲区。

      • 返回值设置。

      减少系统调用的方法:

      • 大 buffer 一次读/写(§13.1)。

      • readv/writev:单次调用处理多缓冲区(§5.7)。

      • mmap:减少用户态/内核态拷贝(§49 章)。

      • io_uring:Linux 5.1+ 异步 I/O 框架(超出 TLPI 范围)。

      When

      • 性能敏感——time 命令对比不同 buffer 大小。

      • 大量小 I/O——readv/writevmmap

      Example:从 §3.1 实测:1000 万次 getppid() 约 2.2 秒(≈0.3 μs/次);普通 C 函数返回整数只需 0.11 秒(≈0.01 μs/次)。

      13.6 stdio 与系统调用的关系

      What:stdio 函数(fopen/fread/fprintf 等)在底层调用 read/write;stdio 缓冲减少系统调用次数。

      Why:理解 stdio 与 syscall 的关系能选择合适的 I/O 接口。

      How

      • fopen → openfopen 调用 open;返回 FILE*。

      • fread → read:从 stdio 缓冲读;缓冲空时调 read 填满。

      • fwrite → write:写入 stdio 缓冲;缓冲满或 fflush 时调 write

      • fclose → close:fflush 后 close。

      混合使用的问题

      • read(fd, …​) 绕过 stdio 缓冲——之后 fgets 看不到这些数据(仍在内核缓冲,但 stdio 缓冲有自己的视图)。

      • fileno(fp) 获取 FILE* 对应的 fd;fdopen(fd, mode) 反向。

      When

      • 纯文件复制——open/read/write/close + 大 buffer(最高效)。

      • 格式化输出——fprintf(printf 已内置缓冲)。

      • 避免混用——同一文件描述符用同一层 API。

      三、关键图表

      非可视化条目(缓冲 API 速查)
      API 用途

      Buffer Cache

      内核 page cache;write 立即返回;异步刷盘

      stdio 缓冲模式

      _IONBF(无)/_IOLBF(行)/_IOFBF(全)

      setvbuf

      配置 stdio 缓冲(必须在首次 I/O 前)

      fflush

      强制刷出;NULL 刷所有

      fsync(fd)

      强制 fd 数据+元数据落盘

      fdatasync(fd)

      仅数据落盘(更快)

      sync()

      同步所有 buffer

      O_SYNC flag

      每次 write 都同步落盘

      O_DSYNC flag

      每次 write 仅同步数据

      O_DIRECT flag

      绕过 buffer cache;需对齐

      BUFSIZ

      默认 stdio buffer 大小(glibc 8192)

      四、思维导图

      mindmap
        root((第 13 章 文件 I O 缓冲))
          内核 buffer cache
            page cache
            write 立即返回
            异步刷盘
            大小无限制
          性能
            buffer 大小
            4096 最优
            50 倍差距
          stdio 缓冲
            _IONBF 无
            _IOLBF 行 终端
            _IOFBF 全 磁盘
            setvbuf fflush
          同步 I O
            fsync
            fdatasync
            sync
            O_SYNC O_DSYNC
            data file integrity
          直接 I O
            O_DIRECT
            对齐要求
            块大小倍数
            数据库场景
          系统调用开销
            模式切换
            数据拷贝
            readv writev
          stdio 与 syscall
            fopen open
            fread read
            fwrite write
            避免混用

      五、重点与易错点

      1. write 不等于落盘:数据进入 page cache;崩溃时丢失;用 fsync/O_SYNC 保证。

      2. 大 buffer 性能显著提升:1 字节 → 4096 字节,性能提升约 50 倍;推荐 4096 字节。

      3. stdio 默认缓冲策略:磁盘文件全缓冲、终端行缓冲、stderr 无缓冲。

      4. 交互式提示需要 fflushprintf("> "); 不立即显示(行缓冲未触发);加 fflush(stdout) 或用 stderr。

      5. O_SYNC 显著降低 write 性能:毫秒级延迟;只在「数据必须落盘」时用。

      6. O_DIRECT 不是「更快」:绕过 OS 缓存;数据库自己管理缓存才有意义;普通应用用 OS 缓存更好。

      7. O_DIRECT 要求严格对齐:用户缓冲区、offset、size 都必须是设备块大小的整数倍;否则 EINVAL。

      8. 混用 read 和 fgets:绕过 stdio 缓冲的 read 数据对 fgets 不可见;保持单一 I/O 层。

      9. Linux 2.4+ 已无独立 buffer cache:统一为 page cache,与 mmap 共享。

      10. 崩溃恢复策略:关键数据用 fsync;可重建数据不用 fsync;批量操作后台 fsync。

      11. 跨章衔接:第 4-5 章是文件 I/O 基础;第 6 章是进程;第 49 章是 mmap;本章是缓冲机制。