当前位置: 首页 > article >正文

从0开始linux(21)——文件(2)文件重定向

欢迎来到博主的专栏:从0开始linux
博主ID:代码小豪

文章目录

    • 设备文件
    • 文件缓冲区
    • 重新认识文件描述符
    • 重定向

设备文件

在前一篇文章博主提到,当一个c/c++进程运行时,会默认打开三个文件流,分别是stdin,stdout,stderr。分别对应键盘,显示器,显示器,我们将这三个文件流称为标准文件流。但是大家有没有思考过这么一个问题?那就是为什么外部输入设备,竟然可以用文件打开?文件不是一个数据吗?

不知道大家有没有听过这么一句话,叫做linux下,一切皆文件。比如我们的指令,它们是文件,保存在/usr/bin目录下,而设备也是文件,保存在/dev目录下。

如果我们仅拿这个论证,证明设备是文件,其实是倒果为因了,因为/dev中存在设备文件,是linux将设备设计成文件的结果之一。因此我们想搞清楚linux是如何将设备描述成文件?又是为什么要将其设计成文件?这才是真正重要的东西。

为了搞清楚这点,我们需要回到系统内核本身来看,在前面的章节中,我们了解到linux内核的主要功能有:管理进程,管理文件,外设交互等等的功能。这里博主就要抛出一个概念了,叫做linux对于管理对象,总是先描述在组织

如何理解先描述在组织呢?简单来说,就是linux会先设计出管理的对象的数据结构,接着通过这些数据结构,再去进行对象的行为操作。比如linux为了管理进程,会创建出task_struct来描述进程,接着在通过task_struct来管理进程,为了管理文件,就创建了struct file来描述文件,接着在设计出针对这些对象的方法,比如关闭进程,打开文件,关闭文件等。

linux则是将设备描述成struct device结构体,以我们常见的pc为例,外部输入设备有键盘,显示器,磁盘,网卡等。而每个设备,其对应的功能又不同,(从系统的角度上看)比如显示器可写不可读,键盘可读不可写。因此不同的设备,其对应的struct device的具体属性是不同的。
在这里插入图片描述
但是由于不同的设备,存在差异,对于系统来说,与外设进行交互实在是太频繁了,而且设备的数量随着时代的发展,也会越来越多,如果系统与外设交互时,都需要创建出特殊的对象,那实在是太麻烦了。

那么linux的设计者想了想,发现了这么一个特性:对于设备来说,种类很多,但是系统的交互无外乎就是读和写两件事,而文件呢?每个文件在结构上基本都是相同的,而且也能进行读和写的操作。那么如果我们借助文件的结构,来描述设备,是不是就能完成统一了?

于是有趣的地方来了,我们的进程,需要与外设进行交互(比如printf),为了让用户的使用方便,在struct device和task_struct之间,建立了一个struct file,作为两者沟通桥梁。于是乎进程对于外设的写入,就变成对文件的写入,而对外设的读取,就变成了文件的读取,方便性得到了大大的提升。
在这里插入图片描述

在上图中,博主将表现这种关系的代码写成了c++风格的代码,但是实际上linux是由C语言写成的,实际上struct
file当中read和write在源码当中其实是函数指针,指向对应设备的write的read函数,这么写只是方便理解罢了。

这里博主为了验证这个观点,放出linux当中的源代码:
在这里插入图片描述
可以看到,在struct file当中,存在一个名叫file_operations的成员,该成员当中保存着各种各样的与文件操作相关的函数指针,因此,如果我们的进程想要与外设进行交互,系统就会将该外设对应的文件打开,然后对文件执行,对应的读写操作。由于存在函数指针这一层的封装,对文件的读写操作,就会转变成,对于外设的读写操作。

文件缓冲区

当我们读写普通文件或者设备文件时,并非是直接向对应的文件直接进行读写的,而是在文件与cpu之间,创建一个文件缓冲区,该缓冲区存在在内存当中,当我们写入文件时,实际上是向文件缓冲区进行写入,读取文件时,首先将文件当中的数据写入到文件缓冲区当中,接着再向文件缓冲区当中读取我们想要的数据。

首先我们要搞清楚一点,就是打开文件的是进程,读写文件的也是进程,操作文件的行为,都是进程执行的。比如我们想要像显示器写入数据,是不是要在进程当中调用printf()?我们想要从键盘当中读取数据,是不是要写scanf?因此操作文件的并非用户,而是进程,或者说是用户在进程当中对文件进行操作。

