首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >峰回百转:Linux基础IO,重定向与缓冲区的诗意探索

峰回百转:Linux基础IO,重定向与缓冲区的诗意探索

作者头像
用户11379153
发布2025-11-05 16:32:01
发布2025-11-05 16:32:01
1630
举报
在这里插入图片描述
在这里插入图片描述

引言

在这个数字的海洋中,Linux如同一位睿智的航海者,带领我们穿越信息的波涛。基础IO操作,尤其是重定向与缓冲区的理解,犹如一首优雅的乐章,蕴含着深邃的哲理与实用的技巧。

🌇序章

文件描述符 fd 是基础IO中的重要概念,一个 fd 表示一个 file 对象,如常用的标准输入、输出、错误流的 fd 分别为 0、1、2 实际进行操作时,OS 只需要使用相应的fd即可,不必关心具体的 file,因此我们可以对标准流实施 重定向,使用指定的文件流,在实际 读/写 时,为了确保 IO 效率,还需要借助 缓冲区 进行批量读取,最大化提高效率。关于上述各种概念,将会在本文中详细介绍,且听我娓娓道来。

🏙️正文

一、文件描述符

在使用 C语言 相关文件操作函数时,可以经常看到 FILE 这种类型,不同的 FILE* 表示不同的文件,实际进行读写时,根据 FILE* 进行操作即可

代码语言:javascript
复制
#include<iostream>
#include <cstdio>

using namespace std;

int main()
{
    //分别打开三个 FILE 对象
    FILE* fp1 = fopen("test1.txt", "w");
    FILE* fp2 = fopen("test2.txt", "w");
    FILE* fp3 = fopen("test3.txt", "w");

    //对不同的 FILE* 进行操作
    //……

    //关闭
    fclose(fp1);
    fclose(fp2);
    fclose(fp3);
    fp1 = fp2 = fp3 = NULL;

    return 0;
}

那么在 C语言 中,OS 是如何根据不同的 FILE 指针,对不同的 FILE 对象进行操作的呢?*

  • 答案是 文件描述符 fd,这是系统层面的标识符,FILE 类型中必然包含了这个成员
在这里插入图片描述
在这里插入图片描述

如何证明呢?实践出真知,在上面代码的基础上,加入打印语句

注:stdin 等标准流在C语言中被覆写为 FILE 类型

代码语言:javascript
复制
//标准文件流
cout << "stdin->fd: " << stdin->_fileno << endl;
cout << "stout->fd: " << stdout->_fileno << endl;
cout << "stderr->fd: " << stderr->_fileno << endl;
cout << "===================================" << endl;
cout << "此时标准流的类型为:" << typeid(stdin).name() << endl;
cout << "此时文件流的类型为:" << typeid(fp1).name() << endl;
cout << "===================================" << endl;
//自己打开的文件流
cout << "fp1->fd: " << fp1->_fileno << endl;
cout << "fp2->fd: " << fp2->_fileno << endl;
cout << "fp3->fd: " << fp3->_fileno << endl;
在这里插入图片描述
在这里插入图片描述

可以看出,FILE 类型中确实有 fd 的存在

文件描述符 是如何设计的?新打开的文件描述符为何是从 3 开始?别急,接着往下看

1.1、先描述,再组织

操作系统是一个伟大的产物,它可以调度各种资源完成各种任务,但资源太多、任务太重,不合理的分配会导致效率低下,因此在进行设计时,必须确保 OS 操作时的高效性

比如现在学习的 文件系统,倘若不进行设计的话,在进行 IO 时,OS 必须先将所有文件扫描一遍,找到目标文件后才能进行操作,这是非常不合理的

因此,根据 先描述、再组织 原则,OS 将所有的文件都统一视为 file 对象,获取它们的 file 指针,然后将这些指针存入指针数组中,可以进行高效的随机访问和管理,这个数组为 file* fd_array[],而数组的下标就是神秘的 文件描述符 fd

  • 当一个程序启动时,OS 会默认打开 标准输入标准输出标准错误 这三个文件流,将它们的 file* 指针依次存入 fd_array 数组中,显然,下标 0、1、2 分别就是它们的文件描述符 fd;
  • 后续再打开文件流时,新的 file* 对象会存入当前未被占用的最小下标处,所以用户自己打开的 文件描述符一般都是从 3 开始

