02-文件IO基础与深入探究.md 49 KB


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()

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

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 两个文件。

flowchart LR
    A["open 源文件<br/>O_RDONLY"] --> B["open 目标文件<br/>O_WRONLY"]
    B --> C["read 源文件<br/>1024 字节"]
    C --> D["write 目标文件<br/>1024 字节"]
    D --> E["close 目标文件"]
    E --> F["close 源文件"]

2. 文件描述符(File Descriptor)

open 执行成功时返回一个非负整数,这个返回值就是文件描述符(fd)。Linux 内核用文件描述符索引所有已打开的文件;所有 IO 系统调用都通过文件描述符找到对应文件。

  • 一个进程可以打开多个文件,但数量有限
  • 默认上限为 1024,即文件描述符取值范围 0~1023
  • 分配原则:从最小的没有被使用的文件描述符开始分配;文件关闭后 fd 被释放,可再次分配给新打开的文件;
  • 每个被打开的文件在同一进程内有唯一 fd,不会重复;
  • 超过进程可打开的最大文件数限制,内核会发送警告信号并结束进程。

查看上限:

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 显示器 |

这三个宏定义在 <unistd.h> 中:

#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 既可以打开已存在的文件,也可以创建新文件。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

open 有两种原型,原因是 C 语言不支持重载,它是可变参函数:可传 2 个参数,也可传 3 个参数,第三个参数 mode 只在 flagsO_CREATO_TMPFILE 时有效。

用 man 查看帮助:

man 2 open      # 2 表示系统调用;1 是命令,3 是标准 C 库函数

3.1 参数说明

