第 4 章 文件 I/O:通用 I/O 模型 (File I/O: The Universal I/O Model)

      +

      核心结论

      • 核心四调用:所有 UNIX 文件 I/O 围绕四个系统调用:open(打开/创建文件)、read(读)、write(写)、close(关闭);它们对所有文件类型(普通文件、设备、管道、套接字)通用。

      • 文件描述符 (fd):进程级非负整数,标识一个打开的文件;0=标准输入,1=标准输出,2=标准错误;通过 open 获取,用 close 释放。

      • open() flags:访问模式 O_RDONLY/O_WRONLY/O_RDWR;创建标志 O_CREATO_EXCLO_TRUNCO_APPEND;状态标志 O_CLOEXECO_NONBLOCKO_SYNCO_DSYNC 等。

      • 内核翻译 I/O:调用 open/read/write/close 时,内核把请求翻译为具体文件系统或设备驱动的操作;应用程序员不需要关心磁盘结构或设备协议。

      • open() 返回最小未用 fd:SUSv3 规定 open() 成功后返回进程未使用的最小 fd 编号——这能保证特定 fd(如 0)被新打开的文件占据。

      • 错误诊断与 flags 组合O_RDWRO_RDONLY | O_WRONLY(前者是值 2,后者是逻辑错误);访问模式标志是值 0/1/2,需用 O_ACCMODE 掩码后比较。

      本章主旨

      本章是 Linux 系统编程「文件 I/O」的入门。核心是 4 个系统调用 open/read/write/close + 文件描述符的概念;并介绍 open() 的 flags 参数——这是后续所有文件操作(普通 I/O、非阻塞 I/O、同步 I/O、内存映射等)的基础。本章不展开 fcntllseekreadv/writev 等高级话题(见第 5 章),也不展开缓冲细节(见第 13 章)。

      一、核心概念

      本章围绕 5 个核心概念展开:文件描述符 + 四大调用 + 通用 I/O 哲学 + open() 的 flags 分类。

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

      文件描述符 (fd)

      进程级的非负整数;标识一个打开的文件;通过 open/socket/pipe 等获取;用 close 释放。

      §4.1;0/1/2 是 stdin/stdout/stderr;POSIX 名 STDIN_FILENO/STDOUT_FILENO/STDERR_FILENO

      四大核心调用

      open(path, flags, mode)read(fd, buf, count)write(fd, buf, count)close(fd)

      §4.1;这些是所有 UNIX 文件 I/O 的基础;std 库 fopen/fread/fwrite/fclose 在它们之上构建。

      通用 I/O (Universality)

      同一组系统调用处理所有文件类型(普通文件、设备、管道、套接字等);内核翻译 I/O 请求为具体操作。

      §4.2;这是 UNIX「一切皆文件」哲学的核心。

      open() flags 三大类

      访问模式(O_RDONLY/WR/RDWR)、创建标志(O_CREAT/EXCL/TRUNC/APPEND)、状态标志(O_CLOEXEC/NONBLOCK/SYNC/DSYNC/ASYNC/DIRECT)。

      §4.3.1;用 `

      ` 组合;SUSv3 标志在大多数 UNIX 系统上可用。

      open() 返回最小未用 fd

      SUSv3 规定 open() 成功后返回进程未使用的最小 fd 编号——可用来强制文件占据特定 fd。

      二、详细笔记

      4.1 文件描述符与四大调用

      What:文件描述符(fd)是进程级的非负整数;标准输入/输出/错误的 fd 是 0/1/2;open/read/write/close 是四大核心调用。

      Why:理解 fd 是进程级的(不是全局的),才能理解「fork 后子进程继承父进程的 fd」和「dup/dup2 复制 fd」等概念(详见第 5 章)。

      How:四大调用签名(§4.1):

      // 摘自《The Linux Programming Interface》第 4 章
      #include <sys/stat.h>
      #include <fcntl.h>
      
      int open(const char *pathname, int flags, ... /* mode_t mode */);
      // 成功返回新 fd;失败返回 -1
      // flags: O_RDONLY (0) / O_WRONLY (1) / O_RDWR (2) / O_CREAT / O_TRUNC / O_APPEND 等
      
      ssize_t read(int fd, void *buffer, size_t count);
      // 成功返回实际读到的字节数(0 表示 EOF);失败返回 -1
      
      ssize_t write(int fd, const void *buffer, size_t count);
      // 成功返回实际写入的字节数;可能 < count(short write)
      
      int close(int fd);
      // 成功返回 0;失败返回 -1

      When

      • 写程序处理任何 I/O——从命令行参数读文件、写日志、socket 通信——都用这一组。

      • 想绕过 stdio 缓冲(fopen)——直接用 open/read/write;第 13 章详述缓冲差异。

      Examplecp 命令的简化实现(Listing 4-1):

      // 摘自《The Linux Programming Interface》第 4 章 fileio/copy.c
      int inputFd = open(argv[1], O_RDONLY);
      int outputFd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);
      char buf[1024];
      ssize_t numRead;
      while ((numRead = read(inputFd, buf, sizeof(buf))) > 0)
          if (write(outputFd, buf, numRead) != numRead)
              fatal("couldn't write whole buffer");

      4.2 通用 I/O 模型

      What:UNIX I/O 的核心是「同一组系统调用处理所有文件类型」——无论普通文件、终端、管道、套接字、设备。

      Why:这是 UNIX 「小工具组合」哲学的基础——./copy test test.old./copy /dev/tty b.txt 用同一份代码。

      How:通用 I/O 通过内核翻译实现:

      1. 应用调用 open/read/write/close

      2. 内核根据 fd 对应的「文件表项」找到对应文件系统或设备驱动。

      3. 内核调用驱动实现的对应操作(vtable 形式)。

      4. 驱动执行实际 I/O(磁盘块、设备寄存器、网络栈)。

      When

      • 写跨文件类型的工具(如 catddtee)——完全用通用 I/O。

      • 需要特定文件类型的能力(如文件锁、终端特殊字符)——用 fcntl()ioctl()(详见第 5 章、第 63 章)。

      Example:以下 4 个 cp 用法都合法(§4.2):

      $ ./copy test test.old           # 普通文件→普通文件
      $ ./copy a.txt /dev/tty          # 普通文件→终端(直接显示)
      $ ./copy /dev/tty b.txt          # 终端→普通文件(读用户输入)
      $ ./copy /dev/pts/16 /dev/tty    # 伪终端对拷

      4.3 open() flags 详解

      Whatopen() 的 flags 参数是位掩码,组合多个标志;分为访问模式、创建标志、状态标志三类。

      Why:flags 决定了「文件是创建还是打开」「写入是覆盖还是追加」「是否在 exec 时关闭」——这是后续所有文件操作的基石。

      How:flags 分类(§4.3.1):

      类别 标志 作用

      访问模式

      O_RDONLY / O_WRONLY / O_RDWR

      三选一,值为 0/1/2

      创建标志

      O_CREAT

      文件不存在则创建;需提供 mode 参数

      创建标志

      O_EXCL

      与 O_CREAT 一起用;文件已存在则失败(原子创建)

      创建标志

      O_TRUNC

      打开时截断到 0 字节

      创建标志

      O_APPEND

      写入总是追加到文件末尾

      状态标志

      O_CLOEXEC

      exec 时自动关闭 fd(防止泄漏给子进程)

      状态标志

      O_NONBLOCK

      非阻塞 I/O

      状态标志

      O_SYNC / O_DSYNC

      同步写入(强制数据到磁盘)

      状态标志

      O_ASYNC

      I/O 就绪时发信号(signal-driven I/O)

      状态标志

      O_DIRECT

      绕过 buffer cache 直接 I/O

      状态标志

      O_NOATIME

      read 不更新访问时间

      状态标志

      O_NOFOLLOW

      不跟随符号链接

      状态标志

      O_DIRECTORY

      pathname 必须是目录

      状态标志

      O_NOCTTY

      不让 pathname 成为控制终端

      When

      • 创建新文件——O_CREAT | O_WRONLY | O_TRUNC,提供 mode(如 0644)。

      • 原子创建——O_CREAT | O_EXCL;若文件已存在则失败。

      • 日志追加——O_WRONLY | O_CREAT | O_APPEND;多进程并发写不交错。

      • exec 时关闭——O_CLOEXEC;多线程程序避免 fd 泄漏给 exec() 后的子进程。

      • 高性能数据库——O_DIRECT + O_SYNC;绕过 page cache。

      Example:常见 open() 模式:

      // 摘自《The Linux Programming Interface》第 4 章
      // 1) 只读打开已有文件
      int fd = open("startup", O_RDONLY);
      
      // 2) 读写打开,不存在则创建,截断
      int fd = open("myfile", O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
      
      // 3) 追加写日志(多进程并发安全)
      int fd = open("w.log", O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);

      4.4 open() 返回最小未用 fd

      What:SUSv3 规定 open() 成功后返回进程未使用的最小 fd 编号;这意味着可以强制新文件占据特定 fd(如 0)。

      Why:某些程序假设某些 fd(如 0 = stdin)已打开;通过「先 close 再 open」可以保证这一假设。

      How:强制 fd 0:

      // 摘自《The Linux Programming Interface》第 4 章
      // 确保文件作为 fd 0 打开
      if (close(STDIN_FILENO) == -1)
          errExit("close");
      int fd = open(pathname, O_RDONLY);
      // 此时 fd == 0(因为 0 是最小未用 fd)

      When

      • 守护进程把 fd 0/1/2 重定向到 /dev/null 或日志文件。

      • 第 5 章的 dup2() 提供更灵活的方式(不依赖 close+open 的时序)。

      Example:守护进程典型模式:

      // 摘自《The Linux Programming Interface》第 4 章
      // 守护进程关闭 0/1/2,然后重新打开到 /dev/null 或日志
      close(STDIN_FILENO);
      close(STDOUT_FILENO);
      close(STDERR_FILENO);
      int fd0 = open("/dev/null", O_RDONLY);    // 必为 0
      int fd1 = open("/dev/null", O_WRONLY);    // 必为 1
      int fd2 = open("/dev/null", O_WRONLY);    // 必为 2

      4.5 mode 参数与文件权限

      Whatopen() 的 mode 参数仅在 O_CREAT 时生效;指定新文件的权限位;实际权限还受进程 umask 影响。

      Why:mode 与 umask 共同决定新文件权限——理解两者关系才能写出「权限正确」的程序。

      How

      常量 含义

      S_IRUSR

      0400

      owner 读

      S_IWUSR

      0200

      owner 写

      S_IXUSR

      0100

      owner 执行

      S_IRGRP

      0040

      group 读

      S_IWGRP

      0020

      group 写

      S_IXGRP

      0010

      group 执行

      S_IROTH

      0004

      other 读

      S_IWOTH

      0002

      other 写

      S_IXOTH

      0001

      other 执行

      实际权限 = mode & ~umask。例如 mode = 0666 & ~umask 0022 = 0644

      When

      • 创建公共可读写文件——mode = 0666;umask 通常是 0022,实际权限 0644

      • 创建私密文件——mode = 0600;不受 umask 影响。

      • 创建可执行文件——mode = 07550750

      Example

      // 摘自《The Linux Programming Interface》第 4 章
      // 公共读写文件:实际权限受 umask 影响
      int fd = open("shared.log", O_WRONLY | O_CREAT | O_TRUNC,
                    S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
      // 0666
      
      // 私密文件:mode 已经是 0600,umask 通常不再削减
      int fd = open("secret.key", O_WRONLY | O_CREAT | O_TRUNC,
                    S_IRUSR | S_IWUSR);
      // 0600

      三、关键图表

      非可视化条目(标准 fd 与 flags 速查)
      项目 描述

      标准 fd 0/1/2

      stdin / stdout / stderr;POSIX 名 STDIN_FILENO / STDOUT_FILENO / STDERR_FILENO

      O_RDONLY / WRONLY / RDWR

      值 0/1/2;不能用

      组合(语义错误);用 fcntl F_GETFL 检查

      O_CREAT

      不存在则创建;需提供 mode

      O_EXCL

      与 O_CREAT 组合;原子创建;文件已存在则失败

      O_TRUNC

      截断到 0 字节

      O_APPEND

      写入追加到末尾;与 lseek+write 相比是原子的

      O_CLOEXEC

      exec 时自动关闭;避免 fd 泄漏(多线程程序必备)

      O_NONBLOCK

      非阻塞 I/O

      O_SYNC / O_DSYNC

      同步写入

      O_DIRECT

      绕过 buffer cache

      mode & ~umask

      四、思维导图

      mindmap
        root((第 4 章 通用 I O 模型))
          文件描述符
            进程级整数
            0 1 2 标准 fd
            POSIX 名称
          四大调用
            open
            read
            write
            close
          通用 I O
            所有文件类型
            内核翻译
            一切皆文件
          open flags
            访问模式
              RDONLY WRONLY RDWR
            创建标志
              CREAT EXCL TRUNC APPEND
            状态标志
              CLOEXEC NONBLOCK SYNC
              DIRECT ASYNC NOATIME
          mode 与权限
            S_IRUSR 等
            umask 影响
            实际权限计算
          返回最小 fd
            SUSv3 保证
            强制特定 fd

      五、重点与易错点

      1. O_RDWR ≠ O_RDONLY | O_WRONLY:访问模式是值 0/1/2 而不是位标志;后者是逻辑错误(得到值 3,无意义)。

      2. O_CREAT 必须提供 mode 参数:否则 mode 从栈取「垃圾值」,新文件权限不可预测。

      3. O_EXCL 必须与 O_CREAT 一起用:单独 O_EXCL 无意义;这是「原子创建文件」的正确方式(详见第 5 章 race condition)。

      4. open() 返回最小未用 fd:可被 dup2fcntl F_DUPFD 绕过;不要假设「我刚 close 的 fd 一定是下一个」。

      5. fd 是进程级的:fork 后子进程继承父进程的 fd 副本;不同进程可以有相同 fd 值指向不同文件。

      6. read 返回 0 表示 EOF:不是错误;不要当成 -1 处理。

      7. read/write 返回 ssize_t:是有符号的;可能小于请求的 count(short read/write),必须循环处理。

      8. read 可能被信号中断(EINTR):要么重试,要么用 SA_RESTART(详见第 20 章)。

      9. write 在普通文件上不保证立即落盘:数据进入 page cache;fsync 才能强制落盘(详见第 13 章)。

      10. O_APPEND 的真正价值是「原子性」:多进程并发追加不会互相覆盖;这是 lseek + write 做不到的(详见第 5 章)。

      11. O_CLOEXEC 是多线程程序的必备:避免 fd 泄漏给 exec 后的子进程;用 fcntl F_SETFD 也可但有竞态。

      12. 跨章衔接:第 5 章展开 fcntldup/dup2pread/pwritereadv/writev;第 13 章展开缓冲机制;第 14 章展开文件系统。

      13. 常见陷阱open("file", O_WRONLY) 创建失败时 mode 参数被忽略;若想创建必须显式 O_CREAT