【Linux】:重定向和缓冲区

朋友们、伙计们,我们又见面了,本期来给大家带来关于重定向和缓冲区的相关知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!

C 语 言 专 栏:C语言:从入门到精通

数据结构专栏:数据结构

个 人 主 页 :stackY、

C + + 专 栏 :C++

Linux 专 栏 :Linux

目录

[1. 重定向](#1. 重定向)

[1.1 输出重定向](#1.1 输出重定向)

[1.2 追加重定向](#1.2 追加重定向)

[1.3 输入重定向](#1.3 输入重定向)

[1.4 重定向系统调用接口](#1.4 重定向系统调用接口)

[2. 标准错误stderr](#2. 标准错误stderr)

[3. 缓冲区](#3. 缓冲区)

[3.1 缓冲区存在的价值](#3.1 缓冲区存在的价值)

[3.2 缓冲区的刷新方式](#3.2 缓冲区的刷新方式)

[3.3 分析样例](#3.3 分析样例)

[3.4 用户缓冲区和内核缓冲区](#3.4 用户缓冲区和内核缓冲区)

[3.5 何为刷新](#3.5 何为刷新)

[3.6 FILE结构体](#3.6 FILE结构体)


1. 重定向

重定向这个概念在前面Linux常见指令章节就介绍过它的指令以及用法,那么本节来一起深入了解一下重定向:

1.1 输出重定向

echo 字符串 > 文件 :将本来输出在显示器文件(标准输出)上的字符串输出至指定的文件。

标准输出对应的文件fd是1。

下面用代码来实现一下重定向的功能:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

#define FILE_NAME "log.txt"

int main()
{
    // 关闭标准输出
    close(1);
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    printf("fd: %d\n", fd);
    fprintf(stdout, "stdout->fd: %d\n", stdout->_fileno);

    // 刷新
    fflush(stdout);
    close(fd);
}

先看结果,再分析代码:


我们都知道文件fd的分配规则,是寻找最小的未被使用的fd进行分配,所以我们先把1号文件描述符关闭,然后再打开文件时,1号文件描述符就被新打开的文件分配走了,这些C语言打印函数,默认是往1号文件描述符对应的文件中打印,简单的说就是它们只认识1这个数字,并不会在乎这个文件到底是不是显示器文件,所以才会把数据打印到新打开的文件中。


至于这里为什么要加这个fflush用来刷新缓冲区在后续会详细介绍。

1.2 追加重定向

追加重定向直接把打开文件时的方式从清空改为追加即可:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

#define FILE_NAME "log.txt"

int main()
{
    // 关闭标准输出
    close(1);
    //int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_APPEND, 0666);
    printf("fd: %d\n", fd);
    fprintf(stdout, "stdout->fd: %d\n", stdout->_fileno);

    // 刷新
    fflush(stdout);
    close(fd);
}

1.3 输入重定向

cat指令默认是从标准输入键盘文件中读取数据;

cat < 文件:本来从键盘读取数据,但是重定向为从指定的文件读取数据。

标准输入对应的文件fd是0。

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

#define FILE_NAME "log.txt"

int main()
{
    // 关闭标准输入
    close(0);

    int fd = open(FILE_NAME, O_RDONLY);

    char buffer[1024];
    fread(buffer, 1, sizeof(buffer), stdin);

    printf("%s\n", buffer);
    close(fd);
}

当我们把标准输入文件fd关闭后,根据文件fd的分配规则,新创建的文件就被分配到了0号文件fd,C语言的这些读取接口只认识0号文件fd,只认识0这个数组,所以就直接从0号fd对应的文件中直接读取。


重定向之后上层的fd不变,但是底层fd的指向在变化,所以重定向的本质是修改特定文件fd的下标内容。

1.4 重定向系统调用接口

cpp 复制代码
#include <unistd.h>
int dup2(int oldfd, int newfd);

代码演示:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define FILE_NAME "log.txt"

int main()
{
    int fd = open(FILE_NAME, O_RDONLY);
    // 重定向
    dup2(fd, 0);
    char buffer[1024];
    fread(buffer, 1, sizeof(buffer), stdin);

    printf("%s\n", buffer);
    close(fd);
}

dpu2接口其实是文件描述符级别的数组内容的拷贝!
注意:

程序替换是不影响曾经的重定向;

程序替换没有创建新的进程,它更改的是物理到虚拟的转化以及对应的页表,并不会影响进程PCB,所以程序替换不会影响重定向。

2. 标准错误stderr

打印结果分为错误打印和正确打印,正确打印对应的是stdin,错误打印对应的是stderr,它们两者都是打印在显示器上的。

有了stderr之后,就可以将正确打印和错误打印的数据分别存储在两个不同的文件,最主要的是为了查错,当程序出错时,直接去存储错误结果文件查找错误原因。

我们在命令行使用的重定向都是简写,完整的写法是:

bash 复制代码
./exe 1 > log.txt  // 输出重定向至log.txt
./exe 1 > log.txt 2 > &1 // 将标准输出和标准错误都重定向至log.txt
./exe 1 > log.txt 2 > log.txt.error // 标准输出重定向至log.txt,标准错误重定向至log.txt.error

3. 缓冲区

在前面的文件fd章节提到过,读写数据的本质是将内核缓冲区中的数据来回拷贝。

那么我们所理解的缓冲区其实就是一块由操作系统提供的内存空间。

3.1 缓冲区存在的价值

举一个现实中的例子来理解缓冲区:

小明居住在西安,他的好朋友居住在苏州,小华和小明每年都要过生日,双方都会在彼此过生日的时候挑选好生日礼物,小明等到小华过生日的前两个月,直接骑着骑行车从西安历经两个月到了苏州,刚好把他给小华准备的生日礼物送到,小华在小明过生日的时候也一样,都是骑两个月自行车去送礼物,就这样持续了好几年,某一天小明和小华家楼下都开了一家快递公司,每小华过生日的小明直接把礼物交给快递公司,让快递公司托运给小华,当小明把礼物给快递公司时,站在小明的视角礼物已经送走了,但是站在小华的视角,礼物当前还没收到,需要时间,但这个时间肯定比小明骑着自行车送过来要更快。

在这个例子中,这个快递公司扮演的角色就类似于缓冲区,正是有了快递公司的存在,大大提升了小明送礼物的效率。
所以缓冲区的存在可以提高使用者的效率,正是因为有了缓冲区的存在,我们可以积累一部分数据再统一发送,减小了发送成本,提升了发送效率。

3.2 缓冲区的刷新方式

因为缓冲区可以暂存数据,所以它必须要有对应的刷新策略;
一般策略:

    1. 无缓冲(有数据立即刷新)
    1. 行缓冲(按行为单位进行刷新)
    1. 全缓冲(等到数据写满缓冲区再刷新)

特殊策略:

    1. 强制刷新
    1. 进程退出的时候,一般要进行刷新缓冲区

对于显示器文件,一般使用的是行刷新策略;

对于磁盘文件,一般使用的是全缓冲策略。

3.3 分析样例

下面以缓冲区这个概念为基础,分析一下下面这段代码:

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
    fprintf(stdout, "C: hello fprintf\n");
    printf("C: hello printf\n");
    fputs("C: hello fputs\n", stdout);
    const char *str = "system call: hello write\n";
    write(1, str, strlen(str));

    fork(); // 注意fork的位置!
    return 0;
}

命令行运行结果:


当我们直接运行是,和预期一致,都是打印在显示器上的,没有任何问题,但是一旦我们重定向至文件,此时就很奇怪了,接下来我们一步一步分析:

    1. 当我们直接向显示器打印,显示器文件的刷新方式是行刷新,我们打印的字符串都有'\n',在fork创建子进程之前,数据已经被刷新完毕,所以三条C接口消息和一条系统调用接口消息。
    1. 当我们将内容重定向至文件log.txt,本质就是向磁盘文件进行写入,我们的系统对于数据的刷新策略从行缓冲变成了全缓冲!
    1. 全缓冲的策略意味着缓冲区变大,我们写入的简单数据不足以把缓冲区写满,所以在fork执行的时候,数据依旧停留在缓冲区中。
    1. 当进程退出的时候,一般要刷新缓冲区,即使数据没有满足刷新条件!
    1. 观察文件中的写入结果发现C接口写入的数据是双倍的,但是系统调用接口写入的数据只有一个,所以这里的缓冲区和和操作系统没有关系,只和C语言本身有关!
    1. C/C++提供给我们的缓冲区,里面一定保存的是用户的数据,属于当前进程在运行时自己的数据,但是,当我们把数据交给了OS,此时该数据就属于OS,不属于用户了。
    1. 刷新缓冲区的这个操作就是把进程的数据写入到操作系统,刷新的操作属于清空、写入,所以,在fork之后,OS检测到了父子进程任意一方要对数据进行写入、清空,此时就发生了写时拷贝,父子进程各有一份数据,所以才会C语言调用的接口写入数据时才会写入两次。
    1. 系统调用接口是直接写入到操作系统,不属于进程数据,所以不发生写时拷贝,只会有一份数据。

3.4 用户缓冲区和内核缓冲区

用户缓冲区就是我们使用的C/C++提供的语言级别的缓冲区。

内核缓冲区是由OS提供的一块内存空间。

3.5 何为刷新

我们使用C语言的接口写入数据时首先是要把数据写入到C语言提供的缓冲区的,那么C语言的缓冲区就有对应的刷新策略(行缓冲、全缓冲等),当数据满足刷新策略时,就会将数据写入到内核缓冲区,所以从用户缓冲区写入到内核缓冲区的这个工作就叫做刷新。

内核缓冲区刷新也有它对应的刷新策略。
C/C++语言提供的缓冲区也是为了提高函数调用(printf、fprintf等)的效率。

3.6 FILE结构体

前面说过FILE结构体中包含了文件描述符,现在来看它里面也必定也包含了C缓冲区

在Linux在命令行输入:vim /usr/include/libio.h +246 就可以查看对应的FILE结构体对象了。

朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,欲知后事如何,请听下回分解~,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!

相关推荐
传而习乎7 分钟前
Linux:CentOS 7 解压 7zip 压缩的文件
linux·运维·centos
soulteary8 分钟前
突破内存限制:Mac Mini M2 服务器化实践指南
运维·服务器·redis·macos·arm·pika
我们的五年16 分钟前
【Linux课程学习】:进程程序替换,execl,execv,execlp,execvp,execve,execle,execvpe函数
linux·c++·学习
爱吃青椒不爱吃西红柿‍️36 分钟前
华为ASP与CSP是什么?
服务器·前端·数据库
IT果果日记38 分钟前
ubuntu 安装 conda
linux·ubuntu·conda
Python私教40 分钟前
ubuntu搭建k8s环境详细教程
linux·ubuntu·kubernetes
羑悻的小杀马特1 小时前
环境变量简介
linux
小陈phd1 小时前
Vscode LinuxC++环境配置
linux·c++·vscode
运维&陈同学1 小时前
【zookeeper01】消息队列与微服务之zookeeper工作原理
运维·分布式·微服务·zookeeper·云原生·架构·消息队列
是阿建吖!1 小时前
【Linux】进程状态
linux·运维