Linux ——— 文件操作与缓冲机制的核心原理

目录

创建文件的当前路径

[一、chdir("/home/ranjiaju/test"); 函数解析](#一、chdir("/home/ranjiaju/test"); 函数解析)

[二、进程 cwd 的查看与验证:/proc/[pid]/cwd](#二、进程 cwd 的查看与验证:/proc/[pid]/cwd)

[三、无 chdir 时进程 cwd 的默认值](#三、无 chdir 时进程 cwd 的默认值)

[四、fopen("log.txt", "w") 与进程 cwd 的关联](#四、fopen("log.txt", "w") 与进程 cwd 的关联)

五、核心总结
[fopen 中以 "w" 的方式写入](#fopen 中以 "w" 的方式写入)

[一、fwrite(message, strlen(message), 1, fp); 函数解析](#一、fwrite(message, strlen(message), 1, fp); 函数解析)

[二、"w" 模式写入的核心特性:文件清空](#二、"w" 模式写入的核心特性:文件清空)

[三、Shell 命令与 "w" 模式的底层关联](#三、Shell 命令与 "w" 模式的底层关联)

四、核心总结
[fopen 中以 "a" 的方式写入](#fopen 中以 "a" 的方式写入)

[一、fopen 中以 "a" 方式写入的核心特性](#一、fopen 中以 "a" 方式写入的核心特性)

[二、Shell 重定向 >> 与 "a" 模式的关联](#二、Shell 重定向 >> 与 "a" 模式的关联)

三、核心总结
[系统调用 open](#系统调用 open)

[一、umask(0); 解释](#一、umask(0); 解释)

[二、系统调用 open 解释](#二、系统调用 open 解释)

[三、open 函数参数详解](#三、open 函数参数详解)

[四、O_WRONLY | O_CREAT | O_TRUNC 含义](#四、O_WRONLY | O_CREAT | O_TRUNC 含义)

[五、O_WRONLY | O_CREAT | O_APPEND 含义](#五、O_WRONLY | O_CREAT | O_APPEND 含义)

[六、open 函数 flags 参数宏定义及常用组合表](#六、open 函数 flags 参数宏定义及常用组合表)

[七、系统调用 write 解释](#七、系统调用 write 解释)

[八、close(fd); 解释](#八、close(fd); 解释)
[深度刨析 open 函数的返回值](#深度刨析 open 函数的返回值)

[一、open 函数的返回值](#一、open 函数的返回值)

二、"先描述,再组织":操作系统管理文件的核心思想

三、进程与文件描述符表的关联

[四、open 返回值与 write 函数参数的关联](#四、open 返回值与 write 函数参数的关联)

核心总结
重定向

一、底层原理

二、命令层面的重定向符号

三、系统调用层面的实现

总结
用户级缓冲区

一、用户级缓冲区的概念

二、缓冲区的三种刷新方式

三、代码解析

四、用户级缓冲区的意义

五、核心总结
全缓冲与fork

[一、重定向 > log.txt 的底层原理](#一、重定向 > log.txt 的底层原理)

二、程序执行流程与缓冲区行为

三、进程结束与缓冲区刷新

四、最终结果分析

核心总结


创建文件的当前路径

复制代码
[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ pwd
/home/ranjiaju/test/learning-linux/myfile

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ ll
total 8
-rw-rw-r-- 1 ranjiaju ranjiaju  84 Dec 29 15:34 makefile
-rw-rw-r-- 1 ranjiaju ranjiaju 309 Dec 29 15:45 myproc.c

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ cat myproc.c 
#include<stdio.h>
#include<string.h>
#include<unistd.h>
int main()
{
    chdir("/home/ranjiaju/test");
   
    printf("pid: %d\n", getpid());

    FILE* fp = fopen("log.txt", "w");
    if(fp == NULL)
    {
        perror("fopen");
        return -1;
    }

    fclose(fp);

    sleep(1000);

    return 0;
}

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ make
gcc myproc.c -o myproc -std=c99

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ ./myproc 
pid: 2250

// 同时查看 2250 进程中的 cwd,确实已经被 chdir 更改
[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ ll /proc/2250/cwd
lrwxrwxrwx 1 ranjiaju ranjiaju 0 Dec 29 15:44 /proc/2250/cwd -> /home/ranjiaju/test

// 同时查看 test 目录下的文件
[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ ll /home/ranjiaju/test
total 4
drwxrwxr-x 12 ranjiaju ranjiaju 4096 Dec 29 15:33 learning-linux
-rw-rw-r--  1 ranjiaju ranjiaju    0 Dec 29 15:50 log.txt

一、chdir("/home/ranjiaju/test"); 函数解析

chdir(Change Directory)是 Linux 中用于修改当前进程工作目录 的系统调用,其核心作用是改变进程对 "当前路径" 的认知,影响后续所有相对路径的文件操作。下面从函数原型、执行逻辑、与 cd 命令的区别三个维度详细解析:

1. 函数原型与参数

复制代码
#include <unistd.h>
int chdir(const char *path);
  • 参数 path :目标目录的路径(绝对路径或相对路径均可),示例中使用绝对路径 /home/ranjiaju/test,确保无论进程当前在哪个目录,都能准确切换到目标目录;
  • 返回值 :成功返回 0,失败返回 -1 并设置 errno(如路径不存在、权限不足等)。

2. chdir 的执行逻辑

示例中进程执行 chdir("/home/ranjiaju/test") 后,内核会完成以下操作:

  1. 更新进程的 cwd 属性 :内核在进程的 PCB(进程控制块)中维护一个名为 cwd(Current Working Directory,当前工作目录)的属性,chdir 会将该属性的值从进程启动时的目录(/home/ranjiaju/test/learning-linux/myfile)修改为 /home/ranjiaju/test
  2. 不影响父进程chdir 仅修改当前进程的 cwd,不会影响启动它的父进程(如 bash 终端),这与 cd 命令的行为一致(cd 是 shell 内置命令,若为外部命令则无法改变 shell 的目录)。

3. 与 cd 命令的本质区别

特性 chdir 函数 cd 命令
作用对象 当前进程(如示例中的 myproc shell 进程本身(如 bash)
作用范围 仅当前进程及其子进程(若有) 仅当前 shell 会话
实现方式 系统调用,直接修改内核中进程的 cwd 属性 shell 内置命令,本质是 shell 进程调用 chdir
对父进程影响 无(子进程修改 cwd 不影响父进程) 无(shell 是独立进程,修改自身 cwd 不影响其父进程)

二、进程 cwd 的查看与验证:/proc/[pid]/cwd

Linux 提供了 **/proc 文件系统 **(虚拟文件系统,存储内核和进程的实时信息),通过该文件系统可直接查看任意进程的 cwd。示例中通过以下命令验证了 chdir 的效果:

复制代码
ll /proc/2250/cwd
  • /proc/2250 :对应 PID 为 2250 的进程的信息目录,每个进程在 /proc 下都有一个以其 PID 命名的子目录;
  • cwd :是一个符号链接 ,指向该进程当前的工作目录。示例中输出 lrwxrwxrwx 1 ranjiaju ranjiaju 0 Dec 29 15:44 /proc/2250/cwd -> /home/ranjiaju/test,明确显示进程 2250 的 cwd 已被 chdir 修改为 /home/ranjiaju/test

三、无 chdir 时进程 cwd 的默认值

若示例中删除 chdir("/home/ranjiaju/test"); 这行代码,进程 2250 的 cwd 会默认继承自其父进程(bash 终端)。具体规则如下:

  1. 进程启动时的 cwd 继承 :当通过 ./myproc 启动进程时,新进程的 cwd完全继承父进程(bash)的 cwd 。示例中执行 ./myproc 时,bash 终端的当前目录是 /home/ranjiaju/test/learning-linux/myfile,因此进程 2250 的默认 cwd 就是该目录;
  2. 验证方式 :若删除 chdir,执行 ll /proc/2250/cwd 会输出 -> /home/ranjiaju/test/learning-linux/myfile,与 bash 终端的当前目录一致。

四、fopen("log.txt", "w") 与进程 cwd 的关联

fopen 打开文件时,若使用相对路径 (如 log.txt),其查找文件的基准路径是当前进程的 cwd,而非程序所在的目录或其他路径。示例中的执行逻辑清晰地证明了这一点:

  1. chdir 修改 cwd :进程 2250 的 cwd/home/ranjiaju/testfopen("log.txt", "w") 会在该目录下查找 log.txt;若文件不存在,则创建新文件(示例中 ll /home/ranjiaju/test 显示 log.txt 已被创建);若文件存在,则清空文件内容并以写模式打开;
  2. chdir :进程 2250 的 cwd/home/ranjiaju/test/learning-linux/myfilefopen("log.txt", "w") 会在该目录下创建或打开 log.txt

关键补充:相对路径 vs 绝对路径

  • 相对路径 :以当前进程的 cwd 为基准进行路径解析,如 log.txt../test.log 等;
  • 绝对路径 :以根目录 / 为基准进行路径解析,不受进程 cwd 的影响,如 /home/ranjiaju/test/log.txt

五、核心总结

  1. chdir 的核心作用 :修改当前进程的 cwd(当前工作目录),影响后续所有相对路径的文件操作,且仅对当前进程有效;
  2. 进程 cwd 的默认值 :进程启动时默认继承父进程的 cwd,可通过 chdir 主动修改;
  3. fopencwd 的关系 :使用相对路径时,fopen 以进程的 cwd 为基准查找文件;使用绝对路径时,不受 cwd 影响;
  4. cwd 的查看方式 :通过 /proc/[pid]/cwd 符号链接可实时查看任意进程的当前工作目录。

这一机制体现了 Linux 进程设计的灵活性:进程可通过 chdir 自主管理其文件操作的基准路径,使得相对路径的使用更加灵活和可预测。


fopen 中以 "w" 的方式写入

复制代码
[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ ll
total 8
-rw-rw-r-- 1 ranjiaju ranjiaju  84 Dec 29 15:34 makefile
-rw-rw-r-- 1 ranjiaju ranjiaju 344 Dec 29 16:17 myproc.c

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ cat myproc.c 
#include<stdio.h>
#include<string.h>
#include<unistd.h>

int main()
{
    printf("pid: %d\n", getpid());

    FILE* fp = fopen("log.txt", "w");
    if(fp == NULL)
    {
        perror("fopen");
        return -1;
    }
    
    const char* message = "hello Linux";

    fwrite(message, strlen(message), 1, fp);

    fclose(fp);
    return 0;
}

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ make
gcc myproc.c -o myproc -std=c99

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ ./myproc 
pid: 2781

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ ll
total 24
-rw-rw-r-- 1 ranjiaju ranjiaju   11 Dec 29 16:19 log.txt
-rw-rw-r-- 1 ranjiaju ranjiaju   84 Dec 29 15:34 makefile
-rwxrwxr-x 1 ranjiaju ranjiaju 8752 Dec 29 16:19 myproc
-rw-rw-r-- 1 ranjiaju ranjiaju  344 Dec 29 16:17 myproc.c

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ cat log.txt 
hello Linux[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ 

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ echo "abcd" 
abcd

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ echo "abcd" > log.txt 

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ cat log.txt 
abcd

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ >log.txt 
[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ cat log.txt 
[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ 

一、fwrite(message, strlen(message), 1, fp); 函数解析

fwrite 是 C 语言标准库中用于二进制数据写入文件的函数,其核心作用是将指定内存区域的数据按块写入文件流。下面结合函数原型和示例代码,详细拆解每个参数的意义及执行逻辑:

1. 函数原型

复制代码
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

2. 参数详解(对应示例中的调用)

参数 示例中的值 核心意义
ptr message 待写入数据的内存起始地址 。示例中 message 是字符串 "hello Linux" 的首地址,fwrite 从该地址开始读取数据。
size strlen(message) 每个数据块的字节大小 。示例中 strlen(message) 计算得字符串长度为 11(不含末尾的 \0),因此每个数据块大小为 11 字节。
nmemb 1 要写入的数据块数量。示例中为 1,表示只写入 1 个大小为 11 字节的数据块。
stream fp 目标文件的文件指针 ,指向已通过 fopen 打开的文件流(示例中为 log.txt)。

3. 执行逻辑与结果

示例中 fwrite 的实际行为是:

  • message 指向的内存地址读取 1 个数据块(每个块 11 字节),即完整读取字符串 "hello Linux"(不含 \0,因为 strlen 不计算结束符);
  • 将这 11 字节数据写入 fp 对应的 log.txt 文件;
  • 函数返回值为成功写入的数据块数量(示例中为 1),若返回值小于 nmemb,则表示写入异常(如磁盘空间不足)。

二、"w" 模式写入的核心特性:文件清空

C 语言中 fopen 函数的 "w"(write,写入模式)是文件操作的基础模式之一,其核心行为是:

  • 文件存在时 :打开文件并清空所有原有内容(文件大小变为 0),后续写入从文件开头开始;
  • 文件不存在时 :创建新文件,权限默认与系统相关(示例中为 rw-rw-r--)。

这种 "先清空再写入" 的特性,是导致示例中文件内容被覆盖的根本原因。

三、Shell 命令与 "w" 模式的底层关联

用户观察到的 Shell 命令行为(如 echo "abcd" > log.txt>log.txt),其底层原理与 C 语言的 "w" 模式完全一致:

1. echo "abcd" > log.txt:重定向写入的本质

  • > 的作用 :Shell 中的 >输出重定向运算符 ,其底层行为等价于:
    1. "w" 模式打开目标文件 log.txt(存在则清空,不存在则创建);
    2. 将命令的标准输出(示例中为 "abcd\n")写入该文件;
    3. 关闭文件。
  • 结果log.txt 原有内容 "hello Linux" 被清空,替换为新内容 "abcd"

2. >log.txt:仅清空文件的特殊重定向

  • > 单独使用 :当 > 后无命令输出时(如 >log.txt),其行为简化为:
    1. "w" 模式打开 log.txt(存在则清空,不存在则创建);
    2. 无任何数据写入(因为没有命令输出);
    3. 立即关闭文件。
  • 结果 :文件仅被清空(大小变为 0),无新内容写入,与 C 语言中 fopen("log.txt", "w") 后直接 fclose(fp) 的效果完全一致。

四、核心总结

  1. fwrite 的核心逻辑 :按 "数据块大小 × 块数量" 写入文件,示例中通过 strlen 精准控制了写入的字符串长度(不含 \0)。
  2. "w" 模式的本质:打开文件时强制清空原有内容,是 "覆盖写入" 的底层实现。
  3. Shell 重定向与 "w" 模式的一致性>>log.txt 均通过 "以 w 模式打开文件" 实现内容覆盖或清空,与 C 语言的文件操作原理相通。

这种设计保证了文件写入的可预测性:无论文件是否存在,"w" 模式和 Shell 重定向都能确保从 "干净的文件开头" 开始写入,避免旧数据干扰。


fopen 中以 "a" 的方式写入

复制代码
[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ ll
total 8
-rw-rw-r-- 1 ranjiaju ranjiaju  92 Dec 29 16:32 makefile
-rw-rw-r-- 1 ranjiaju ranjiaju 346 Dec 29 16:32 myproc.c

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ cat myproc.c 
#include<stdio.h>
#include<string.h>
#include<unistd.h>

int main()
{
    printf("pid: %d\n", getpid());

    FILE* fp = fopen("log.txt", "a");
    if(fp == NULL)
    {
        perror("fopen");
        return -1;
    }
    
    const char* message = "hello Linux\n";

    fwrite(message, strlen(message), 1, fp);

    fclose(fp);
    return 0;
}

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ make
gcc myproc.c -o myproc -std=c99

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ ./myproc 
pid: 3052

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ cat log.txt 
hello Linux

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ ./myproc 
pid: 3054

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ cat log.txt 
hello Linux
hello Linux

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ echo "hello linux" >> log.txt 

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ cat log.txt 
hello Linux
hello Linux
hello linux

一、fopen 中以 "a" 方式写入的核心特性

在 C 语言中,fopen 函数的 "a"(append,追加模式)是文件写入的另一种基础模式,其核心行为与 "w"(写入模式)形成鲜明对比:

  • 文件存在时 :打开文件但不清空原有内容 ,而是将文件读写指针移动到文件末尾,后续所有写入操作都从文件末尾开始,实现 "追加" 效果;
  • 文件不存在时 :与 "w" 模式相同,会创建新文件。

这种 "不清空、只追加" 的特性,是示例中多次执行 ./myproc 后文件内容不断累加的根本原因。

二、Shell 重定向 >>"a" 模式的关联

我们观察到的 echo "hello linux" >> log.txt 行为,其底层原理与 C 语言的 "a" 模式完全一致:

  • >> 的作用 :Shell 中的 >>追加输出重定向运算符 ,其底层行为等价于:
    1. "a" 模式打开目标文件 log.txt(存在则保留内容并将指针移至末尾,不存在则创建);
    2. 将命令的标准输出(示例中为 "hello linux\n")写入文件末尾;
    3. 关闭文件。
  • 结果 :新内容 "hello linux" 被追加到 log.txt 已有内容之后,而非覆盖原有内容。

三、核心总结

  1. "a" 模式的本质:打开文件时不清空内容,仅将读写指针移至末尾,实现追加写入;
  2. >>"a" 的关系 :Shell 的 >> 重定向底层通过调用 "a" 模式实现,两者行为完全一致,均为 "追加写入"。

这一机制体现了文件操作的分层设计:Shell 命令的重定向功能是对底层 C 语言文件操作模式(如 "a" 模式)的封装和简化。


系统调用 open

复制代码
[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ ll
total 8
-rw-rw-r-- 1 ranjiaju ranjiaju  92 Dec 29 16:32 makefile
-rw-rw-r-- 1 ranjiaju ranjiaju 797 Dec 29 20:46 myproc.c

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ cat myproc.c 
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

int main()
{
    umask(0);

    // int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
    int fd = open("log.txt", O_WRONLY|O_CREAT|O_APPEND, 0666);
    if(fd < 0)
    {
        printf("open file error\n");
        return -1;
    }

    const char* message = "hell open";
    write(fd, message, strlen(message));

    close(fd);

    return 0;
}

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ make
gcc myproc.c -o myproc -std=c99

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ ./myproc 

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ ll
total 24
-rw-rw-rw- 1 ranjiaju ranjiaju    9 Dec 29 20:47 log.txt
-rw-rw-r-- 1 ranjiaju ranjiaju   92 Dec 29 16:32 makefile
-rwxrwxr-x 1 ranjiaju ranjiaju 8696 Dec 29 20:47 myproc
-rw-rw-r-- 1 ranjiaju ranjiaju  797 Dec 29 20:46 myproc.c

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ cat log.txt 
hell open[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ 

一、umask(0); 解释

umask (User File-creation Mask) 是一个进程级别的权限掩码 。其核心作用是:在创建新文件或目录时,从请求的权限中 "屏蔽" 掉某些权限位

工作原理:

  1. 默认行为 :每个进程都有一个默认的 umask 值(通常是 00020022)。这个值决定了新创建文件的默认权限。
  2. 计算方式 :新文件的最终权限 = open 函数中请求的权限 - umask 的值。
    • 注意 :这里的 "减" 不是数学减法,而是按位取反后再按位与的操作。
    • 公式为:最终权限 = (请求的权限) & (~umask)

代码中的 umask(0)

  • umask(0) 的作用是将当前进程的权限掩码设置为 0
  • 这意味着不屏蔽任何权限 。因此,新文件的最终权限将完全等于 open 函数中 mode 参数所请求的权限。
  • 在代码中,因为 umask 被设置为 0,所以 log.txt 的最终权限就是 0666

二、系统调用 open 解释

open 是一个非常基础且强大的 Linux 系统调用 ,用于打开或创建一个文件,并返回一个文件描述符(File Descriptor, fd),后续的文件读写等操作都通过这个 fd 来进行。

其函数原型如下:

复制代码
int open(const char *pathname, int flags, mode_t mode);

三、open 函数参数详解

1. pathname (路径名)

  • 示例值 : "log.txt"
  • 含义 : 要打开或创建的文件的路径名。可以是相对路径(如 log.txt)或绝对路径(如 /home/user/log.txt)。

2. flags (标志位)

这是 open 函数最灵活的部分,它通过位掩码 (bitmask)的方式,使用 | (按位或) 操作符来组合多个选项。flags 大致可以分为以下几类:

访问模式 (Access Mode) - 必须指定一个

  • O_RDONLY: 以只读方式打开文件。
  • O_WRONLY: 以只写方式打开文件。
  • O_RDWR: 以可读可写方式打开文件。

创建和截断模式 (Creation and Truncation Flags)

  • O_CREAT: 如果文件不存在 ,则创建 它。如果这个标志被使用,open 函数必须提供第三个参数 mode 来指定新文件的权限。
  • O_TRUNC: 如果文件已存在 并且是以写方式打开的,则清空 文件内容(将文件大小截断为 0)。这类似于 fopen 中的 "w" 模式。
  • O_APPEND: 以追加 方式打开文件。每次写入数据时,都会将数据添加到文件末尾。这类似于 fopen 中的 "a" 模式。

其他常用模式

  • O_NONBLOCK: 以非阻塞模式打开文件。对于普通文件,这个标志通常没有效果,但对于管道、套接字等特殊文件非常重要。

3. mode (权限)

  • 示例值 : 0666
  • 含义 : 当 flags 中包含 O_CREAT 时,此参数用于设置新创建文件的权限。它是一个八进制数。

四、O_WRONLY | O_CREAT | O_TRUNC 含义

这是 flags 参数的一个组合,其含义是:

  • O_WRONLY : 程序想要以只写的方式操作这个文件。
  • O_CREAT : 在打开之前,系统会检查文件是否存在。如果不存在,就创建一个新文件
  • O_TRUNC : 如果文件已存在 ,则在打开的同时清空其所有内容

所以,O_WRONLY | O_CREAT | O_TRUNC 的完整意思是:"请打开这个文件用于写入。如果文件不存在,请先创建它。如果文件已存在,请先清空它的内容。" 这完全等同于 C 标准库中的 fopen(path, "w")

五、O_WRONLY | O_CREAT | O_APPEND 含义

这是 flags 参数的另一个组合,其含义是:

  • O_WRONLY : 程序想要以只写的方式操作这个文件。
  • O_CREAT : 在打开之前,系统会检查文件是否存在。如果不存在,就创建一个新文件
  • O_APPEND : 如果文件已存在 ,则在打开的同时,将文件的读写指针移动到文件末尾

所以,O_WRONLY | O_CREAT | O_APPEND 的完整意思是:"请打开这个文件用于写入。如果文件不存在,请先创建它。如果文件已存在,请将写入位置设置在文件末尾。" 这完全等同于 C 标准库中的 fopen(path, "a")

六、open 函数 flags 参数宏定义及常用组合表

宏定义 (Macro) 含义 (Meaning) 类别 (Category)
O_RDONLY 只读方式打开 访问模式
O_WRONLY 只写方式打开 访问模式
O_RDWR 读写方式打开 访问模式
O_CREAT 如果文件不存在则创建 创建 / 状态标志
O_TRUNC 如果文件存在且为写模式,则清空文件 创建 / 状态标志
O_APPEND 每次写入前,将文件指针移至末尾 创建 / 状态标志
O_NONBLOCK 非阻塞模式打开 其他
常用组合 (Common Combinations) 含义 (Meaning) 等效 fopen 模式
`O_WRONLY O_CREAT O_TRUNC`
`O_WRONLY O_CREAT O_APPEND`
`O_RDWR O_CREAT O_TRUNC`
`O_RDWR O_CREAT O_APPEND`
O_RDWR 读写,文件必须已存在 "r+"

七、系统调用 write 解释

write 是用于将数据从内存写入已打开文件的系统调用。

其函数原型如下:

复制代码
ssize_t write(int fd, const void *buf, size_t count);
  • fd : open 函数返回的文件描述符,用于标识要写入的文件。
  • buf : 指向要写入数据的内存缓冲区的指针。
  • count : 要写入的字节数

返回值:

  • 成功: 返回实际写入的字节数。
  • 失败 : 返回 -1,并设置 errno

在代码中,write(fd, message, strlen(message)); 的作用是将 message 指针指向的字符串(长度为 strlen(message))写入到文件描述符 fd 所代表的文件中。

八、close(fd); 解释

close 是用于关闭一个已打开文件的系统调用。

其函数原型如下:

复制代码
int close(int fd);
  • fd : 要关闭的文件的文件描述符

核心作用:

  1. 释放资源: 通知操作系统,程序不再使用该文件,可以回收与之相关的内核资源(如文件表项、内存等)。
  2. 保证数据完整性: 关闭文件时,操作系统会确保所有在缓冲区中的数据都被刷新(flush)到物理磁盘上。
  3. 避免资源泄漏 : 每个进程能打开的文件数量是有限的。如果不关闭文件,文件描述符会一直被占用,最终导致程序无法打开新文件,这被称为文件描述符泄漏

在代码中,close(fd); 是一个必不可少的步骤,它确保了文件被正确关闭,所有写入的数据都已安全保存,并且文件描述符被释放以供他用。


深度刨析 open 函数的返回值

一、open 函数的返回值

open 函数的返回值是一个 int 类型的整数,称为文件描述符(File Descriptor, fd)。它是内核为进程分配的、用于唯一标识一个已打开文件的索引。

  • 成功时:返回一个非负整数(通常是从 3 开始的小整数,0、1、2 分别被标准输入、标准输出、标准错误占用)。
  • 失败时 :返回 -1,并通过 errno 变量存储具体的错误原因(如文件不存在、权限不足等)。

文件描述符是进程与内核之间操作文件的 "凭证",后续的 readwriteclose 等系统调用都需要通过它来指定操作的目标文件。

二、"先描述,再组织":操作系统管理文件的核心思想

操作系统管理任何资源(包括文件)的逻辑遵循 "先描述,再组织" 的原则:

  1. 描述 :当一个文件被打开时,内核会创建一个 **struct file 结构体 **,用于存储该文件的所有相关信息,相当于文件在内存中的 "身份证"。

    • 结构体中包含的信息示例:
      • 文件的路径、状态(如是否可读 / 可写);
      • 文件的当前读写偏移量(f_pos);
      • 指向文件索引节点(struct inode)的指针(inode 存储文件的元数据,如大小、权限、磁盘位置等);
      • 用于链接其他 struct file 的指针(prevnext)。
  2. 组织 :当一个进程打开多个文件时,内核会将这些文件对应的 struct file 结构体通过双链表的形式组织起来。

    • 每个 struct file 结构体中包含 struct file* prevstruct file* next 指针,分别指向链表中的前一个和后一个文件结构体;
    • 这种组织方式使得内核可以高效地遍历、查找和管理进程打开的所有文件。

三、进程与文件描述符表的关联

进程的内核数据结构 task_struct(进程控制块)中包含一个 struct files_struct* files 指针,该指针指向一个名为 files_struct 的结构体,其核心作用是管理进程打开的所有文件。

files_struct 结构体中包含一个关键的数组 ------文件描述符表,定义类似:

复制代码
struct files_struct {
    // ... 其他成员 ...
    struct file* fd_array[];  // 文件描述符表,存储指向 struct file 的指针
};
  • 数组下标 :即文件描述符(fd),是 open 函数的返回值;
  • 数组元素 :是指向对应 struct file 结构体的指针。

例如,当进程调用 open("log.txt", ...) 成功后,内核会:

  1. 创建一个 struct file 结构体描述 log.txt
  2. 将该结构体的地址存入 fd_array 数组的某个空闲下标(如 3);
  3. 返回下标 3 作为文件描述符。

四、open 返回值与 write 函数参数的关联

write 函数的第一个参数要求传入文件描述符,其本质是让进程通过该下标在 fd_array 数组中找到对应的 struct file 指针,进而定位到要写入的文件。

流程示例 :当执行 write(fd, message, strlen(message)) 时:

  1. 进程通过 task_struct 找到 files_struct 指针;
  2. fd 为下标,在 fd_array 数组中获取指向 log.txtstruct file 指针;
  3. 内核根据该结构体中的信息(如文件偏移量、inode 指针),将数据写入文件的正确位置。

核心总结

  1. open 的返回值 :是文件描述符(fd),即文件描述符表 fd_array 的下标;
  2. 内核管理逻辑 :通过 struct file 结构体描述文件,用双链表组织多个文件,再通过 files_struct 将文件与进程关联;
  3. write 的参数意义 :通过 fd 作为下标,在文件描述符表中找到目标文件的 struct file 指针,从而完成写入操作。

这种 "描述 - 组织 - 索引" 的设计,使得操作系统能够高效、安全地管理进程与文件之间的交互。


重定向

Linux 重定向 的本质是修改进程文件描述符的指向,通过改变数据的输入输出目标,实现命令或程序的输入输出流重定向。以下从底层原理、命令语法、系统调用三个维度展开解释。

一、底层原理

Linux 系统遵循一切皆文件的设计哲学,进程启动时会默认打开三个标准文件描述符(File Descriptor,FD),每个文件描述符对应内核中的文件描述符表项,表项指向系统级的文件表(包含文件状态、偏移量等),最终关联到物理文件或设备。

  • 标准输入 (stdin) :文件描述符为0,默认指向终端输入设备(/dev/tty),负责接收进程的输入数据。
  • 标准输出 (stdout) :文件描述符为1,默认指向终端输出设备(/dev/tty),负责输出进程的正常执行结果。
  • 标准错误 (stderr) :文件描述符为2,默认指向终端输出设备(/dev/tty),负责输出进程的错误信息。

重定向的底层逻辑是修改进程文件描述符表中特定 FD 的指向 ,将原本指向终端设备的 FD,改为指向普通文件、管道、设备文件等其他文件对象。例如执行ls > file.txt时,shell 会先打开 file.txt 文件,然后将当前进程的 1 号 FD(stdout)的指向从终端设备替换为 file.txt,后续ls命令的输出数据会通过 1 号 FD 写入 file.txt,而非终端。

二、命令层面的重定向符号

shell 提供了多种重定向符号,用于控制不同类型的数据流方向,核心符号及功能如下:

符号 功能说明 对应文件描述符 示例
> 标准输出覆盖重定向,目标文件存在则清空 1>(可省略 1) ls > file.txt
>> 标准输出追加重定向,目标文件存在则追加内容 1>>(可省略 1) echo "test" >> file.txt
< 标准输入重定向,从文件读取输入数据 0<(可省略 0) cat < file.txt
2> 标准错误覆盖重定向,单独重定向错误信息 2 gcc test.c 2> error.log
2>> 标准错误追加重定向 2 ./a.out 2>> error.log
&> 标准输出 + 标准错误同时覆盖重定向 1+2 ./a.out &> all.log
2>&1 将标准错误重定向到标准输出的当前目标 2→1 ./a.out > all.log 2>&1

关键注意点:2>&1 的顺序不可颠倒 ,必须先执行> all.log将 1 号 FD 指向 all.log,再执行2>&1将 2 号 FD 的指向同步到 1 号 FD 的目标,否则会导致错误重定向失效。

三、系统调用层面的实现

shell 的重定向功能,底层依赖 Linux 提供的dup()dup2() 两个核心系统调用,用于复制文件描述符,从而修改 FD 的指向。

  1. 核心函数定义

    复制代码
    #include <unistd.h>
    
    // 复制oldfd,返回当前最小可用的新文件描述符
    int dup(int oldfd);
    
    // 将oldfd复制到newfd,若newfd已打开则先关闭,复制成功后oldfd和newfd指向同一文件
    int dup2(int oldfd, int newfd);
  2. 实现逻辑(以 stdout 重定向为例) 执行command > file.txt时,shell 内部执行步骤为:

    • 调用open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644) 打开文件,得到一个新的文件描述符(假设为 3)。
    • 调用dup2(3, 1),将 1 号 FD(stdout)的指向替换为 3 号 FD 对应的 file.txt,此时 1 号和 3 号 FD 指向同一文件。
    • 调用close(3) 关闭多余的 3 号 FD,避免文件描述符泄露。
    • 调用execve() 执行commandcommand的所有 stdout 输出(write (1, ...))都会写入 file.txt。
  3. 代码示例(C 语言实现 stdout 重定向)

    复制代码
    #include <stdio.h>
    #include <unistd.h>
    #include <fcntl.h>
    
    int main() 
    {
        // 打开文件,模式为写入、当文件不存在时创建此文件、每次进入文件清空文件内容
        int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
        
        if (fd == -1) 
        {
            perror("open failed");
            return 1;
        }
        
        // 将stdout(1号FD)重定向到fd对应的文件
        dup2(fd, 1);
        
        close(fd); // 关闭多余的文件描述符
        
        // printf默认输出到stdout,此时会写入output.txt
        printf("This is redirected output\n");
        
        return 0;
    }

    编译运行结果:

    复制代码
    [ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ ll
    total 8
    -rw-rw-r-- 1 ranjiaju ranjiaju   92 Dec 29 16:32 makefile
    -rw-rw-r-- 1 ranjiaju ranjiaju 1376 Dec 30 14:10 myproc.c
    
    [ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ cat myproc.c 
    #include<stdio.h>
    #include<string.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/stat.h>
    #include<fcntl.h>
    int main() 
    {
        //  打开文件,模式为写入、当文件不存在时创建此文件、每次进入文件清空文件内容
        int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
        if (fd == -1) 
        {
            perror("open failed");
            return 1;
        }
    
        // 将stdout(1号FD)重定向到fd对应的文件
        dup2(fd, 1);
        
        close(fd); // 关闭多余的文件描述符
        
        // printf默认输出到stdout,此时会写入output.txt
        printf("This is redirected output\n");
        return 0;
    }
    
    [ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ make
    gcc myproc.c -o myproc -std=c99
    
    [ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ ./myproc 
    
    [ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ ll
    total 24
    -rw-rw-r-- 1 ranjiaju ranjiaju   92 Dec 29 16:32 makefile
    -rwxrwxr-x 1 ranjiaju ranjiaju 8648 Dec 30 14:10 myproc
    -rw-rw-r-- 1 ranjiaju ranjiaju 1376 Dec 30 14:10 myproc.c
    -rw-r--r-- 1 ranjiaju ranjiaju   26 Dec 30 14:11 output.txt
    
    [ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ myfile]$ cat output.txt 
    This is redirected output

总结

  1. 底层原理 进程默认打开 3 个文件描述符:0(标准输入 stdin)、1(标准输出 stdout)、2(标准错误 stderr),默认均指向终端。重定向通过改变这三类 FD 的指向,让数据读写目标切换为普通文件、设备等。
  2. 命令符号 核心符号包括覆盖输出>、追加输出>>、输入重定向<、错误重定向2>,以及合并输出2>&1/&>。需注意 **2>&1必须放在标准输出重定向之后 **,否则失效。
  3. 系统调用实现 依赖dup()/dup2() 系统调用,流程为:open 目标文件获取新 FD → dup2 将新 FD 复制到目标编号(如 1)→ close 多余 FD → execve 执行命令,完成 FD 指向替换。

用户级缓冲区

复制代码
[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ Buffer]$ ll
total 8
-rw-rw-r-- 1 ranjiaju ranjiaju  92 Jan  2 13:33 makefile
-rw-rw-r-- 1 ranjiaju ranjiaju 409 Jan  2 13:33 myproc.c

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ Buffer]$ cat myproc.c 
#include<stdio.h>
#include<string.h>
#include<unistd.h>

int main()
{
    const char* fstr = "hello fwrite";
    const char* str = "hello write";

    // c语言接口
    printf("hello printf"); //stdout->1
    fprintf(stdout, "hello fprintf"); //stdout->1
    fwrite(fstr, strlen(fstr), 1, stdout); //stdout->1
    
    // 操作系统接口
    write(1, str, strlen(str));

    close(1);

    return 0;
}

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ Buffer]$ make
gcc myproc.c -o myproc -std=c99

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ Buffer]$ ./myproc 
hello write[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ Buffer]$ 

一、用户级缓冲区的概念

用户级缓冲区是 C 语言标准库在用户空间 为每个文件流(如stdout)维护的一块内存区域。其核心作用是:将多次小规模的 I/O 操作合并为一次大规模的系统调用,减少 CPU 在内核态与用户态之间的切换开销,从而提升程序执行效率。

用户级缓冲区与内核缓冲区的关系:

  • 用户级缓冲区 :位于用户空间,由 C 库管理,程序可通过fflush等函数主动控制刷新;
  • 内核缓冲区:位于内核空间,由操作系统管理,用于暂存数据并优化磁盘 I/O(如延迟写入以合并多次磁盘操作)。

C 语言 I/O 接口(如printf)会先将数据写入用户级缓冲区,满足刷新条件后再通过系统调用(如write)将数据从用户级缓冲区拷贝到内核缓冲区,最终由操作系统写入物理设备(如显示器)。

二、缓冲区的三种刷新方式

C 语言标准库根据文件流的类型,将用户级缓冲区分为三种刷新策略:

刷新方式 触发条件 适用场景 示例
无缓冲 数据写入后立即刷新,直接调用系统调用 标准错误流stderr(需立即显示错误信息) fprintf(stderr, "error");(立即输出)
行缓冲 缓冲区满或遇到换行符\n时刷新 标准输出流stdout(终端输出场景) printf("hello\n");(遇\n刷新)
全缓冲 缓冲区满或调用fflush时刷新 文件流(如fopen打开的普通文件) fprintf(fp, "data");(缓冲区满后刷新)

三、代码解析

代码中同时使用了 C 语言 I/O 接口和操作系统接口,通过对比可清晰体现用户级缓冲区的作用:

1. C 语言 I/O 接口(用户级缓冲区)

复制代码
printf("hello printf");          // 行缓冲,无`\n`,不刷新
fprintf(stdout, "hello fprintf"); // 行缓冲,无`\n`,不刷新
fwrite(fstr, strlen(fstr), 1, stdout); // 行缓冲,无`\n`,不刷新
  • 行缓冲特性stdout默认是行缓冲,上述代码均未以\n结尾,因此数据仅写入用户级缓冲区,未触发刷新;
  • 进程结束时的刷新 :若程序正常结束(无close(1)),C 库会在main函数返回前自动刷新所有用户级缓冲区,此时所有数据会通过系统调用写入内核并显示到终端。

2. 操作系统接口(无用户级缓冲区)

复制代码
write(1, str, strlen(str)); // 直接调用系统调用,无用户级缓冲区
  • 无缓冲特性write是操作系统提供的系统调用,直接将数据从用户空间拷贝到内核缓冲区,不经过 C 库的用户级缓冲区;
  • 即时输出:数据写入内核缓冲区后,由操作系统根据自身策略(如终端设备通常会立即刷新)显示到终端,因此 "hello write" 会立即输出。

3. close(1)的影响

复制代码
close(1); // 关闭标准输出文件描述符(1号文件)
  • 文件描述符:0、1、2 分别对应标准输入、标准输出、标准错误,是内核为进程默认分配的文件描述符;
  • 内核视角close是系统调用,仅操作内核中的文件描述符表,无法感知用户级缓冲区的存在
  • 数据丢失 :当close(1)执行时,内核会关闭标准输出对应的文件。此时,C 语言接口写入用户级缓冲区的数据尚未刷新到内核,由于文件已关闭,后续即使进程结束,C 库也无法通过系统调用将数据写入内核,导致数据丢失。

四、用户级缓冲区的意义

  1. 提高效率

    • 若每次printf都直接调用write,会导致频繁的系统调用(CPU 从用户态切换到内核态),开销较大;
    • 用户级缓冲区将多次小数据写入合并为一次系统调用,显著减少切换次数,提升程序执行效率。
  2. 支持格式化输出

    • C 语言 I/O 接口(如printf)提供丰富的格式化功能(如%d%s),这些功能需要在用户空间完成数据拼接和格式转换;
    • 用户级缓冲区为格式化操作提供了暂存空间,待数据格式处理完成后再统一写入内核。

五、核心总结

  1. 用户级缓冲区:C 库在用户空间维护的内存区域,用于合并 I/O 操作以提升效率;
  2. 刷新方式 :行缓冲(stdout)需\n或缓冲区满时刷新,全缓冲(文件)需缓冲区满或fflush刷新,无缓冲(stderr)立即刷新;
  3. close(1)的影响:关闭标准输出后,用户级缓冲区的数据因无法通过系统调用写入内核而丢失;
  4. 存在意义:减少系统调用开销,支持格式化输出,平衡效率与功能需求。

这种 "用户级缓冲 + 内核级缓冲" 的分层设计,是操作系统与应用程序协同优化 I/O 性能的典型体现。


全缓冲与fork

复制代码
[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ Buffer]$ ll
total 8
-rw-rw-r-- 1 ranjiaju ranjiaju  92 Jan  2 13:33 makefile
-rw-rw-r-- 1 ranjiaju ranjiaju 414 Jan  2 14:25 myproc.c

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ Buffer]$ cat myproc.c
#include<stdio.h>
#include<string.h>
#include<unistd.h>

int main()
{
    const char* fstr = "hello fwrite\n";
    const char* str = "hello write\n";

    // c语言接口
    printf("hello printf\n"); //stdout->1
    fprintf(stdout, "hello fprintf\n"); //stdout->1
    fwrite(fstr, strlen(fstr), 1, stdout); //stdout->1
    
    // 操作系统接口
    write(1, str, strlen(str));

    fork();
    return 0;
}

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ Buffer]$ make
gcc myproc.c -o myproc -std=c99

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ Buffer]$ ./myproc > log.txt

[ranjiaju@iZ2vc15k23y9vpuyi3tiqzZ Buffer]$ cat log.txt
hello write
hello printf
hello fprintf
hello fwrite
hello printf
hello fprintf
hello fwrite

一、重定向 > log.txt 的底层原理

当执行 ./myproc > log.txt 时,Shell(命令行解释器)在启动 myproc 进程之前,做了以下关键操作:

  1. 打开文件 :Shell 调用 open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666)

    • O_WRONLY:以只写方式打开。
    • O_CREAT:如果 log.txt 不存在,则创建它。
    • O_TRUNC:如果 log.txt 已存在,则清空其所有内容。
  2. 重定向文件描述符 :Shell 将 open 返回的新文件描述符(假设是 3)复制 到文件描述符 1 的位置。这个操作通过 dup2(new_fd, 1) 系统调用完成。

    • 这意味着,在 myproc 进程看来,文件描述符 1(标准输出)不再指向终端,而是指向了 log.txt 文件。
  3. 启动子进程 :Shell 调用 execve 启动 myproc 程序。新的 myproc 进程继承了这个已经被修改过的文件描述符表。

核心影响 :由于标准输出(stdout)现在指向一个普通文件而不是终端,其缓冲模式发生了改变:

  • 默认行为 :当标准输出连接到终端时,是行缓冲 模式(遇到 \n 刷新)。
  • 重定向后 :当标准输出重定向到文件时,变为全缓冲模式(缓冲区满或进程结束时才刷新)。

二、程序执行流程与缓冲区行为

现在我们来跟踪 myproc 进程的执行:

1. C 语言 I/O 接口(printf, fprintf, fwrite

复制代码
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
fwrite(fstr, strlen(fstr), 1, stdout);
  • 目标 :这些函数都试图向 stdout(文件描述符 1)写入数据。
  • 缓冲行为 :因为 stdout 现在是全缓冲 模式,所以这些函数并不会立即调用系统调用。它们会将数据("hello printf\n", "hello fprintf\n", "hello fwrite\n")暂存到用户级缓冲区 中。此时,这些数据只存在于进程的用户空间内存中,并未写入 log.txt 文件

2. 系统调用接口(write

复制代码
write(1, str, strlen(str));
  • 目标 :直接向文件描述符 1 写入数据。
  • 无缓冲行为write 是一个系统调用,它绕过了 C 语言的用户级缓冲区 。它会直接将数据("hello write\n")从用户空间拷贝到内核缓冲区,并最终由操作系统写入 log.txt 文件。
  • 结果log.txt 文件中此时已经有了第一行内容:hello write

3. fork() 的关键作用

复制代码
fork();
  • 创建子进程fork() 创建了一个与父进程几乎完全相同的子进程。
  • 继承与共享 :子进程会继承 父进程的文件描述符表和用户级缓冲区。这意味着,在 fork 的那一刻,子进程的用户级缓冲区里也包含了尚未刷新的 C 语言 I/O 数据("hello printf\n", "hello fprintf\n", "hello fwrite\n")。
  • 写时拷贝(Copy-on-Write, COW) :在 fork 之后,父进程和子进程的用户级缓冲区是共享 的,内核只为它们维护一份物理内存。只有当其中一个进程试图修改 这块内存时(例如,向缓冲区写入新数据,或者在刷新时标记缓冲区为已清空),内核才会为该进程复制一份新的内存副本,然后再执行修改。

三、进程结束与缓冲区刷新

fork() 之后,父进程和子进程会各自独立地继续执行,直到 main 函数返回。

  1. 父进程结束 :当父进程执行到 return 0; 时,C 语言运行时库会自动调用 fflush 来刷新所有打开的流。

    • 它会尝试刷新用户级缓冲区中的数据。这个 "刷新" 操作本身就是一种修改
    • 触发写时拷贝 :由于父进程要修改共享的用户级缓冲区,内核会为它创建一个私有副本
    • 执行刷新 :父进程将自己副本中的数据("hello printf\n", "hello fprintf\n", "hello fwrite\n")通过系统调用写入 log.txt
  2. 子进程结束 :几乎在同一时间,子进程也执行到 return 0;

    • 它同样会尝试刷新自己的用户级缓冲区。
    • 使用自己的副本:因为父进程已经触发了写时拷贝,子进程现在也拥有了自己的一份用户级缓冲区副本(内容与父进程刷新前完全相同)。
    • 执行刷新 :子进程将自己副本中的数据同样写入 log.txt

四、最终结果分析

  • 系统调用 write :只在父进程中执行了一次,并且是在 fork 之前,所以 hello writelog.txt 中只出现一次
  • C 语言 I/O 接口 :它们的数据被暂存在用户级缓冲区中。fork 导致父进程和子进程各有一份相同的缓冲区数据。当两个进程分别结束并刷新时,它们各自将缓冲区内容写入文件,导致 C 语言 I/O 的内容在 log.txt 中出现两次

log.txt 内容解析

复制代码
hello write             <-- 来自父进程在 fork() 前的 write() 调用
hello printf            \
hello fprintf           |-- 来自父(子)进程结束时的缓冲区刷新
hello fwrite            /
hello printf            \
hello fprintf           |-- 来自子(父)进程结束时的缓冲区刷新
hello fwrite            /

核心总结

  1. 重定向改变缓冲模式> log.txtstdout 从终端(行缓冲)改为文件(全缓冲),导致 C 语言 I/O 数据暂存。
  2. 系统调用无缓冲write 直接与内核交互,不受用户级缓冲区影响。
  3. fork 继承缓冲区:子进程完整继承了父进程的用户级缓冲区数据。
  4. 写时拷贝与双重刷新:父、子进程在结束时分别刷新自己的缓冲区副本,导致 C 语言 I/O 内容被写入两次。

这个案例完美地展示了用户级缓冲区、I/O 重定向和fork写时拷贝机制如何协同工作并产生非直观的结果。

相关推荐
牛奶咖啡135 小时前
Linux的ext4文件系统元数据故障恢复实践教程
linux·服务器·机械硬盘的结构·ext4文件系统的构成·ext4超级块故障的修复·ext4块组描述故障修复·ext4块组的构成
hhzz5 小时前
Docker 搭建 NextCloud + OnlyOffice 完整教程(Linux Centos7系统)
linux·docker·容器·onlyoffice·nextcloud
.普通人5 小时前
树莓派4Linux 可操作多个gpio口驱动编写
linux
01传说5 小时前
Linux-yum源切换阿里centos7 实战好用
linux·运维·服务器
颜子鱼5 小时前
Linux字符设备驱动
linux·c语言·驱动开发
是娇娇公主~5 小时前
Redis 悲观锁与乐观锁
linux·redis·面试
晚风_END6 小时前
Linux|服务器运维|diff和vimdiff命令详解
linux·运维·服务器·开发语言·网络
HIT_Weston6 小时前
83、【Ubuntu】【Hugo】搭建私人博客:文章目录(二)
linux·运维·ubuntu
.普通人6 小时前
树莓派4Linux 单个gpio口驱动编写
linux