参数 类型 说明
pathname const char * 文件路径,可含绝对/相对路径;若为符号链接会对其解引用
flags int 打开标志,可用 `
mode mode_t 新建文件的访问权限,仅当 flagsO_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 手册中会说明某标志从哪个版本开始支持。

多个标志通过位或组合:

open("./src_file", O_RDONLY);                    /* 单独使用一个标志 */
open("./src_file", O_RDONLY | O_NOFOLLOW);       /* 多个标志组合 */

其它常用但本篇稍后展开的标志还有:O_APPENDO_TRUNCO_NONBLOCKO_SYNCO_DSYNCO_ASYNCO_NOATIMEO_TMPFILE 等。

3.3 mode 文件权限

mode 类型是 mode_tu32 无符号整型)。权限位从低位往上看,每 3 个 bit 一组:

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(特殊权限)

这些宏可单独使用,也可位或组合:

S_IRUSR | S_IWUSR | S_IROTH

O_RDONLY / O_WRONLY / O_RDWR 决定"以什么方式打开",与文件权限是两回事:只有用户对该文件具有相应权限时,才能用对应标志打开文件,否则打开失败。若程序对该文件只有只读权限,用 O_RDWRO_WRONLY 打开会失败。

3.4 open 使用示例

/* (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 写文件

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
参数 说明
fd 文件描述符
buf 待写入数据所在的缓冲区
count 要写入的字节数

返回值:成功返回实际写入的字节数(0 表示未写入任何字节);若返回值小于 count这不是错误(例如磁盘空间已满可能发生这种情况);写入出错返回 -1


5. read 读文件

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
参数 说明
fd 文件描述符
buf 存放读取数据的缓冲区
count 希望读取的字节数

返回值:成功返回实际读取的字节数;实际值可能小于 count,也可能为 0(如已到文件末尾)。

例如文件在到达末尾前只剩 30 字节,而要求读 100 字节,则 read 返回 30;下一次再 read 返回 0。


6. close 关闭文件

#include <unistd.h>

int close(int fd);

返回值:成功返回 0,失败返回 -1。

  • 进程终止时,内核会自动关闭该进程打开的所有文件,所以很多程序不显式调用 close
  • 但显式关闭是良好编程习惯,代码更可读、更可靠;
  • 文件描述符是有限资源,不再需要时应释放归还系统。

7. lseek 定位读写位置

每个打开的文件,系统都会记录它的读写位置偏移量(读写偏移量),read/write 从当前偏移量开始操作。文件第一个字节的位置偏移量为 0;打开文件时偏移量指向文件开始处;每次 read/write 后会自动移动,指向已读/已写数据后的下一字节。

#include <sys/types.h>
#include <unistd.h>

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

/* 移动到文件开头 */
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 开头,最后显式关闭所有文件。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

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 泄漏

编译运行:

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 编号:

ls -i file
# 或
stat file
flowchart LR
    F["文件名"] -->|"1. 找到 inode 编号"| N["inode 编号"]
    N -->|"2. 在 inode table 中定位"| IT["inode table<br/>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)等。

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["文件表项<br/>状态标志 / 引用计数<br/>偏移量 / i-node 指针"]
    end
    INODE["inode<br/>数据 block 位置 / 权限 / 大小"]
    FD3 --> FT1
    FT1 --> INODE
    INODE --> BLK["磁盘数据块"]

10. 返回错误处理与 errno

前面示例出错后只能返回 -1,却不知道原因。Linux 为常见错误做了编号,函数出错时操作系统把错误编号赋给 errno

  • errnoint 类型变量,每个进程维护自己的 errno
  • 存储就近发生的错误编号,下一次出错会覆盖上一次;
  • 并非所有系统调用或库函数出错都会设置 errno,可查 man 手册的返回值描述;
  • 包含 <errno.h> 即可使用。

    #include <stdio.h>
    #include <errno.h>
    
    int main(void)
    {
    printf("%d\n", errno);
    return 0;
    }
    

10.1 strerror()

把错误编号转换成可读字符串(C 库函数,man 3 strerror):

#include <string.h>

char *strerror(int errnum);
参数 说明
errnum 错误编号 errno

返回值:对应错误编号的字符串描述。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

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;
}

文件不存在时打印:

Error: No such file or directory

10.2 perror()

更常用:内部自动读取 errno,直接把错误信息打印出来,还可在前面加自己的提示信息(man 3 perror):

#include <stdio.h>

void perror(const char *s);
参数 说明
s 错误提示字符串之前附加的信息,不加则传空字符串

返回值:无(void)。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main(void)
{
    int fd;

    fd = open("./test_file", O_RDONLY);
    if (-1 == fd) {
        perror("open error");
        return -1;
    }

    close(fd);
    return 0;
}

输出:

open error: No such file or directory

perror 会在附加信息后自动加入冒号和空格分隔。推荐优先使用 perror


11. exit、_exit、_Exit

进程退出分正常退出异常退出,这里只讨论正常退出。正常退出有三种方式:

11.1 _exit()_Exit()(系统调用)

_exit() 会清除进程使用的内存空间,销毁内核中的各种数据结构,关闭进程所有文件描述符,结束进程并把控制权交给操作系统。它会直接进入内核终止进程,不执行 stdio 缓冲刷新等清理。

#include <unistd.h>

void _exit(int status);
#include <stdlib.h>

void _Exit(int status);

_exit()_Exit() 两者等价,用法作用一样,都是系统调用。status 为状态标志:0 表示正常结束,其它值表示执行过程中检测到有错误发生。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

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() 一样。

#include <stdlib.h>

void exit(int status);

11.3 三种终止方法对比

方式 类型 是否刷新 stdio 缓冲 头文件
mainreturn 语言机制
exit(status) C 库函数 <stdlib.h>
_exit(status) 系统调用 <unistd.h>
_Exit(status) 系统调用 <stdlib.h>

推荐使用 exit()returnexit()_exit()/_Exit() 的区别在标准 I/O 缓冲场景下会显现(见 [[01-Linux应用编程基础/03-标准IO库]])。


12. 空洞文件(Hole File)

lseek 不仅能改变位置偏移量,还允许偏移量超出文件长度。例如一个 4K 的文件,把偏移量移到 6000 字节处再 write,那么 4096~6000 字节之间没有写入数据,形成空洞,该文件称为空洞文件

  • 空洞部分不占用物理空间,直到对该区域写入数据时才分配;
  • 但空洞文件形成时,逻辑上文件大小包含空洞部分

用途:对多线程共同操作文件非常有用。要构建一个大文件,可把文件分段,多个线程各写一段。实际场景:

  • 迅雷下载未完成时文件已占据全部大小,从不同地址同时写入;
  • 创建虚拟机分配 100G 磁盘,实际只用了 3、4G。

    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    
    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);
    }
    

测试:

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。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

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 都会自动把写位置偏移量移动到文件末尾,从文件末尾开始写入。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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_APPENDO_TRUNC 同时指定时会怎样?


14. 多次打开同一个文件

同一个文件可以被多次打开:在一个进程中多次打开,或在多个进程中分别打开。

14.1 得到的 fd 不同

一个进程内多次 open 同一文件,会得到多个不同的 fd,关闭时需依次 close

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)从文件头读取,若能读到刚写的数据,说明共享同一份动态文件。

/* 通过 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。

flowchart LR
    subgraph P["进程"]
        A["fd1"]
        B["fd2"]
    end
    A --> T1["文件表 1<br/>偏移量 off1<br/>i-node 指针"]
    B --> T2["文件表 2<br/>偏移量 off2<br/>i-node 指针"]
    T1 --> I["同一个 inode"]
    T2 --> I

多个不同进程 open 同一文件,内存中同样只维护一份动态文件,多个进程共享,但各有独立的读写偏移量。

动态文件何时关闭? 当文件的引用计数为 0 时,系统自动关闭它。文件表中记录引用计数,表示当前被多少个 fd 关联。

14.4 分别写还是接续写

两个 fd 各自独立偏移量,所以默认是分别写(各从各的位置写,会互相覆盖)。测试:

/* 循环 4 次,交替写入 */
write(fd1, buffer1, 4);   /* 0x11 0x22 0x33 0x44 */
write(fd2, buffer2, 4);   /* 0xAA 0xBB 0xCC 0xDD */

因为两者偏移量独立、都从 0 开始,读取结果是 aabbccdd...(fd1 写的数据被 fd2 覆盖)。

想变成接续写,可用 O_APPENDO_APPEND 使每次 write 都自动到文件末尾,于是变成 11223344aabbccdd...


15. 复制文件描述符:dup 与 dup2

复制成功得到新的 fd,新旧 fd 都可操作文件,权限相同。复制得到的 fd 与旧 fd 指向同一个文件表,所以对文件的读写权限、状态标志、文件偏移量都相同——"复制"实则是复制文件表。使用完也要 close

15.1 dup

#include <unistd.h>

int dup(int oldfd);
参数 说明
oldfd 需要被复制的文件描述符

返回值:成功返回新的文件描述符(由系统按 fd 分配原则分配);失败返回 -1 并设置 errno

fd2 = dup(fd1);   /* fd1=6 -> 得到 fd2=7 */

由于新旧 fd 指向同一个文件表,偏移量共享,因此不用 O_APPEND 也能实现接续写

15.2 dup2

dup 不能自己指定 fd,dup2 修复了这个缺陷:

#include <unistd.h>

int dup2(int oldfd, int newfd);
参数 说明
oldfd 需要被复制的文件描述符
newfd 指定一个文件描述符(需是当前进程没有使用的)

返回值:成功返回手动指定的 newfd;失败返回 -1 并设置 errno

fd2 = dup2(fd1, 100);   /* 得到 fd2 == 100 */

同一个 fd 可以多次调用 dup/dup2 复制,得到多个不同的 fd。

完整 dup 测试源码(实现接续写):

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

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 指向同一个文件

三种常见实现方式:

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["同一个文件表<br/>共享偏移量"]
        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 刚写入的数据

    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,且把两步组成原子操作。

#include <unistd.h>

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);
参数 说明
fdbufcount read/write 意义相同
offset 当前读或写的位置偏移量

返回值:与 read/write 意义相同。

与「lseek + read/write」的区别:

  1. 调用 pread 时,无法中断其定位和读操作(即原子操作);
  2. 不更新文件表中的当前位置偏移量

验证第 2 点:pread(fd, buffer, 100, 1024) 后,lseek(fd, 0, SEEK_CUR) 返回 0 而不是 1124。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

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 执行一系列控制操作,类似"多功能文件描述符管理工具箱"。

#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );
参数 说明
fd 文件描述符
cmd 操作命令,以 F_XXX 开头
... 可变参,第三个参数根据 cmd 传入

cmd 大致分 5 类功能:

功能 cmd
复制文件描述符 F_DUPFDF_DUPFD_CLOEXEC
获取/设置文件描述符标志 F_GETFDF_SETFD
获取/设置文件状态标志 F_GETFLF_SETFL
获取/设置异步 IO 所有权 F_GETOWNF_SETOWN
获取/设置记录锁 F_GETLKF_SETLK

返回值:失败返回 -1 并设置 errno;成功时返回值与 cmd 有关(如 F_DUPFD 返回新 fd、F_GETFD 返回描述符标志、F_GETFL 返回文件状态标志)。

(1) 复制文件描述符

cmd = F_DUPFD:根据 fd 复制出新 fd,第三个参数指出新 fd 是大于或等于该参数的可用 fd;若该参数等于已存在的 fd,则取大于它的可用 fd。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

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_RDONLYO_WRONLYO_RDWR)不能被设置;
  • 文件创建标志O_CREATO_EXCLO_NOCTTYO_TRUNC)不能被设置,会被忽略;
  • Linux 下只有 O_APPENDO_ASYNCO_DIRECTO_NOATIMEO_NONBLOCK 这些标志可以被修改。

    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    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 相关信息)。

#include <sys/ioctl.h>

int ioctl(int fd, unsigned long request, ...);
参数 说明
fd 文件描述符
request 与具体要操作的对象有关,没有统一值,表示向 fd 请求相应操作
... 可变参,第三个参数根据 request 决定

返回值:成功返回 0,失败返回 -1。


19. 截断文件:truncate 与 ftruncate

使用 truncate()ftruncate() 可将普通文件截断为指定字节长度。

#include <unistd.h>
#include <sys/types.h>

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_WRONLYO_RDWR)。

注意:调用这两个函数不会改变文件读写位置偏移量,截断后一般需要重新设置读写位置,以免之前指向的位置已不存在而出错。

返回值:成功返回 0,失败返回 -1 并设置 errno

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

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);
}

运行前 file1file2 均为 592 字节;运行后 file1 变 0,file2 变 1024。


20. 实验步骤与调试方法

20.1 通用实验流程

# 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 -ldu
fd 泄漏 出错分支没 close lsof -p <pid> 观察,检查 goto 清理链
6、7、8 而不是 3、4、5 父进程(如 VSCode)占用了 3、4、5 直接在系统 Terminal 运行验证

常用工具:

man 2 open            # 系统调用手册
man 3 strerror        # 库函数手册
strace ./testApp      # 跟踪程序发起的系统调用与参数、返回值
lsof -p $(pgrep testApp)   # 查看进程打开的文件描述符
ls -i file            # 查看 inode 编号
stat file             # 查看文件元数据
du -h file            # 查看实际占用

strace 尤其适合验证本篇知识点:

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:openO_APPENDO_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:dupdup2 有什么区别?如何用它们在不用 O_APPEND 的情况下实现接续写?

dup(oldfd) 复制文件描述符,新 fd 由系统按 fd 分配原则分配,不能指定;dup2(oldfd, newfd) 可手动指定新 fd(须是当前进程未使用的),失败均返回 -1 并设置 errno。二者复制得到的 fd 与原 fd 指向同一个文件表,因此共享文件偏移量、状态标志和访问权限。所以只要 dup/dup2 出第二个 fd,两个 fd 交替 write 时就会接着对方的位置写,实现接续写,而无需 O_APPEND

Q5:什么是竞争冒险?open/pread 中分别用什么机制避免?preadlseek+read 有何区别?

:竞争冒险指多个进程/线程并发操作同一共享资源时,因获得 CPU 的先后顺序不可预期,导致结果不可预期。典型场景是两个进程分别"先 lseek 到文件末尾、再 write"做追加写,结果互相覆盖。解决思路是把多步操作合并为原子操作open 层面用 O_APPEND(把"定位末尾+写"合并),创建文件用 O_EXCL|O_CREAT(把"判断存在+创建"合并);读写层面用 pread/pwrite(把"定位+读写"合并)。pread/pwritelseek+read/write 的区别有两点:一是定位和读写不可中断、是原子操作;二是不更新文件表中记录的当前位置偏移量preadlseek(SEEK_CUR) 仍返回 0)。


内容来源:《I.MX6U嵌入式Linux C应用编程指南》第二章 文件I/O基础、第三章 深入探究文件I/O