【Linux】基础IO -- 重定向

前言

上篇基础IO的文件,我们对文件有了初步的认识,也学习了系统调用的文件操作。
打开文件open的返回值 -- 文件描述数,我们还没有学习。
另外,本篇博客也会进一步学习文件相关的知识,还有Linux的一个操作 -- 重定向
那么话不多说,让我们开始今天的学习

一. 文件描述符

1. 问题发现

我们回顾上一篇博客的内容

  1. 当我们要打开一个叫log.txt的文件时,如果没有,那么open函数得返回值是-1
    在这里插入图片描述
  2. 当log.txt的文件存在,或者我们以O_CREAT的方式创建成功调用open函数后,返回一个文件描述符
    在这里插入图片描述

但不禁引发我们思考,为什么文件描述符返回值是3呢?
我们再做个实验

我们同时打开同一个文件5次,每次都接收其文件描述符并打印。我们观察一下文件描述符的情况。

在这里插入图片描述

实验结果如下
在这里插入图片描述
随着我们不断打开文件,返回的文件描述符也逐渐递增。

那这是否就说明,在我们打开log.txt这个文件之前,就已经有文件处于打开状态了呢?

事实就是如此。


2. 三个文件

其实,任何一个进程,在启动的时候,默认会打开当前进程的三个文件
分别是标准输入,标准输出,标准错误
在这里插入图片描述

三个文件相对应的设备文件分别如下
在这里插入图片描述
虽然标准输出和标准错误的默认设备文件都是显示器文件,但二者还是有所不同的
以下作个小实验

我们分别使用C语言和C++在标准输出和标准错误输出语句
在这里插入图片描述
实验结果如下
在这里插入图片描述
二者都向显示器打印了
但是如果我们将输出重定向到文件中呢
在这里插入图片描述

我们发现标准输出被重定向到文件中了,但是标准错误仍然还是输出到显示器中。
所以二者并不一样

回归我们刚才的问题,为什么打开新文件返回的文件描述符直接是3呢?有文件提前打开了吗?
答案是,是的。的确有文件已经打开了,就是我们以上提到的三个文件:标准输入,标准输出,标准错误
标准输入对应0号文件,标准输出对应1号文件,标准错误对应2号文件。然后我们新打开的文件自然只能从3开始


还有一个问题:如果我们打开文件又关闭,那么再打开新的文件,他的文件描述符是多少呢?

我们再做一个小实验
我们在新打开两个文件后,关闭0号文件–标准输入文件
然后再打开3个文件。最后打印他们的文件描述符
在这里插入图片描述
实验结果如下
在这里插入图片描述

我们发现在关闭0号文件后,新打开的文件的文件描述符变成了0
这样的文件描述符分配有些像静态链表

3. 文件描述符分配规则

我们首先看一个图
在这里插入图片描述

我们对这个图作出一些解释

  1. 我们知道文件在被加载到内存后,会创建一个struct file的结构体
    这个结构体内部有文件的属性,内容,还有一个缓冲区
  2. 我们还知道,因为被打开的文件会同时存在多个,所以操作系统会创建进程来管理文件,也就是struct task_struct,其内部有一个指针,该指针的类型是struct file_struct*
  3. struct file_struct也是一个结构体,其内部有一个指针数组,该数组存储的指针类型是struct file*,也就是文件的结构体。这样就可以用数据结构来管理文件了。而文件描述符就是其结构体在该指针数组的下标
  4. 文件描述符的分配类似静态链表,会自动索引第一个空着的位置,然后返回其下标。
  5. 我们调用write,其实是将要写入文件的内容,通过文件描述符,找到指定的文件,然后将内容写入其缓冲区
  6. 并且通过struct file* array[],指针,可以将操作系统和文件系统进行解耦

4. FILE结构体

在这里插入图片描述

我们看到C语言的三个标准文件的类型都是FILE*
那这个FILE和struct file有关系吗?
结论是二者不同,有上下级的关系

在这里插入图片描述
正如这个图
stdin,stdout,stderr是C语言的三个结构体,语言层面是不能和操作系统直接交互的,必须借助系统接口。
在这里插入图片描述
仅通过C语言的函数接口是无法访问操作系统的,所以其内部一定封装了系统调用的fd
而其内部将fd重命名成了_fileno

我们做个小测试展示一下
在这里插入图片描述
实验结果如下
在这里插入图片描述
所以,函数调用内部必定封装系统调用
FILE结构体内部一定封装fd文件描述符,并且通过文件描述符才能找到指定的文件

二. 重定向

通过以上文件描述符的学习,我们要进入新的知识点 – 重定向

我们首先看一段实验代码
输出重定向
在这里插入图片描述

实验结果如下
在这里插入图片描述
我们发现,本来printf应该是打印到显示器中,但是这里并没有,反而是将内容打印到了log.txt。


我们再做个实验
输入重定向
我们先关掉0号文件,再已读的方式打开log,txt,然后scanf输入a和b的值,最后再打印a和b
在这里插入图片描述
我们在log.txt中写入123 456
在这里插入图片描述

实验结果如下
在这里插入图片描述
scanf直接从log.txt中获取了a和b的值

1. 重定向的定义

其实printf和scanf作为C语言的函数接口,同样无法直接和显示器和键盘作交互,其必须依靠相应的文件,也就是标准输出和标准输入
但是依靠的指向,并不是FILE*,而是文件描述符
printf依靠1号文件,scanf依靠0号文件
所以当我们关掉原本标准输出和标准输入的文件,而用log,txt文件替代时,相应的操作会从log,txt中进行