以普通文件log.txt为例,当我们打开文件时,首先操作系统会在内存当中生成对应的struct file结构。在struct file当中会存在一个指针,该指针指向文件缓冲区(buffer)。
在这里插入图片描述
此时进程1调用write()函数,向文件写入"hello world",实际上并不是向磁盘当中的log.txt文件写入“hello world”,而是向buffer当中写入"hello world".在这里插入图片描述
此时,磁盘当中的log.txt其实并没有被写入数据,只有当操作系统刷新缓冲区时,这个数据才会被写入到磁盘当中的log.txt里面,比如我们在写word文档时,实际上并不是将文本写入到磁盘当中的word文档,而是写在其对应的文件缓冲区当中。只有我们点击了保存以后,才算是真正将数据写入到磁盘当中的word文档,否则写的文本在下次打开word文档时就会消失(不过现在有自动保存,这个现象很少能见到了)。

那么当我们读取文件的数据时,也并不是直接读取磁盘当中的内容,而是操作系统会先将文件的数据加载到文件缓冲区当中,然后进程在文件缓冲区当中读取,比如我们现在运行进程2,让进程2读取log.txt文件的数据(“hello world”)。
在这里插入图片描述
为什么要创建一个文件缓冲区而不是直接读取磁盘当中的数据呢?其实还是效率问题,首先我们要知道,cpu的计算速度是很快的,而外设与内存之间的交互是很慢的,cpu与内存的交互速度远快于内存与外设的交互速度。因此如果频繁的让内存与磁盘进行交互,运行速度会非常慢,因此有了文件缓冲区,让内存一次性获取磁盘文件的大量数据,这样就能减少内存与磁盘交互的次数。运行效率会快上不少。(我们的读写操作,从cpu->内存->磁盘,变成cpu->内存,当必要时才让内存与磁盘交互,这样就减少了内存与磁盘的交互次数。)

重新认识文件描述符

我们在前一篇章节当中重点讲解了文件描述符,但是有一个细节博主只是简单带过了,因为这个细节与文件的重定向操作有关,因此博主将其放在这篇文章当中讲述,因为后面就要讲解与重定向相关的内容了。

首先,文件是由进程打开的,因此在进程的pcb(task_struct)当中,存在一个指针,指向当前已被打开的文件。由于一个进程可以打开多个文件,因此只有一个指针是不够的(要指向多个文件),因此pcb当中管理被打开文件是一个文件指针数组fd_array[N]。
在这里插入图片描述

一个进程被运行,会创建一个对应的task_struct结构,而每一个打开的文件,都有一个对应struct file结构,而一个进程可以打开多个文件,因此就有多个struct file结构被创建。这个fd_array,保存的是指向struct file的指针。

在上一篇博客中。博主简单了的提了一下,open函数会返回一个文件的文件描述符fd,如果我们要在进程中使用write,read函数对文件进行读写操作,首先是要告诉write,read函数,我们要操作的文件的fd是多少,因此fd对于文件来说,是起一个指向作用的,就好比进程的pid一样。
在这里插入图片描述

在这里插入图片描述

当我们使用open函数时,会打开一个在磁盘当中的文件,这个打开文件的操作,实际上是分成一下几步的:
(1)创建描述该文件的struct file
(2)生成该文件的对应的文件缓冲区
(3)将该文件对应的struct file,记录在task_struct的fd_array当中
(4)该文件的fd,实际上是在fd_array数组的下标。

如何验证这一点呢?还记得博主在上一篇文章提到的吗?每一个c/c++程序运行,都会默认打开三个文件流,分别是stdin,stdout,stderr。因此最先加载到fd_array的文件就是这三个。因此我们每一个进程,最初的fd_array都是这样的:
在这里插入图片描述
而被打开的文件对应的fd,实际上是它们在fd_array[N]的数组下标,因此stdin的fd为0,stdout的fd为1,stderr的fd为2,如果此时我们再让进程,打开log.txt文件,此时它就会被放在fd_array数组的3号下标处。因此其fd就是3.
在这里插入图片描述
不信?不信我们就来写一份代码验证一下。

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

