--- title: 文件 IO 基础与深入探究 tags: [ 嵌入式Linux, Linux应用编程, 文件IO, 文件描述符, open, read, write, lseek, dup, fcntl, 原子操作, inode, IMX6ULL, ] created: 2026-09-18 updated: 2026-09-18 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第二章 文件I/O基础、第三章 深入探究文件I/O" --- # 文件 IO 基础与深入探究 > 💡 **关联知识**:[[01-Linux应用编程基础/01-应用编程概念与开发环境]]、[[01-Linux应用编程基础/03-标准IO库]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/2. 系统调用IO]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/3. 文件描述符操作]] Linux 下"一切皆文件"。文件 I/O 既是应用编程最基础的部分,也是最容易"会用但不懂"的部分。本篇分两层: - **基础层**(第二章):`open / read / write / close / lseek` 五个系统调用,够你完成绝大多数简单文件读写; - **深入层**(第三章):文件描述符背后发生了什么、`errno` 怎么用、`O_APPEND/O_TRUNC` 的细节、多次打开同一文件、`dup`、文件共享、原子操作与竞争冒险、`fcntl/ioctl`、文件截断。 --- ## 1. 一个简单的文件 IO 示例 通用 IO 模型通常包括:打开文件、读写文件、关闭文件,涉及 4 个函数 `open()`、`read()`、`write()`、`close()`。 ```c #include #include #include #include int main(void) { char buff[1024]; int fd1, fd2; int ret; /* 打开源文件 src_file(只读方式) */ fd1 = open("./src_file", O_RDONLY); if (-1 == fd1) return fd1; /* 打开目标文件 dest_file(只写方式) */ fd2 = open("./dest_file", O_WRONLY); if (-1 == fd2) { ret = fd2; goto out1; } /* 读取源文件 1KB 数据到 buff 中 */ ret = read(fd1, buff, sizeof(buff)); if (-1 == ret) goto out2; /* 将 buff 中的数据写入目标文件 */ ret = write(fd2, buff, sizeof(buff)); if (-1 == ret) goto out2; ret = 0; out2: /* 关闭目标文件 */ close(fd2); out1: /* 关闭源文件 */ close(fd1); return ret; } ``` 流程:先 `open` 源文件和目标文件,成功后 `read` 源文件 1KB 数据,再 `write` 到目标文件,最后 `close` 两个文件。 ```mermaid flowchart LR A["open 源文件
O_RDONLY"] --> B["open 目标文件
O_WRONLY"] B --> C["read 源文件
1024 字节"] C --> D["write 目标文件
1024 字节"] D --> E["close 目标文件"] E --> F["close 源文件"] ``` --- ## 2. 文件描述符(File Descriptor) `open` 执行成功时返回一个**非负整数**,这个返回值就是**文件描述符(fd)**。Linux 内核用文件描述符索引所有已打开的文件;所有 IO 系统调用都通过文件描述符找到对应文件。 - 一个进程可以打开多个文件,但**数量有限**; - 默认上限为 1024,即文件描述符取值范围 `0~1023`; - 分配原则:**从最小的没有被使用的文件描述符开始分配**;文件关闭后 fd 被释放,可再次分配给新打开的文件; - 每个被打开的文件在同一进程内有唯一 fd,不会重复; - 超过进程可打开的最大文件数限制,内核会发送警告信号并结束进程。 查看上限: ```bash ulimit -n # 1024 ``` ### 2.1 0、1、2 去哪了 程序里 `open` 得到的 fd 通常从 **3** 开始,因为 `0、1、2` 已被系统默认占用: | 文件描述符 | 名称 | 宏 | 默认对应设备 | | ---------- | -------- | --------------- | ------------ | | 0 | 标准输入 | `STDIN_FILENO` | 键盘 | | 1 | 标准输出 | `STDOUT_FILENO` | LCD 显示器 | | 2 | 标准错误 | `STDERR_FILENO` | LCD 显示器 | 这三个宏定义在 `` 中: ```c #define STDIN_FILENO 0 /* Standard input. */ #define STDOUT_FILENO 1 /* Standard output. */ #define STDERR_FILENO 2 /* Standard error output. */ ``` > Linux 下一切皆文件,也包括各种硬件设备。每个硬件设备都对应系统下的某个**设备文件**,应用通过对设备文件读写来操控硬件,如 LCD、串口、音频、键盘等。标准输入可理解为"打开键盘设备文件得到的 fd",标准输出/标准错误可理解为"打开显示设备文件得到的 fd"。 --- ## 3. open 打开文件 操作文件前要先打开,得到 fd,读写后再关闭。`open` 既可以打开已存在的文件,也可以创建新文件。 ```c #include #include #include int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); ``` `open` 有两种原型,原因是 C 语言不支持重载,它是**可变参函数**:可传 2 个参数,也可传 3 个参数,第三个参数 `mode` 只在 `flags` 含 `O_CREAT` 或 `O_TMPFILE` 时有效。 用 man 查看帮助: ```bash man 2 open # 2 表示系统调用;1 是命令,3 是标准 C 库函数 ``` ### 3.1 参数说明 | 参数 | 类型 | 说明 | | ---------- | -------------- | ---------------------------------------------------------------- | | `pathname` | `const char *` | 文件路径,可含绝对/相对路径;若为符号链接会对其解引用 | | `flags` | `int` | 打开标志,可用 ` | ` 组合多个 | | `mode` | `mode_t` | 新建文件的访问权限,仅当 `flags` 含 `O_CREAT`/`O_TMPFILE` 时有效 | **返回值**:成功返回文件描述符(非负整数);失败返回 `-1`。 ### 3.2 flags 常用标志 | 标志 | 用途 | 说明 | | ------------- | ----------------------------- | ----------------------------------------------------------------------------- | | `O_RDONLY` | 以只读方式打开 | 这三个是文件访问权限标志,**必须且只能包含其中一种**,决定 fd 能做什么操作 | | `O_WRONLY` | 以只写方式打开 | 同上 | | `O_RDWR` | 以可读可写方式打开 | 同上 | | `O_CREAT` | 文件不存在则创建 | 需传入第 3 个参数 `mode` 指定权限 | | `O_DIRECTORY` | 要求是目录 | `pathname` 不是目录则调用失败 | | `O_EXCL` | 与 `O_CREAT` 配合专门创建文件 | 若文件已存在则返回错误,可用于测试文件是否存在,使"测试+创建"成为**原子操作** | | `O_NOFOLLOW` | 不解引用符号链接 | 若 `pathname` 是符号链接直接返回错误;不加则解引用 | > 不同内核版本支持的 flags 存在差别,man 手册中会说明某标志从哪个版本开始支持。 多个标志通过位或组合: ```c open("./src_file", O_RDONLY); /* 单独使用一个标志 */ open("./src_file", O_RDONLY | O_NOFOLLOW); /* 多个标志组合 */ ``` 其它常用但本篇稍后展开的标志还有:`O_APPEND`、`O_TRUNC`、`O_NONBLOCK`、`O_SYNC`、`O_DSYNC`、`O_ASYNC`、`O_NOATIME`、`O_TMPFILE` 等。 ### 3.3 mode 文件权限 `mode` 类型是 `mode_t`(`u32` 无符号整型)。权限位从低位往上看,每 3 个 bit 一组: ```text S U U U G G G O O O rwx rwx rwx ``` | 字段 | 含义 | | ------------ | ----------------------------------------- | | `O`(3 bit) | 其他用户的权限 | | `G`(3 bit) | 同组用户(与文件所有者有相同组 ID)的权限 | | `U`(3 bit) | 文件所属用户的权限 | | `S`(3 bit) | 文件特殊权限,一般少用 | 每组 3 个 bit 按 `rwx` 顺序分配:最高位(权值 4)读、中间位(权值 2)写、最低位(权值 1)执行。 举例: - `111111111`(二进制)= `777`(八进制)= `511`(十进制):所有用户都有读/写/执行权限; - `111000000`:仅文件所属者拥有读、写、执行权限; - `100100100`:所属者、同组用户、其他用户都只有读权限。 实际编程中直接使用 Linux 已定义好的权限宏: | 宏定义 | 说明 | | --------- | ------------------------ | | `S_IRUSR` | 文件所属者读 | | `S_IWUSR` | 文件所属者写 | | `S_IXUSR` | 文件所属者执行 | | `S_IRWXU` | 文件所属者读、写、执行 | | `S_IRGRP` | 同组用户读 | | `S_IWGRP` | 同组用户写 | | `S_IXGRP` | 同组用户执行 | | `S_IRWXG` | 同组用户读、写、执行 | | `S_IROTH` | 其他用户读 | | `S_IWOTH` | 其他用户写 | | `S_IXOTH` | 其他用户执行 | | `S_IRWXO` | 其他用户读、写、执行 | | `S_ISUID` | set-user-ID(特殊权限) | | `S_ISGID` | set-group-ID(特殊权限) | | `S_ISVTX` | sticky(特殊权限) | 这些宏可单独使用,也可位或组合: ```c S_IRUSR | S_IWUSR | S_IROTH ``` > `O_RDONLY / O_WRONLY / O_RDWR` 决定"以什么方式打开",与文件权限是两回事:**只有用户对该文件具有相应权限时,才能用对应标志打开文件,否则打开失败**。若程序对该文件只有只读权限,用 `O_RDWR` 或 `O_WRONLY` 打开会失败。 ### 3.4 open 使用示例 ```c /* (1) 只读方式打开已存在文件 */ int fd = open("./app.c", O_RDONLY); if (-1 == fd) return fd; /* (2) 可读可写方式打开已存在文件 */ int fd = open("./app.c", O_RDWR); if (-1 == fd) return fd; /* (3) 可读可写打开;若是符号链接则不解引用,直接返回错误 */ int fd = open("/home/dengtao/hello", O_RDWR | O_NOFOLLOW); if (-1 == fd) return fd; /* (4) 不存在则创建:所属者 rwx,同组和其他用户只读 */ int fd = open("/home/dengtao/hello", O_RDWR | O_CREAT, S_IRWXU | S_IRGRP | S_IROTH); if (-1 == fd) return fd; ``` --- ## 4. write 写文件 ```c #include ssize_t write(int fd, const void *buf, size_t count); ``` | 参数 | 说明 | | ------- | ---------------------- | | `fd` | 文件描述符 | | `buf` | 待写入数据所在的缓冲区 | | `count` | 要写入的字节数 | **返回值**:成功返回实际写入的字节数(0 表示未写入任何字节);若返回值小于 `count`,**这不是错误**(例如磁盘空间已满可能发生这种情况);写入出错返回 `-1`。 --- ## 5. read 读文件 ```c #include ssize_t read(int fd, void *buf, size_t count); ``` | 参数 | 说明 | | ------- | -------------------- | | `fd` | 文件描述符 | | `buf` | 存放读取数据的缓冲区 | | `count` | 希望读取的字节数 | **返回值**:成功返回实际读取的字节数;实际值可能小于 `count`,也可能为 0(如已到文件末尾)。 例如文件在到达末尾前只剩 30 字节,而要求读 100 字节,则 `read` 返回 30;下一次再 `read` 返回 0。 --- ## 6. close 关闭文件 ```c #include int close(int fd); ``` **返回值**:成功返回 0,失败返回 -1。 - 进程终止时,内核会**自动关闭**该进程打开的所有文件,所以很多程序不显式调用 `close`; - 但显式关闭是良好编程习惯,代码更可读、更可靠; - 文件描述符是有限资源,不再需要时应释放归还系统。 --- ## 7. lseek 定位读写位置 每个打开的文件,系统都会记录它的**读写位置偏移量**(读写偏移量),`read/write` 从当前偏移量开始操作。文件第一个字节的位置偏移量为 0;打开文件时偏移量指向文件开始处;每次 `read/write` 后会自动移动,指向已读/已写数据后的下一字节。 ```c #include #include off_t lseek(int fd, off_t offset, int whence); ``` | 参数 | 说明 | | -------- | ---------------------------- | | `fd` | 文件描述符 | | `offset` | 偏移量,以字节为单位 | | `whence` | 偏移量参考基准,取以下宏之一 | | `whence` | 含义 | | ---------- | ---------------------------------------------- | | `SEEK_SET` | 指向 `offset` 字节处(从文件头算) | | `SEEK_CUR` | 指向「当前位置 + offset」处,`offset` 可正可负 | | `SEEK_END` | 指向「文件末尾 + offset」处,`offset` 可正可负 | **返回值**:成功返回从文件头部算起的当前位置偏移量(字节);出错返回 `-1`。 ```c /* 移动到文件开头 */ off_t off = lseek(fd, 0, SEEK_SET); /* 移动到文件末尾 */ off_t off = lseek(fd, 0, SEEK_END); /* 移动到偏移文件头 100 字节处 */ off_t off = lseek(fd, 100, SEEK_SET); /* 获取当前读写位置偏移量 */ off_t off = lseek(fd, 0, SEEK_CUR); ``` --- ## 8. 完整示例:文件拷贝 + 偏移读取 下面这份源码对应第二章的编程实战例 1:打开 `src_file`(只读),新建 `dest_file`(只写、权限 `rwxr--r--`),从 `src_file` 偏移 500 字节处读取 1KB,写入 `dest_file` 开头,最后显式关闭所有文件。 ```c #include #include #include #include #include int main(void) { char buffer[1024]; int fd1, fd2; int ret; /* 打开 src_file 文件 */ fd1 = open("./src_file", O_RDONLY); if (-1 == fd1) { printf("Error: open src_file failed!\n"); return -1; } /* 新建 dest_file 文件并打开 */ fd2 = open("./dest_file", O_WRONLY | O_CREAT | O_EXCL, S_IRWXU | S_IRGRP | S_IROTH); if (-1 == fd2) { printf("Error: open dest_file failed!\n"); ret = -1; goto err1; } /* 将 src_file 读写位置移动到偏移文件头 500 个字节处 */ ret = lseek(fd1, 500, SEEK_SET); if (-1 == ret) goto err2; /* 读取 src_file 数据,大小 1KByte */ ret = read(fd1, buffer, sizeof(buffer)); if (-1 == ret) { printf("Error: read src_file failed!\n"); goto err2; } /* 将 dest_file 读写位置移动到文件头 */ ret = lseek(fd2, 0, SEEK_SET); if (-1 == ret) goto err2; /* 将 buffer 中的数据写入 dest_file,大小 1KByte */ ret = write(fd2, buffer, sizeof(buffer)); if (-1 == ret) { printf("Error: write dest_file failed!\n"); goto err2; } printf("OK: test successful\n"); ret = 0; err2: close(fd2); err1: close(fd1); return ret; } ``` 逐段解释: | 代码 | 说明 | | ------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | `open("./src_file", O_RDONLY)` | 只读打开源文件;失败返回 -1 | | `open("./dest_file", O_WRONLY \| O_CREAT \| O_EXCL, S_IRWXU\|S_IRGRP\|S_IROTH)` | 只写方式创建新文件;`O_EXCL` 保证文件已存在时失败;权限 `rwxr--r--` | | `lseek(fd1, 500, SEEK_SET)` | 把源文件读位置移到偏移 500 字节处 | | `read(fd1, buffer, 1024)` | 读 1KB 到 `buffer` | | `lseek(fd2, 0, SEEK_SET)` | 把目标文件写位置移到开头 | | `write(fd2, buffer, 1024)` | 写 1KB | | `goto err2/err1` | 出错时逐级释放资源,避免 fd 泄漏 | 编译运行: ```bash gcc -o testApp_1 testApp_1.c # 准备一个 >= 1KB 的 src_file ls -l src_file ./testApp_1 ls -l dest_file # 大小应为 1024,权限为 rwxr--r-- ``` --- ## 9. Linux 系统如何管理文件(深入) ### 9.1 静态文件与 inode 文件未被打开时存放在磁盘文件系统中,称为**静态文件**。磁盘的最小存储单位是**扇区**(Sector,512 字节);操作系统一次读取一个**块**(block),通常 4KB(连续 8 个扇区)。 磁盘分区、格式化时会分为两个区域: - **数据区**:存储文件中的数据; - **inode 区**:存放 `inode table`,其中是一个个 `inode`。 每个文件对应唯一一个 inode,inode 本质是一个结构体,记录:文件字节大小、文件所有者、读/写/执行权限、时间戳、文件类型、**文件数据存储的 block 位置**等信息。注意:**文件名不记录在 inode 中**。 查看 inode 编号: ```bash ls -i file # 或 stat file ``` ```mermaid flowchart LR F["文件名"] -->|"1. 找到 inode 编号"| N["inode 编号"] N -->|"2. 在 inode table 中定位"| IT["inode table
inode 结构体"] IT -->|"3. 根据 inode 记录的 block 位置"| B["数据区中的 block"] ``` 打开一个文件,系统内部大致分三步: 1. 找到文件名对应的 inode 编号; 2. 通过 inode 编号从 inode table 找到 inode 结构体; 3. 根据 inode 记录的信息确定数据所在的 block,读出数据。 > U 盘"快速格式化"很快,因为它只清除了 inode table,真正存储文件数据的区域没动,所以数据仍可能被找回。 ### 9.2 打开文件时的状态 调用 `open` 时,内核会申请一段内存(缓冲区),把静态文件数据从磁盘读取到内存中管理、缓存。内存中的这份叫**动态文件(内核缓冲区)**。之后对该文件的读写都是针对内存中的动态文件,而不是磁盘中的静态文件。 当动态文件被改写后,与磁盘静态文件不同步,内核会在之后把动态文件更新到磁盘。这解释了: - 打开大文件比较慢(要先读入内存); - 文档写了一半没保存就断电,重启后内容丢失(尚未同步到磁盘)。 这样设计的原因:磁盘/硬盘/U 盘多为 Flash 块设备,块设备以"块"为单位读写,改一个字节也要把整块读出、修改、写回,非常不灵活;而内存可按字节随机操作,且读写速率远高于磁盘。因此**先把静态文件缓存为动态文件**,读写针对内存,效率更高。 ### 9.3 进程、文件描述符表、文件表、inode 内核为每个进程设置专门的数据结构管理进程状态、运行特征等,称为**进程控制块(PCB)**。 PCB 中有指针指向**文件描述符表(File descriptors)**;表中的每个元素索引到对应的**文件表(File table)**;文件表记录:文件状态标志、引用计数、当前文件读写偏移量、i-node 指针(指向该文件对应的 inode)等。 ```mermaid flowchart LR subgraph 进程["进程 PCB"] FDT["文件描述符表"] FD0["fd 0"] FD1["fd 1"] FD2["fd 2"] FD3["fd 3"] FDT --- FD0 FDT --- FD1 FDT --- FD2 FDT --- FD3 end subgraph 打开文件表["打开文件表(File table)"] FT1["文件表项
状态标志 / 引用计数
偏移量 / i-node 指针"] end INODE["inode
数据 block 位置 / 权限 / 大小"] FD3 --> FT1 FT1 --> INODE INODE --> BLK["磁盘数据块"] ``` --- ## 10. 返回错误处理与 errno 前面示例出错后只能返回 -1,却不知道原因。Linux 为常见错误做了编号,函数出错时操作系统把错误编号赋给 **`errno`**: - `errno` 是 `int` 类型变量,每个进程维护自己的 `errno`; - 存储**就近发生**的错误编号,下一次出错会覆盖上一次; - **并非所有**系统调用或库函数出错都会设置 `errno`,可查 man 手册的返回值描述; - 包含 `` 即可使用。 ```c #include #include int main(void) { printf("%d\n", errno); return 0; } ``` ### 10.1 strerror() 把错误编号转换成可读字符串(C 库函数,`man 3 strerror`): ```c #include char *strerror(int errnum); ``` | 参数 | 说明 | | -------- | ---------------- | | `errnum` | 错误编号 `errno` | **返回值**:对应错误编号的字符串描述。 ```c #include #include #include #include #include #include #include int main(void) { int fd; fd = open("./test_file", O_RDONLY); if (-1 == fd) { printf("Error: %s\n", strerror(errno)); return -1; } close(fd); return 0; } ``` 文件不存在时打印: ```text Error: No such file or directory ``` ### 10.2 perror() 更常用:内部自动读取 `errno`,直接把错误信息打印出来,还可在前面加自己的提示信息(`man 3 perror`): ```c #include void perror(const char *s); ``` | 参数 | 说明 | | ---- | ---------------------------------------------- | | `s` | 错误提示字符串之前附加的信息,不加则传空字符串 | **返回值**:无(`void`)。 ```c #include #include #include #include #include int main(void) { int fd; fd = open("./test_file", O_RDONLY); if (-1 == fd) { perror("open error"); return -1; } close(fd); return 0; } ``` 输出: ```text open error: No such file or directory ``` `perror` 会在附加信息后自动加入冒号和空格分隔。推荐优先使用 `perror`。 --- ## 11. exit、_exit、_Exit 进程退出分**正常退出**和**异常退出**,这里只讨论正常退出。正常退出有三种方式: ### 11.1 `_exit()` 与 `_Exit()`(系统调用) `_exit()` 会清除进程使用的内存空间,销毁内核中的各种数据结构,关闭进程所有文件描述符,结束进程并把控制权交给操作系统。它会**直接进入内核终止进程**,不执行 stdio 缓冲刷新等清理。 ```c #include void _exit(int status); ``` ```c #include void _Exit(int status); ``` `_exit()` 和 `_Exit()` 两者等价,用法作用一样,都是系统调用。`status` 为状态标志:0 表示正常结束,其它值表示执行过程中检测到有错误发生。 ```c #include #include #include #include #include int main(void) { int fd; fd = open("./test_file", O_RDONLY); if (-1 == fd) { perror("open error"); _exit(-1); } close(fd); _exit(0); } ``` ### 11.2 `exit()`(标准 C 库函数) `exit()` 是标准 C 库函数,会执行一些清理工作,**最后调用 `_exit()`**。用法与 `_exit()`/`_Exit()` 一样。 ```c #include void exit(int status); ``` ### 11.3 三种终止方法对比 | 方式 | 类型 | 是否刷新 stdio 缓冲 | 头文件 | | ------------------ | -------- | ------------------- | ------------ | | `main` 中 `return` | 语言机制 | 是 | — | | `exit(status)` | C 库函数 | 是 | `` | | `_exit(status)` | 系统调用 | **否** | `` | | `_Exit(status)` | 系统调用 | **否** | `` | 推荐使用 `exit()`。`return`、`exit()`、`_exit()/_Exit()` 的区别在标准 I/O 缓冲场景下会显现(见 [[01-Linux应用编程基础/03-标准IO库]])。 --- ## 12. 空洞文件(Hole File) `lseek` 不仅能改变位置偏移量,还**允许偏移量超出文件长度**。例如一个 4K 的文件,把偏移量移到 6000 字节处再 `write`,那么 4096~6000 字节之间没有写入数据,形成**空洞**,该文件称为**空洞文件**。 - 空洞部分**不占用物理空间**,直到对该区域写入数据时才分配; - 但空洞文件形成时,**逻辑上文件大小包含空洞部分**。 用途:对多线程共同操作文件非常有用。要构建一个大文件,可把文件分段,多个线程各写一段。实际场景: - 迅雷下载未完成时文件已占据全部大小,从不同地址同时写入; - 创建虚拟机分配 100G 磁盘,实际只用了 3、4G。 ```c #include #include #include #include #include #include #include int main(void) { int fd; int ret; char buffer[1024]; int i; /* 打开文件:不存在则创建,权限 rw-r--r-- */ fd = open("./hole_file", O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (-1 == fd) { perror("open error"); exit(-1); } /* 将文件读写位置移动到偏移文件头 4096 字节(4K)处 */ ret = lseek(fd, 4096, SEEK_SET); if (-1 == ret) { perror("lseek error"); goto err; } /* 初始化 buffer 为 0xFF */ memset(buffer, 0xFF, sizeof(buffer)); /* 循环写入 4 次,每次 1K */ for (i = 0; i < 4; i++) { ret = write(fd, buffer, sizeof(buffer)); if (-1 == ret) { perror("write error"); goto err; } } ret = 0; err: close(fd); exit(ret); } ``` 测试: ```bash gcc -o hole hole.c && ./hole ls -l hole_file # 逻辑大小 8K(4K 空洞 + 4K 数据) du -h hole_file # 实际占用 4K ``` `ls` 看到的是**逻辑大小**(含空洞),`du` 看到的是**实际占用存储块大小**。 > 思考题:用 `read` 读取文件空洞部分,读到的是什么?(可自己写代码验证。) --- ## 13. O_APPEND 与 O_TRUNC ### 13.1 O_TRUNC 使用 `O_TRUNC` 打开文件时,会将文件原本的内容**全部丢弃**,文件大小变为 0。 ```c #include #include #include #include #include #include int main(void) { int fd; fd = open("./test_file", O_WRONLY | O_TRUNC); if (-1 == fd) { perror("open error"); exit(-1); } close(fd); exit(0); } ``` 即使不读写,仅用 `O_TRUNC` 打开再关闭,文件也会被清空(比如 8760 字节 → 0)。 ### 13.2 O_APPEND 使用 `O_APPEND` 打开文件后,**每次 `write` 都会自动把写位置偏移量移动到文件末尾**,从文件末尾开始写入。 ```c #include #include #include #include #include #include #include int main(void) { char buffer[16]; int fd; int ret; /* 打开文件,携带 O_APPEND */ fd = open("./test_file", O_RDWR | O_APPEND); if (-1 == fd) { perror("open error"); exit(-1); } /* 初始化 buffer 中的数据 */ memset(buffer, 0x55, sizeof(buffer)); /* 写入 4 个字节数据 */ ret = write(fd, buffer, 4); if (-1 == ret) { perror("write error"); goto err; } /* 将 buffer 清 0 */ memset(buffer, 0x00, sizeof(buffer)); /* 将位置偏移量移动到距离文件末尾 4 个字节处 */ ret = lseek(fd, -4, SEEK_END); if (-1 == ret) { perror("lseek error"); goto err; } /* 读取最后 4 个字节 */ ret = read(fd, buffer, 4); if (-1 == ret) { perror("read error"); goto err; } printf("0x%x 0x%x 0x%x 0x%x\n", buffer[0], buffer[1], buffer[2], buffer[3]); ret = 0; err: close(fd); exit(ret); } ``` 打印 `0x55 0x55 0x55 0x55`,说明 `O_APPEND` 生效:写入确实发生在文件末尾。 两个重要细节: 1. **`O_APPEND` 不影响读**:读文件时位置偏移量默认仍在文件头; 2. **`O_APPEND` 下用 `lseek` 改不了写位置**:写入依然从文件末尾开始。 > 思考题:`O_APPEND` 与 `O_TRUNC` 同时指定时会怎样? --- ## 14. 多次打开同一个文件 同一个文件可以被多次打开:在一个进程中多次打开,或在多个进程中分别打开。 ### 14.1 得到的 fd 不同 一个进程内多次 `open` 同一文件,会得到**多个不同的 fd**,关闭时需依次 `close`。 ```c fd1 = open("./test_file", O_RDWR); fd2 = open("./test_file", O_RDWR); fd3 = open("./test_file", O_RDWR); printf("%d %d %d\n", fd1, fd2, fd3); /* 例如 6 7 8 */ ``` > 在 VSCode 提供的终端里运行,得到的可能是 6、7、8 而不是 3、4、5,因为 3、4、5 已被 VSCode 进程占用,而子进程会继承父进程的文件描述符。直接在 Ubuntu Terminal 里运行则得到 3、4、5。 ### 14.2 内存中只有一份动态文件 多次打开同一文件,**内存中不会存在多份动态文件**。验证:先用 `fd1` 写入 4 字节,再用 `fd2`(未 `lseek`)从文件头读取,若能读到刚写的数据,说明共享同一份动态文件。 ```c /* 通过 fd1 写入 4 个字节 0x11 0x22 0x33 0x44 */ write(fd1, buffer, 4); /* fd2 从文件头读取 */ lseek(fd2, 0, SEEK_SET); read(fd2, buffer, 4); /* 读出的仍是 0x11 0x22 0x33 0x44,证明只有一份动态文件 */ ``` ### 14.3 不同 fd 的读写偏移量相互独立 多次打开会得到多个 fd,对应多个**文件表**,读写偏移量记录在文件表中,因此不同 fd 的偏移量**相互独立**;但文件表中的 i-node 指针指向同一个 inode。 ```mermaid flowchart LR subgraph P["进程"] A["fd1"] B["fd2"] end A --> T1["文件表 1
偏移量 off1
i-node 指针"] B --> T2["文件表 2
偏移量 off2
i-node 指针"] T1 --> I["同一个 inode"] T2 --> I ``` > 多个不同进程 `open` 同一文件,内存中同样只维护一份动态文件,多个进程共享,但各有独立的读写偏移量。 **动态文件何时关闭?** 当文件的**引用计数为 0** 时,系统自动关闭它。文件表中记录引用计数,表示当前被多少个 fd 关联。 ### 14.4 分别写还是接续写 两个 fd 各自独立偏移量,所以默认是**分别写**(各从各的位置写,会互相覆盖)。测试: ```c /* 循环 4 次,交替写入 */ write(fd1, buffer1, 4); /* 0x11 0x22 0x33 0x44 */ write(fd2, buffer2, 4); /* 0xAA 0xBB 0xCC 0xDD */ ``` 因为两者偏移量独立、都从 0 开始,读取结果是 `aabbccdd...`(fd1 写的数据被 fd2 覆盖)。 想变成**接续写**,可用 `O_APPEND`:`O_APPEND` 使每次 `write` 都自动到文件末尾,于是变成 `11223344aabbccdd...`。 --- ## 15. 复制文件描述符:dup 与 dup2 复制成功得到新的 fd,新旧 fd 都可操作文件,权限相同。**复制得到的 fd 与旧 fd 指向同一个文件表**,所以对文件的读写权限、状态标志、文件偏移量都相同——"复制"实则是复制文件表。使用完也要 `close`。 ### 15.1 dup ```c #include int dup(int oldfd); ``` | 参数 | 说明 | | ------- | ---------------------- | | `oldfd` | 需要被复制的文件描述符 | **返回值**:成功返回新的文件描述符(由系统按 fd 分配原则分配);失败返回 `-1` 并设置 `errno`。 ```c fd2 = dup(fd1); /* fd1=6 -> 得到 fd2=7 */ ``` 由于新旧 fd 指向同一个文件表,偏移量共享,因此**不用 `O_APPEND` 也能实现接续写**。 ### 15.2 dup2 `dup` 不能自己指定 fd,`dup2` 修复了这个缺陷: ```c #include int dup2(int oldfd, int newfd); ``` | 参数 | 说明 | | ------- | -------------------------------------------- | | `oldfd` | 需要被复制的文件描述符 | | `newfd` | 指定一个文件描述符(需是当前进程没有使用的) | **返回值**:成功返回手动指定的 `newfd`;失败返回 `-1` 并设置 `errno`。 ```c fd2 = dup2(fd1, 100); /* 得到 fd2 == 100 */ ``` > 同一个 fd 可以多次调用 `dup`/`dup2` 复制,得到多个不同的 fd。 **完整 dup 测试源码**(实现接续写): ```c #include #include #include #include #include #include int main(void) { unsigned char buffer1[4], buffer2[4]; int fd1, fd2; int ret; int i; /* 创建新文件 test_file 并打开 */ fd1 = open("./test_file", O_RDWR | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); if (-1 == fd1) { perror("open error"); exit(-1); } /* 复制文件描述符 */ fd2 = dup(fd1); if (-1 == fd2) { perror("dup error"); ret = -1; goto err1; } printf("fd1: %d\nfd2: %d\n", fd1, fd2); buffer1[0] = 0x11; buffer1[1] = 0x22; buffer1[2] = 0x33; buffer1[3] = 0x44; buffer2[0] = 0xAA; buffer2[1] = 0xBB; buffer2[2] = 0xCC; buffer2[3] = 0xDD; /* 循环写入数据,共享偏移量,因此是接续写 */ for (i = 0; i < 4; i++) { write(fd1, buffer1, sizeof(buffer1)); write(fd2, buffer2, sizeof(buffer2)); } /* 回到文件头读取全部数据 */ lseek(fd1, 0, SEEK_SET); for (i = 0; i < 8; i++) { read(fd1, buffer1, sizeof(buffer1)); printf("%x%x%x%x", buffer1[0], buffer1[1], buffer1[2], buffer1[3]); } printf("\n"); ret = 0; err2: close(fd2); err1: close(fd1); exit(ret); } ``` 输出应为 `11223344aabbccdd11223344aabbccdd`,证明是接续写。 --- ## 16. 文件共享 **文件共享**指同一个文件(对应同一个 inode)被多个独立读写体同时进行 IO 操作。独立读写体可理解为同一个文件的多个不同 fd(多次 `open` 得到,或 `dup/dup2` 得到)。核心问题:**如何制造多个不同 fd 指向同一个文件**。 三种常见实现方式: ```mermaid flowchart TB subgraph M1["方式一:同进程多次 open"] A1["fd1"] --> TA1["文件表 1"] A2["fd2"] --> TA2["文件表 2"] TA1 --> IA["同一 inode"] TA2 --> IA end subgraph M2["方式二:不同进程各自 open"] B1["进程1 fd1"] --> TB1["文件表 1"] B2["进程2 fd2"] --> TB2["文件表 2"] TB1 --> IB["同一 inode"] TB2 --> IB end subgraph M3["方式三:dup / dup2 复制"] C1["fd1"] --> TC["同一个文件表
共享偏移量"] C2["fd2"] --> TC TC --> IC["同一 inode"] end ``` | 方式 | fd 是否各自独立 | 是否共享偏移量 | 典型用途 | | ------------------- | ---------------- | -------------- | ---------------- | | 同进程多次 `open` | 是(多个文件表) | 否 | 需要独立读写位置 | | 不同进程各自 `open` | 是(多个文件表) | 否 | 进程间共享文件 | | `dup/dup2` 复制 | 是(同一文件表) | **是** | 接续写、重定向 | 文件共享存在**竞争冒险**,且要关心不同读写体之间是分别写还是接续写。 --- ## 17. 原子操作与竞争冒险 ### 17.1 竞争冒险 假设进程 A 和 B 都对同一文件追加写(文件末尾写入),都调用了 `open` 但**未使用 `O_APPEND`**。每个进程有自己的 PCB、自己的文件表(独立偏移量),但共享同一个 inode。 1. 进程 A 运行,`lseek` 把 A 的偏移量设为 1500(文件末尾); 2. A 时间片耗尽,切换到 B; 3. 进程 B `lseek`,也把 B 的偏移量设为 1500; 4. B `write` 100 字节,B 的偏移量变为 1600; 5. B 时间片耗尽,切回 A; 6. A `write`,从 A 的偏移量 1500 开始写——但 1500 处已不是文件末尾,**覆盖了 B 刚写入的数据**。 ```mermaid sequenceDiagram participant A as 进程 A participant B as 进程 B participant F as 文件 A->>F: lseek 到 1500(末尾) Note over A: 时间片耗尽 B->>F: lseek 到 1500(末尾) B->>F: write 100 字节(到 1600) Note over B: 时间片耗尽 A->>F: write(仍从 1500 开始)→ 覆盖 B 的数据 ``` 这种两个(或多个)操作共享资源、结果不可预期的情形,就是**竞争状态(竞争冒险)**。因为进程获得 CPU 使用权的先后由操作系统调配,不可预期。 ### 17.2 原子操作 问题出在"先定位到文件末尾、再写"分成了两个函数调用。解决办法是把这两步合并成一个**原子操作**:原子操作由多步组成,要么一步也不执行,一旦执行就必须执行完所有步骤,不可能只执行子集。 **(1) `O_APPEND` 实现原子操作** `O_APPEND` 使每次 `write` 时把「移动写位置到文件末尾、写入数据」组成一个原子操作。无论怎么调换顺序,写入都从文件末尾开始,不会出现 A 覆盖 B 的情况。 **(2) `pread()` 与 `pwrite()`** 与 `read`/`write` 作用一样,区别是可用于实现原子操作,并传入位置偏移量 `offset`。调用 `pread` 相当于 `lseek` + `read`,且把两步组成原子操作。 ```c #include ssize_t pread(int fd, void *buf, size_t count, off_t offset); ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset); ``` | 参数 | 说明 | | -------------------- | -------------------------- | | `fd`、`buf`、`count` | 与 `read`/`write` 意义相同 | | `offset` | 当前读或写的位置偏移量 | **返回值**:与 `read`/`write` 意义相同。 与「`lseek` + `read/write`」的区别: 1. 调用 `pread` 时,无法中断其定位和读操作(即**原子操作**); 2. **不更新文件表中的当前位置偏移量**。 验证第 2 点:`pread(fd, buffer, 100, 1024)` 后,`lseek(fd, 0, SEEK_CUR)` 返回 0 而不是 1124。 ```c #include #include #include #include #include #include int main(void) { unsigned char buffer[100]; int fd; int ret; fd = open("./test_file", O_RDWR); if (-1 == fd) { perror("open error"); exit(-1); } /* 从偏移文件头 1024 字节处读取 100 字节 */ ret = pread(fd, buffer, sizeof(buffer), 1024); if (-1 == ret) { perror("pread error"); goto err; } /* 获取当前位置偏移量,预期仍为 0 */ ret = lseek(fd, 0, SEEK_CUR); if (-1 == ret) { perror("lseek error"); goto err; } printf("Current Offset: %d\n", ret); /* 输出 0 */ ret = 0; err: close(fd); exit(ret); } ``` **(3) 创建一个文件(`O_EXCL`)** 进程 A、B 都要打开一个不存在的文件:A 先 `open("./file", O_RDWR)` 失败,时间片耗尽;B 同样失败,接着 B 用 `O_CREAT` 创建成功;切回 A,A 也用 `O_CREAT` 创建"成功"——同一个文件被创建两次,这是不允许的。 用 `O_EXCL` + `O_CREAT`:若文件已存在则返回错误,若不存在则创建,把「判断是否存在、创建文件」合并成一个原子操作,保证进程是文件的创建者。 --- ## 18. fcntl 与 ioctl ### 18.1 fcntl `fcntl()` 可对一个已打开的 fd 执行一系列控制操作,类似"多功能文件描述符管理工具箱"。 ```c #include #include int fcntl(int fd, int cmd, ... /* arg */ ); ``` | 参数 | 说明 | | ----- | --------------------------------- | | `fd` | 文件描述符 | | `cmd` | 操作命令,以 `F_XXX` 开头 | | `...` | 可变参,第三个参数根据 `cmd` 传入 | `cmd` 大致分 5 类功能: | 功能 | cmd | | ----------------------- | ---------------------------- | | 复制文件描述符 | `F_DUPFD`、`F_DUPFD_CLOEXEC` | | 获取/设置文件描述符标志 | `F_GETFD`、`F_SETFD` | | 获取/设置文件状态标志 | `F_GETFL`、`F_SETFL` | | 获取/设置异步 IO 所有权 | `F_GETOWN`、`F_SETOWN` | | 获取/设置记录锁 | `F_GETLK`、`F_SETLK` | **返回值**:失败返回 `-1` 并设置 `errno`;成功时返回值与 `cmd` 有关(如 `F_DUPFD` 返回新 fd、`F_GETFD` 返回描述符标志、`F_GETFL` 返回文件状态标志)。 **(1) 复制文件描述符** `cmd = F_DUPFD`:根据 `fd` 复制出新 fd,第三个参数指出新 fd 是**大于或等于该参数的可用 fd**;若该参数等于已存在的 fd,则取大于它的可用 fd。 ```c #include #include #include #include #include #include int main(void) { int fd1, fd2; int ret; fd1 = open("./test_file", O_RDONLY); if (-1 == fd1) { perror("open error"); exit(-1); } /* 复制 fd1,新 fd 需大于等于 0 */ fd2 = fcntl(fd1, F_DUPFD, 0); if (-1 == fd2) { perror("fcntl error"); ret = -1; goto err; } printf("fd1: %d\nfd2: %d\n", fd1, fd2); ret = 0; close(fd2); err: close(fd1); exit(ret); } ``` **(2) 获取/设置文件状态标志** `F_GETFL` 获取、`F_SETFL` 设置。设置的就是调用 `open` 时传入的 flags,但有限制: - **文件权限标志**(`O_RDONLY`、`O_WRONLY`、`O_RDWR`)不能被设置; - **文件创建标志**(`O_CREAT`、`O_EXCL`、`O_NOCTTY`、`O_TRUNC`)不能被设置,会被忽略; - Linux 下只有 `O_APPEND`、`O_ASYNC`、`O_DIRECT`、`O_NOATIME`、`O_NONBLOCK` 这些标志可以被修改。 ```c #include #include #include #include #include #include int main(void) { int fd; int ret; int flag; fd = open("./test_file", O_RDWR); if (-1 == fd) { perror("open error"); exit(-1); } /* 获取文件状态标志 */ flag = fcntl(fd, F_GETFL); if (-1 == flag) { perror("fcntl F_GETFL error"); ret = -1; goto err; } printf("flags: 0x%x\n", flag); /* 在原标志基础上添加 O_APPEND */ ret = fcntl(fd, F_SETFL, flag | O_APPEND); if (-1 == ret) { perror("fcntl F_SETFL error"); goto err; } ret = 0; err: close(fd); exit(ret); } ``` ### 18.2 ioctl `ioctl()` 可认为是一个文件 IO 操作的"杂物箱",处理的事情非常杂、不统一,一般用于操作特殊文件或硬件外设(例如用它获取 LCD 相关信息)。 ```c #include int ioctl(int fd, unsigned long request, ...); ``` | 参数 | 说明 | | --------- | ---------------------------------------------------------- | | `fd` | 文件描述符 | | `request` | 与具体要操作的对象有关,没有统一值,表示向 fd 请求相应操作 | | `...` | 可变参,第三个参数根据 `request` 决定 | **返回值**:成功返回 0,失败返回 -1。 --- ## 19. 截断文件:truncate 与 ftruncate 使用 `truncate()` 或 `ftruncate()` 可将普通文件截断为指定字节长度。 ```c #include #include int truncate(const char *path, off_t length); int ftruncate(int fd, off_t length); ``` 两者功能一样,区别在于:`ftruncate()` 用文件描述符 `fd` 指定目标文件,`truncate()` 直接用文件路径 `path`。 **行为**: - 若当前文件大小 **大于** `length`,多余数据丢失(被"砍"掉); - 若当前文件大小 **小于** `length`,文件被扩展,扩展部分读取得到空字节 `"\0"`。 **使用 `ftruncate()` 前**必须先用 `open()` 打开文件得到 fd,且必须具有可写权限(`O_WRONLY` 或 `O_RDWR`)。 **注意**:调用这两个函数**不会改变文件读写位置偏移量**,截断后一般需要重新设置读写位置,以免之前指向的位置已不存在而出错。 **返回值**:成功返回 0,失败返回 `-1` 并设置 `errno`。 ```c #include #include #include #include #include #include int main(void) { int fd; /* 打开 file1 文件 */ if (0 > (fd = open("./file1", O_RDWR))) { perror("open error"); exit(-1); } /* 使用 ftruncate 将 file1 截断为 0 字节 */ if (0 > ftruncate(fd, 0)) { perror("ftruncate error"); exit(-1); } /* 使用 truncate 将 file2 截断为 1024 字节 */ if (0 > truncate("./file2", 1024)) { perror("truncate error"); exit(-1); } close(fd); exit(0); } ``` 运行前 `file1`、`file2` 均为 592 字节;运行后 `file1` 变 0,`file2` 变 1024。 --- ## 20. 实验步骤与调试方法 ### 20.1 通用实验流程 ```bash # 1. 建目录 mkdir -p ~/vscode_ws/2_chapter && cd ~/vscode_ws/2_chapter # 2. 创建源文件(用 VSCode 或 vi) # 粘贴本篇示例,如 testApp_1.c、hole.c、dup_test.c 等 # 3. 编译 gcc -Wall -o testApp_1 testApp_1.c # 4. 准备测试文件 dd if=/dev/urandom of=src_file bs=1K count=2 # 生成 2KB 随机文件 # 5. 运行、验证 ./testApp_1 ls -l dest_file xxd dest_file | head # 查看内容 ``` ### 20.2 调试方法 | 现象 | 可能原因 | 排查手段 | | ------------------------- | --------------------------------- | ---------------------------------------------------- | | `open` 返回 -1 | 文件不存在/权限不足/路径错误 | `perror("open error")`;`ls -l` 查权限;`pwd` 查路径 | | `read` 返回 0 | 已到文件末尾 | 结合 `lseek(fd,0,SEEK_END)` 看文件大小 | | `read` 返回值小于 `count` | 到达文件末尾,不是错误 | 用返回值而非 `count` 判断 | | `write` 少于 `count` | 磁盘空间满等,不是错误 | 检查磁盘空间 `df -h` | | 读到的内容和预期不符 | 偏移量没设置对 | 用 `lseek(..., SEEK_CUR)` 打印当前偏移量 | | 数据"少了"/互相覆盖 | 竞争冒险或分别写 | 加 `O_APPEND`,或用 `dup`/`pread` | | 文件逻辑大小与占用不符 | 空洞文件 | 对比 `ls -l` 与 `du` | | fd 泄漏 | 出错分支没 `close` | 用 `lsof -p ` 观察,检查 `goto` 清理链 | | 6、7、8 而不是 3、4、5 | 父进程(如 VSCode)占用了 3、4、5 | 直接在系统 Terminal 运行验证 | 常用工具: ```bash man 2 open # 系统调用手册 man 3 strerror # 库函数手册 strace ./testApp # 跟踪程序发起的系统调用与参数、返回值 lsof -p $(pgrep testApp) # 查看进程打开的文件描述符 ls -i file # 查看 inode 编号 stat file # 查看文件元数据 du -h file # 查看实际占用 ``` `strace` 尤其适合验证本篇知识点: ```bash strace -e trace=open,openat,read,write,lseek,close ./testApp_1 ``` --- ## 21. 跨平台对比:IMX6ULL vs STM32 vs RK3568 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。 | 维度 | I.MX6ULL | STM32(裸机/RTOS) | RK3568 | | ---------- | ----------------------------------------------------- | ----------------------------------------------- | --------------------------------- | | 文件抽象 | 一切皆文件,fd 为索引 | 无 fd 概念,直接操作存储/寄存器 | 同 I.MX6ULL | | 打开方式 | `open` 返回 fd | 无;用 FatFs 的 `f_open()` 返回 `FIL` | `open` 返回 fd | | 读写接口 | `read`/`write`(系统调用) | `f_read`/`f_write`(库函数,操作 FILE) | `read`/`write` | | 偏移定位 | `lseek` | `f_lseek` | `lseek` | | 缓冲 | read/write 无用户缓冲;内核有 page cache | 通常无内核缓冲,由 FatFs 自管 | 同 I.MX6ULL | | 并发与原子 | 多进程共享文件,存在竞争冒险;`O_APPEND`/`pread` 解决 | 一般单任务,竞争问题多由 RTOS 信号量/临界区解决 | 同 I.MX6ULL | | dup/重定向 | `dup`/`dup2`/`fcntl` | 无此概念 | 同 I.MX6ULL | | 权限模型 | 有 uid/gid/rwx,`open` 受权限约束 | 无文件权限概念 | 同 I.MX6ULL | | 设备访问 | 通过 `/dev/xxx` + `ioctl` | 直接读写寄存器或 HAL 库 | 通过 `/dev/xxx` + `ioctl` | | 32/64 位 | 32 位(`off_t` 注意大文件) | 通常 32 位,无文件系统或 FatFs | 64 位,注意 `off_t`/`size_t` 宽度 | 要点: - I.MX6ULL 与 RK3568 的**应用层文件 IO 接口完全通用**,代码可直接迁移,差异主要在架构位宽、glibc 版本、存储介质(NAND/eMMC)与内核版本; - STM32 是"另一套世界观":没有系统调用、没有 fd、没有 inode、没有权限,不要把这些概念套到裸机上; - 在 I.MX6ULL 上操作 `/dev` 下的字符设备时,`open/read/write/ioctl` 的语义由驱动定义,理解本篇的 fd 与文件表模型是读懂驱动行为的基础。 --- ## 22. 面试精选 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。 ### Q1:什么是文件描述符?0、1、2 分别是什么?为什么 `open` 通常从 3 开始分配? **答**:文件描述符是内核返回给进程、用于索引已打开文件的**非负整数**。0、1、2 在进程启动时已被默认占用,分别对应标准输入、标准输出、标准错误(宏 `STDIN_FILENO`/`STDOUT_FILENO`/`STDERR_FILENO`)。fd 从最小可用值开始分配,所以 `open` 通常得到 3。默认上限 1024(`ulimit -n`),范围 0~1023;fd 关闭后会被释放并可重新分配。若超过进程最大文件数,内核会发信号并结束进程。 ### Q2:`open` 的 `O_APPEND` 和 `O_TRUNC` 分别有什么作用?为什么说 `O_APPEND` 实现了原子操作? **答**:`O_TRUNC` 在打开文件时将原有内容全部丢弃、文件大小变 0。`O_APPEND` 使每次 `write` 都自动把写位置偏移量移动到文件末尾,从文件末尾开始写。之所以说它是原子操作:追加写的本质是"先定位到文件末尾,再写",若分成 `lseek` + `write` 两步,在多进程环境下会出现 A 定位后被切走、B 写完、A 回来仍从旧位置写而覆盖 B 数据的竞争冒险。`O_APPEND` 把这两步合并成一个不可分割的原子操作,从而避免覆盖。补充:`O_APPEND` 不影响读;且使用 `O_APPEND` 后,`lseek` 无法改变写位置。 ### Q3:同一个文件被多次打开,内存中会有多份文件数据吗?不同 fd 的读写偏移量互相影响吗? **答**:不会有多份动态文件。多次打开同一文件,内核在内存中只维护一份动态文件(数据缓存),多个 fd 共享;但每次 `open` 会得到不同的 fd 和不同的**文件表**,而读写偏移量记录在文件表中,所以各 fd 的偏移量**相互独立**,默认是"分别写"。而通过 `dup`/`dup2` 复制得到的 fd 指向**同一个文件表**,因此共享偏移量,可实现"接续写"。动态文件在引用计数为 0 时被系统关闭。 ### Q4:`dup` 和 `dup2` 有什么区别?如何用它们在不用 `O_APPEND` 的情况下实现接续写? **答**:`dup(oldfd)` 复制文件描述符,新 fd 由系统按 fd 分配原则分配,不能指定;`dup2(oldfd, newfd)` 可手动指定新 fd(须是当前进程未使用的),失败均返回 -1 并设置 `errno`。二者复制得到的 fd 与原 fd 指向同一个文件表,因此共享文件偏移量、状态标志和访问权限。所以只要 `dup`/`dup2` 出第二个 fd,两个 fd 交替 `write` 时就会接着对方的位置写,实现接续写,而无需 `O_APPEND`。 ### Q5:什么是竞争冒险?`open`/`pread` 中分别用什么机制避免?`pread` 和 `lseek+read` 有何区别? **答**:竞争冒险指多个进程/线程并发操作同一共享资源时,因获得 CPU 的先后顺序不可预期,导致结果不可预期。典型场景是两个进程分别"先 `lseek` 到文件末尾、再 `write`"做追加写,结果互相覆盖。解决思路是把多步操作合并为**原子操作**:`open` 层面用 `O_APPEND`(把"定位末尾+写"合并),创建文件用 `O_EXCL|O_CREAT`(把"判断存在+创建"合并);读写层面用 `pread`/`pwrite`(把"定位+读写"合并)。`pread`/`pwrite` 与 `lseek`+`read`/`write` 的区别有两点:一是定位和读写不可中断、是原子操作;二是**不更新文件表中记录的当前位置偏移量**(`pread` 后 `lseek(SEEK_CUR)` 仍返回 0)。 --- **内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第二章 文件I/O基础、第三章 深入探究文件I/O