除了文件描述符外,还需要知道文件权限大小路径引用计数挂载数等信息,将这些文件属性汇集起来,就构成了 struct files_struct 这个结构体,而它正是 task_struct 中的成员之一

1.2、files_struct

files_struct 结构体是对已打开文件进行描述后形成的结构体,其中包含了众多文件属性,本文探讨的是 文件描述符 fd

在这里插入图片描述
在这里插入图片描述

进程的 PCB 信息中必然包含文件操作相关信息,这就是 files_struct

注:文件被打开后,并不会加载至内存中(这样内存早爆了),而是静静的躺在磁盘中,等待进程与其进行 IO,而文件的 inode可以找到文件的详细信息:所处分区、文件大小、读写权限等,关于 inode 的更多详细信息将会在 【深入理解文件系统】 中讲解

1.3、分配规则

fd 的分配规则为:先来后到,优先使用当前最小的、未被占用的 fd

存在下面两种情况:

  • 直接打开文件 file.txt,分配 fd 为 3
  • 先关闭标准输入 stdin 中原文件执行流(键盘),再打开文件 file.txt,分配 fd 为 0,因为当前 0 为最小的,且未被占用的 fd
代码语言:javascript
复制
#include<iostream>
#include <cstdio>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

using namespace std;

int main()
{
    //先打开文件 file.txt
    int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    assert(fd != -1);   //存在打开失败的情况

    cout << "单纯打开文件 fd: " << fd << endl;

    close(fd);  //记得关闭

    //先关闭,再打开
    close(0);   //关闭1号文件执行流
    fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);

    cout << "先关闭1号文件执行流,再打开文件 fd: " << fd << endl;

    close(fd);

    return 0;
}
在这里插入图片描述
在这里插入图片描述

注意: 假若将标准输出 stdout 中的原文件执行流(显示器)关闭了,那么后续的打印语句将不再向显示器上打印,而是向此时 fd 为 1 的文件流中打印

这其实就是 重定向 的基本操作

1.4、一切皆文件

如何理解 Linux 中一切皆文件这个概念?

  • 现象:即使是标准输入(键盘)、标准输出(显示器) 在OS 看来,不过是一个file对象
  • 原理:无论是硬件(外设),还是软件(文件),对于 OS 来说,只需要提供相应的 读方法写方法 就可以对其进行驱动,打开文件流后,将 file* 存入fd_array中管理即可,因此在 Linux 中,一切皆文件
在这里插入图片描述
在这里插入图片描述

二、重定向

在学习重定向前,首先要明白 标准输入输出错误 的用途

  • 标准输入(stdin)-> 设备文件 -> 键盘文件
  • 标准输出(stdout)-> 设备文件 -> 显示器文件
  • 标准错误(stderr)-> 设备文件 -> 显示器文件

标准输入:从键盘中读取数据 标准输出:将数据输出至显示器中 标准错误:将可能存在的错误信息输出至显示器中

标准输出 标准错误 都是向显示器中输出数据,为什么不合并为一个?

  • 因为在进行排错时,可能需要单独查看错误信息,若是合并在一起,查看日志时会非常麻烦;但如果分开后,只需要将 标准错误 重定向后,即可在一个单独的文件中查看错误信息

C/C++ 中进行标准输入、输出、错误对应流:

  • 标准输入:stdin / cin
  • 标准输出:stdout / cout
  • 标准错误:stderr / cerr

使用cerr函数可直接向标准错误流中打印信息

2.1、重定向的本质

前面说过,OS 在进行 IO 时,只会根据标准输入、输出、错误对应的文件描述符 0、1、2 来进行操作,也就是说 OS 作为上层不必关心底层中具体的文件执行流信息(fd_array[] 中存储的对象) 因此我们可以做到 “偷梁换柱”,将这三个标准流中的原文件执行流进行替换,这样就能达到重定向的目的了

在这里插入图片描述
在这里插入图片描述

2.2、利用指令重定向