int main()
{
    int fd1=open("log.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);//open的返回值,是被打开的文件的fd
    printf("stdin fd:%d\n",stdin->_fileno);//stdin->_fileno是stdin的fd,后者同理
    printf("stdout fd:%d\n",stdout->_fileno);
    printf("stderr fd:%d\n",stderr->_fileno);
    printf("log.txt fd:%d\n",fd1);
}

接着我们运行该程序,并且查看结果。
在这里插入图片描述
从打印的结果来看,证明了我们上面所言非虚,fd其实是被打开的文件在fd_array当中的数组下标。

重定向

这里给大家补充一个知识点,是关于printf()函数的,这printf谁不会啊?从刚开始学C语言的时候就用过了。不就是向显示器打印字符串嘛。

这里大家有没有想过,stdout是显示器的文件流,而stdout的fd是1,而printf函数是向显示器输出数据,也就是向stdout输出数据,实际上也就是向fd等于1的文件输出数据,如果我们先用close函数关闭fd为1的文件(stdout),接着打开log.txt,此时log.txt就会顺位变成fd为1的文件。那么此时printf会向什么文件进行写入呢?

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

int main()
{
    close(1);//关闭stdout
    open("log.txt",O_CREAT|O_WRONLY|O_TRUNC,0666);//log.txt会顺位变成fd为1
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    fflush(stdout);//将stdout的缓冲区刷新
    return 0;

}

接着我们运行该程序,此时我们惊讶的发现,printf竟然没有向屏幕打印"hello world",接着我们查看一下log.txt文件。可以发现printf将数据都输出到了log.txt当中。
在这里插入图片描述
这是因为,stdout其实就是fd为1的文件,如果我们先将fd为1的文件关闭,也就是将进程与显示器之间的输出流关闭,接着我们打开log.txt,而此时log.txt会顺位进入fd_array的1数组下标的位置,即fd为1的文件变成了log.txt。此时,stdout就从显示器,变成了log.txt(stdout并非显示器,而是fd为1的文件!!!只是默认是显示器)。而printf是向stdout写入数据,由于stdout流向了log.txt,于是printf就变成向log.txt写入数据了。

我们将这种文件流进行修改的操作,叫做文件重定向。而文件重定向当中又分为输出重定向,输入重定向,追加重定向,但是它们的原理都是差不多的,就是将文件的fd进行替换。详细的内容我们后面再讲。、

文件重定向的系统叫做dup系列,分别为duo,dup2,dup3。

#include <unistd.h>

int dup(int oldfd);
int dup2(int oldfd, int newfd);

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <fcntl.h>              /* Obtain O_* constant definitions */
#include <unistd.h>

int dup3(int oldfd, int newfd, int flags);

这里博主重点介绍dup2。dup2又两个参数,叫做newfd和oldfd,其中,newfd是被替换的文件的fd,oldfd是替换的文件的fd。比如我们想让log.txt替换掉fd为1的文件,我们就应该这么写:

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

int main()
{
    int fd= open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    dup2(fd,1);//fd替换掉1的文件

    printf("hello world\n");//向stdout写入
    fprintf(stdout,"hello byte\n");//向stdout写入
    const char str[]="hello code\n";
    fwrite(str,sizeof(str),1,stdout);//向stdout写入
    
    return 0;
}

接着我们运行该程序,并且查看log.txt文件的内容。
在这里插入图片描述
这里我们谈谈dup2函数的原理是什么,首先我们都知道,文件的fd其实就是被打开的struct file在fd_array的数组下标。而dup2函数则是将oldfd中的struct file,拷贝到newfd当中,其中newfd的原结构可能会被删除,而oldfd的原结构则会被保留。

在这里插入图片描述
从上图可以看到,其实替换的实质,是一个赋值操作,即将fd(3)号下标的struct file对象,赋值给fd_array的1号下标。原来的三号下标的指向的文件依然不变。但是如果某个文件在fd_array当中的指针个数变为了0,那么这个文件流就会被关闭。


http://www.kler.cn/a/385249.html

相关文章:

  • 基于 SSM(Spring + Spring MVC + MyBatis)框架构建电器网上订购系统
  • qt QFileSystemModel详解
  • Dify 本地部署指南
  • 【1个月速成Java】基于Android平台开发个人记账app学习日记——第7天,申请阿里云SMS短信服务SDK
  • 说说webpack proxy工作原理?为什么能解决跨域
  • K8S node节点没有相应的pod镜像运行故障处理办法
  • Hive 查询各类型专利 Top 10 申请人及对应的专利申请数
  • 记录offcanvas不能显示和关闭的修复方法
  • QT监控文件夹变化(文件增加、删除、改名)
  • B2C分销管理系统(源码+文档+部署+讲解)
  • C++20 STL CookBook 4:使用range在容器中创建view
  • c# 动态lambda实现二级过滤(多种参数类型)
  • 『VUE』21. 组件注册(详细图文注释)
  • Kubernetes时代的APM部署革新:基于Webhook的Agent动态注入
  • docker镜像文件导出导入
  • GPU服务器厂家:AI 赋能科学研究的创新突破
  • 1.每日SQL----2024/11/7
  • 计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-10-30
  • 为什么人工智能增强的威胁和法律不确定性成为风险主管最关心的问题
  • 5G智能对讲终端|北斗有源终端|北斗手持机|单兵|单北斗
  • Java | Leetcode Java题解之第543题二叉树的直径
  • 关于遥感影像BIL、BIP、BSQ你知道多少?给一个二进制文件你会读取嘛~
  • uniapp使用腾讯即时通讯IM(复制即可使用)
  • 小白初入Android_studio所遇到的坑以及怎么解决
  • Java I/O流面试之道
  • 【JavaScript】网络请求之Promise fetch Axios及异步处理