向上面两个实验,在上层无法感知的情况下,在操作系统内部,更改进程对应的文件描述符表中,特定下标的指向,就是重定向

2. >输出重定向&<输入重定向

我们以前在学习Linux指令时,学习过>和<两个指令。>代表输出重定向,<代表输入重定向
本质就是>将输出从显示器更改到指定文件,<将输入从键盘更改到指定文件
这就可以解释我们上面 一. 2.三个文件中
在这里插入图片描述

输出重定向的结果是

在这里插入图片描述

这是因为>是输出重定向,只会更改标准输出的文件描述符,也就是将指向显示器的文件描述符更改为log,txt,但是并不会更改标准错误的文件描述符,所以标准错误依然输出到显示器上,但标准输出输出到了指定的文件中

3. 重定向的应用

通过重定向,我们就可以更加灵活的交互各个文件,实现分流,汇流

(1). 分流

我们知道,标准输出和标准错误,都会将内容打印到显示器上,但二者的内容是有不同意义的,如果同时打印到显示器上,难免会混淆。
所以这里我们就可以使用重定向将两个输出分到两个文件

我们先通过接口调用实现

在这里插入图片描述
我们用两个文件,分别替代原先的stdout和stderr,这样就可以将不同内容写入相应的文件
在这里插入图片描述

(2). 汇流

在之前,我们输出重定向只能将标准输出重定向到文件中,但要是我们也想把标准错误输出到文件中呢?

通过指令我们可以这样实现
首先,运行a.out,会有三个标准输出,和两个标准错误
在这里插入图片描述
仅输出重定向,我们只能将标准输出重定向到文件中
在这里插入图片描述
但是,我们还这样
在这里插入图片描述

这里对上述操作作详细解释
首先,我们运行a.out是将标准输出和标准错误都打到1号文件--显示器
但是我们直接使用>输出重定向,因为>是指令,本质是可执行程序,运行>,会创建进程,在这个进程里关掉1号标准输出文件,用指定文件顶替1号文件描述符,实现重定向。然后进程结束。但是>进程不影响bash这个进程,所以退出>进程,bash的1号文件还是标准输出
这样我们就成功将标准输出重定向到指定文件
此时1号文件其实log.txt,然后我们将2号文件用1号文件顶替,也就是log.txt文件,这样也成功将标准错误重定向到指定文件了

分流,我们同样可以使用指令完成

在这里插入图片描述

这里是首先运行a.out,会向1号文件和2号文件写入,然后我们将1号文件用log.txt顶替,2号文件用err.txt顶替
这样就成功将标准输出和标准错误成功重定向不同的文件

4. 重定向的系统调用接口

dup2

在这里插入图片描述
在这里插入图片描述
我们在使用这个函数的第一步,往往都是弄懂函数的参数和返回值

返回值

_dup 返回一个新的文件说明符。 _dup2 返回 0 以指示成功。 如果发生错误,则每个函数返回 -1,并设置为’errnoEBADF’文件描述符无效,或者’EMFILE’没有其他文件描述符可用

参数

根据文献的翻译,newfd会成为oldfd的拷贝,我们在输出重定向时,是将原本标准输出的文件用其他文件顶替,也就是让标准输出成为其他文件的拷贝
所以,newfd是被重定向的文件,oldfd是要重定向指向的文件
在这里插入图片描述
比如,这里1和2就是newfd,log.txt和err.txt是oldfd

使用代码如下
我们将printf的1号文件重定向到logNormal中
在这里插入图片描述

结果如下
在这里插入图片描述

三. 缓冲区

我们先做个小实验
我们分别用C库和系统调用,往显示器打印内容
在这里插入图片描述
实验结果如下
在这里插入图片描述
我们成功将内容输出到了显示器
但是,当我们将这些内容重定向到文件中,会出现不一样的现象
在这里插入图片描述
我们发现,C库的输出既然打印了两次,这是为什么呢?

接下来,我们就来介绍,关于缓冲区的结构


在这里插入图片描述

结合我们以上学习的知识,我们知道C语言的文件操作会创建一个FILE的结构体,并且其内部一定封装文件描述符
其实,C语言的FILE结构体内部也有缓冲区,我们要输出的东西,会先写入语言层面的缓冲区,并且其有自己的刷新缓冲区的规则

  1. 无缓冲:写一个,刷新一个
  2. 行缓冲:遇到\n刷新缓冲区
  3. 全缓冲:缓冲区全部写满才刷新

显示器采用的刷新策略:行缓冲
普通文件采用的刷新策略:全缓冲
语言层面的缓冲区刷新了,就会通过文件描述符,找到指定文件,然后将缓冲区内容写入指定文件的缓冲区

所以,为什么上面重定向到文件中,C语言的输出会有两次呢?

因为

  1. 当我们重定向时,write是系统调用,也就是bash调用,子进程的改变不影响其他进程,所以write的1号文件并没有被重定向
  2. fprintf因为是语言层面的调用,所以是在子进程中调用,将内容写入FILE结构体的缓冲区。原本是像显示器输出,其策略是行刷新,但是突然被重定向到普通文件,变成了全刷新,所以在fork()时,缓冲区的内容并没有被刷新。然后父子进程,谁先刷新,谁先进行写时拷贝,这样最后就会有两个fprintf了

结束语

本章是对文件操作的进一步学习,感谢您的阅读
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值