下面直接在命令行中实现输出重定向,将数据输出至指定文件中,而非屏幕中

代码语言:javascript
复制
echo you can see me > file.txt

当然也可以 从 file.txt 中读取数据,而非键盘

代码语言:javascript
复制
cat < file.txt

现在可以理解了,

  • > 可以起到将标准输出重定向为指定文件流的效果,
  • >> 则是追加写入
  • < 则是从指定文件流中,标准输入式的读取出数据

除此之外,我们还可以利用程序进行操作,在运行后进行重定向即可

代码语言:javascript
复制
#include <iostream>

using namespace std;

int main()
{
    cout << "标准输出 stdout" << endl;
    cerr << "标准错误 stderr" << endl;
    return 0;
}

直接运行的结果,此时的标准输出和标准错误都是向显示器上打印

在这里插入图片描述
在这里插入图片描述

利用命令行只对 标准输出 进行重定向,file.txt 中只收到了来自 标准输出 的数据,这是因为 标准输出标准错误 是两个不同的 fd,现在只重定向了 标准输出 1

2.3、利用函数重定向

系统级接口 int dup2(int oldfd, int newfd)

在这里插入图片描述
在这里插入图片描述

函数解读:

  • oldfd表示需要重定向的fd,例如上文中我们将输出到标准输出的内容输出到了file.txt中,则file.txt的fd为oldfd,newfd为2
  • newfd会被oldfd的内容覆盖

下面来直接使用,模拟实现报错场景,将正常信息输出至 log.normal,错误信息输出至log.error

代码语言:javascript
复制
#include <iostream>
#include <cstdlib>
#include <cerrno>
#include <cassert>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

using namespace std;

int main()
{
    //打开两个目标文件
    int fdNormal = open("log.normal", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fdError = open("log.error", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    assert(fdNormal != -1 && fdError != -1);

    //进行重定向
    int ret = dup2(fdNormal, 1);
    assert(ret != -1);
    ret = dup2(fdError, 2);
    assert(ret != -1);

    for(int i = 10; i >= 0; i--)
        cout << i << " ";  //先打印部分信息
    cout << endl;

    int fd = open("cxk.txt", O_RDONLY); //打开不存在的文件
    if(fd == -1)
    {
        //对于可能存在的错误信息,最好使用 perror / cerr 打印,方便进行重定向
        cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(-1);   //退出程序
    }

    close(fd);

    return 0;
}
在这里插入图片描述
在这里插入图片描述

在开发大型项目时,将 错误信息 单独剥离出来是一件很重要的事

三、缓冲区

3.1、缓冲区存在的意义

在【基础IO】 中还存在一个重要概念:缓冲区

缓冲区 其实就是一个buffer数组,配合不同的刷新策略,起到提高IO效率的作用

必要性:

CPU 计算速度非常快!而磁盘的读取速度相对于 CPU来说是非常非常慢的,因此需要先将数据写入缓冲区中,依据不同的刷新策略,将数据刷新至内核缓冲区中,供 CPU进行使用,这样做的是目的是尽可能的提高效率,节省调用者的时间

因此在进行 读取 / 写入 操作时,常常会借助 缓冲区 buffer

代码语言:javascript
复制
#include <iostream>
#include <cassert>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

using namespace std;

int main()
{
    int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    assert(fd != -1);

    char buffer[256] = { 0 };   //缓冲区
    int n = read(0, buffer, sizeof(buffer));    //读取信息至缓冲区中
    buffer[n] = '\0';

    //写入成功后,在写入文件中
    write(fd, buffer, strlen(buffer));

    close(fd);
    return 0;
}

3.2、缓冲区刷新策略

缓冲区有多种刷新策略,比如C语言中 scanf 的缓冲区刷新策略为:遇到空白字符或换行就刷新,因此在输入时需要按一下回车,缓冲区中的数据才能刷新至内核缓冲区中,而 printf 的刷新策略为 行缓冲,即遇到 \n 才会进行刷新

总体来说,缓冲区的刷新策略分为以下三种:

  • 无缓冲 -> 没有缓冲区
  • 行缓冲 -> 遇到 \n 才进行刷新,一次冲刷一行
  • 全缓冲 -> 缓冲区满了才进行刷新

一般而言,显示器的刷新策略为 行缓冲,而普通文件的刷新策略为 全缓冲

一个简单的 demo 观察 行缓冲

代码语言:javascript
复制
#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{
    while(true)
    {
        //未能触发行缓冲的刷新策略,只能等缓冲区满了被迫刷新
        printf("%s", "hehehehe");
        sleep(1);
    }

    return 0;
}

运行结果:无内容打印

稍微改一下代码

代码语言:javascript
复制
while(true)
{
    //能触发行缓冲的刷新策略
    printf("%s\n", "hehehehe");
    sleep(1);
}

运行结果:每隔一秒,打印一次

文件分为两种,内存文件磁盘文件

  • 对文件的任何操作,都必须先把文件加载到内核对应的文件缓冲区内(即磁盘——内存的拷贝)
  • 一个进程可能打开多个文件,之前进程提到的用task_struct(PCB)结构体采取双链表的方式进行管理,文件同理。
  • 文件采用strcut file,利用链表和指针在缓冲区内层层相连
  • 进程在使用不同文件时,区分方法即为fd,fd数组下标,分别与不同的文件形成映射

3.3、普通缓冲区与内核级缓冲区

每一个file对象中都有属于自己的缓冲区及刷新策略,而在系统中,还存在一个内核级缓冲区,这个缓冲区才是 CPU 真正进行IO的区域

IO 流程:

  • 先将普通缓冲区中的数据刷新至内核级缓冲区中,CPU 再从内核级缓冲区中取数据进行运算,然后存入内核级缓冲区中,最后再由内核级缓冲区冲刷给普通缓冲区
在这里插入图片描述
在这里插入图片描述

这里有一段比较有意思的代码:

代码语言:javascript
复制
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>

using namespace std;

int main()
{
    fprintf(stdout, "hello fprintf\n");
    const char* str = "hello write\n";
    write(1, str, strlen(str));

    fork(); //创建子进程
    return 0;
}

当我们直接运行程序时,结果如下:

在这里插入图片描述
在这里插入图片描述

而当我们进行重定向后,结果如下:

在这里插入图片描述
在这里插入图片描述

重定向前后出现两种截然不同的打印结果

原因分析:

  • 显示器刷新策略为 行缓冲,而普通文件为全缓冲
  • 直接运行程序时:此时是向 显示器 中打印内容,因为有 \n,所以两条语句都直接进行了冲刷
  • 进行重定向后:此时是向 普通文件 中打印内容,因为普通文件是写满后才能刷新,并且 fprintf 有属于自己的缓冲区,这就导致 fork() 创建子进程后,父子进程的 fprintf 缓冲区中都有内容,当程序运行结束后,统一刷新,于是就是打印了两次 hello fprintf

注:系统级接口是没有自己的缓冲区的,直接冲刷至内核级缓冲区中,比如 write,所以创建子进程对 write 的冲刷没有任何影响

结语:重定向与缓冲区的交响乐

在Linux的世界里,重定向与缓冲区的理解不仅是技术的掌握,更是对信息流动的深刻洞察。它们如同两位舞者,在命令行的舞台上翩翩起舞,交织出一曲曲动人的旋律。通过灵活运用这些技巧,我们不仅能够高效地处理数据,还能在这片广袤的数字海洋中,找到属于自己的航道。让我们继续探索,书写更多的篇章,迎接下一个技术的曙光。

本篇关于重定向与缓冲区的介绍就暂告段落啦,希望能对大家的学习产生帮助,欢迎各位佬前来支持斧正!!!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-05-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 🌇序章
  • 🏙️正文
  • 一、文件描述符
    • 1.1、先描述,再组织
    • 1.2、files_struct
    • 1.3、分配规则
    • 1.4、一切皆文件
  • 二、重定向
    • 2.1、重定向的本质
    • 2.2、利用指令重定向
    • 2.3、利用函数重定向
  • 三、缓冲区
    • 3.1、缓冲区存在的意义
    • 3.2、缓冲区刷新策略
    • 3.3、普通缓冲区与内核级缓冲区
  • 结语:重定向与缓冲区的交响乐
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档