引言:
北京时间:2023/3/28/7:19,周二,早八的一天,难过!终于进入C站周创作榜啦!开心!给大家推荐一首歌《盛夏的果实》,给我的感觉非常的放松,劳逸结合,音乐非它莫属,为了下周周榜可以继续前进,今天我们就来学习一下基础IO的知识吧!
回顾重定向
从上篇博客,我们明白,重定向不过只是关闭在文件描述符表下标为0 1 2 默认打开的标准输入、标准输出和标准错误,然后把新文件打开,然后占用文件描述符表中的0 1 2 下标,因为上层并不在乎下标中的文件对象地址是否是标准输入等,而是直接向该下标中的文件对象地址写入;并且我们要明白 ,标准输入文件本质上指向的就是我们的键盘,标准输出和标准错误本质上指向的是显示器, 所以标准输出和标准错误都应该是往显示器打印的,但是如下图所示:
我们可以发现,当我们使用了输出重定向(“>”),此时它会把标准输出文件关闭,然后使用新打开的文件占用原标准输出的在文件描述符表中的下标处的指针指向的地址,所以可以很好的看到,当我们运行可执行程序的时候,此时向标准输出打印的内容已经看不到了,只能看到向标准错误中打印的内容,这也就表示,stdout,cout 都是往文件描述符下标为1对应的文件对象打印,stderr,cerr 都是往文件描述符下标为2对应的文件对象打印,并且此时输出重定向改的只是1下标对应的指向,并不会影响2号,所以导致2下标继续向显示器打印,因为上述的本质,标准输出和标准错误本质上都是向显示器打印,但是使用输出重定向并不会影响下标2,所以不影响向标准错误中打印的数据,所以在我们的显示器上依然可以看到标准错误需要打印的数据,但是向标准输出中打印的数据却不能在显示器上看到,而是只能在重定向的新文件中看到,也就是log.txt中看到了,因为重定向导致此时的标准输出文件地址变成了log.txt。
所以通过上述的知识,我们就可以知道,标准输出和标准错误本质上都是向显示器打印,只是路径有一些不同而已,标准输出是向文件描述符表下标为1处指针指向的文件对象写入,而标准错误是向文件描述符表下标为2处指针指向的文件对象写入,所以此时如果我们有一个需求,就是将一个程序中的正常信息,打印到logNormal.txt中,把错误消息打印到logError.txt当中,目的就是为了区别错误信息和正常信息此时应该怎么办呢?
基本思路:
将文件描述符表中的下标1和2对应的标准输出和标准错误文件给关闭,然后打开我们想要接收信息的文件,此时程序对应的错误信息和正常信息就可以很好的打印到对应的文件之中了,具体代码如下:
所以使用上述的方法,就可以很好的把文件中的不同类型的信息给区分开来,当然在实际中,并不需要这么麻烦,因为在Linux指令中,Linux系统已经为我们提供了重定向的接口("<",">"),我们就可以直接使用从定向的接口,来完成相应的操作,让系统自己帮我们去关闭对应的文件描述符表中的对应的下标指针指向的文件,指令:./a.out 1>logNormal.txt 2>logErrno.txt ,表示的也就是上图中的功能,本质上就是系统对我们上图所示代码通过 "<",">" 做了一定的封装效果而已,不管怎样都还是要去关闭标准输入、标准输出或者标准错误,然后打开对应的新文件,因为原理就是这样如此!
注意: ./a.out > log.txt 2>&1 意思是将标准错误和标准输出放到同一个文件中,意思:2重定向为1的地址
重定向接口(dup2)
明白了上述,此时发现像我们以前的写法,先关闭标准输入、标准输出或者标准错误文件,然后再打开一个新文件,这样的写法是比较麻烦的,所以我们可以直接使用dup2这样的重定向函数,使用方式:int dup2(int oldfd,int newfd);,但是值得我们注意的是,此时需要把newfd和oldfd给区分一下,如下图:
按照使用原理中的解释,表明newfd
是oldfd
的一份拷贝,并且因为我们是需要把新文件的fd
去替换到标准输出文件(1),所以可以知道,最后剩下的肯定是新文件的fd
,所以明白新文件的fd就是oldfd
,具体使用如下图所示:
深入缓冲区
在上篇博客中,在讲一切皆文件的时候,我们已经浅浅的认识了一下缓冲区,所以我们承接以前的内容,来深入一下缓冲区,来看看缓冲区到底有什么用呢?
如上图,我们知道,在 struct file 文件对象中,是存在着缓冲区的概念的,并且明白,当操作系统需要写数据到相应的外设中,那么此时第一时间就是将数据拷贝到文件对象的文件缓冲区中,然后让文件对象中的相关函数指针去和外设的驱动程序进行交互,最终把文件对象中的文件缓冲区中的数据按照缓冲区对应的刷洗策略给刷新到外设中 ,所以让我们按照这个原理,来深入认识一下文件缓冲区吧!
首先我们看一个现象,这个现象可以很好的证明缓冲区的刷新策略等知识!
如下图:
高能知识点:综合性非常高
要把FILE结构体和文件对象(struct file)搞明白,就要知道 struct FILE ,并不等于是 struct file ,其中,struct file是操作系统为了更好的管理文件,创建的文件对象,所以本质上,该结构体是属于系统级别的结构体,而struct FILE是用户调用系统调用接口,自己封装出来的一个结构体,本质上是属于用户级别,例:封装一个C库,然后使用C库中的库函数fprintf函数来调系统调用接口 write ,此时调用C库中的fprintf函数,它的第一步不是直接去调用系统调用,而是先malloc一个FILE结构体,因为如我们之前所说,在FILE结构体中才拥有指向文件描述符表的指针和缓冲区,所以fprintf函数想要调用write函数,前提一定是要有fd(通过指向文件描述符表的那个指针获得),其次是要有缓冲区,也就等于fprintf函数一定要有FILE结构体,然后才可以去调用write接口,并且调用write接口时,也是把FILE结构体中的fd文件描述符通过传参的形式传递给它,然后系统调用接口write才根据文件描述符找到对应的文件对象(struct file),最终把需要写入的数据拷贝到struct file文件对象对应的缓冲区中(该缓冲区目的:提高写入效率),并且根据文件对象中指向外设驱动程序的读写接口的函数指针实现将数据写入相应的外设
大致关系: C库( fprintf()->FILE->struct FILE(fd),缓冲区),表明缓冲区就是在C库中的,并且C库文件结构中,最主要的就是struct FILE结构体,因为该结构体中有缓冲区和指向文件描述符表的指针,也就是有fd,所以当我们在调用fprintf打印数据的时候,此时这个被打印的数据,并不是直接通过系统调用访问操作系统,然后找到文件描述符,然后找到对应的文件对象,然后通过文件对象写入到外设(显示器),而是只是把数据放到了对应的缓冲区当中,然后此时C库就会根据对应的缓冲区的刷新策略把缓冲区中的数据刷新到外设中(具体如何刷新到外设的)原理:调用系统调用接口write,然后根据FILE中的指向文件描述符表的指针,找到文件对象的fd,然后通过文件对象,将数据写入外设,(再次强调,FILE结构体不是文件对象结构体,虽然二者都有缓冲区,但是后者的缓冲区目的只是为了提高效率)
注意点: 就是fd是文件描述符,并不是指向文件描述符表的指针,所以此时跟C库是没什么关系的,本质上fd就是在FILE结构中的,原因,只有FILE结构中有指向文件描述符表的指针,所以FILE结构体就可以通过这个指针拿到对应的fd
C库中缓冲区的刷新策略:
(1.)全缓冲:在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。全缓冲的典型代表是 对磁盘文件的读写 。
(2.)行缓冲:在这种情况下,当在输入和输出中遇到换行符时,执行真正的I/O操作。这时,我们输入的字符先存放在缓冲区,等按下回车键换行时才进行实际的I/O操作。典型代表是 标准输入(stdin) 和 标准输出(stdout) 。
(3.)无缓冲:也就是不进行缓冲,标准出错情况stderr是典型代表,这使得出错信息可以直接尽快地显示出来
所以此时可以明白,标准输入、标准输出的刷新策略是行缓冲,标准错误的刷新策略是无缓冲,普通文件采用的刷新策略是全缓冲
缓冲区最重要的意义:节省调用者的时间,本质因为系统调用需要花费时间
明白了上述的知识,所以此时关于缓冲区的第一个问题(也就是上图),就可以得到解释了,问题:为什么向标准输出中打印是两条语句,而向重定向文件中打印就是三条语句呢?原因,就是缓冲区的刷新策略,并且如果使用write接口,那么操作系统此时是直接将数据写入到文件对象中,但是如果使用的是fprintf,此时根据C库中FILE结构体中的缓冲区,此时就需要根据该缓冲区的刷新策略来把该数据刷新到文件对象中,并且此时因为,我们是向标准输出中打印,并且带了 \n ,所以此时根据C库缓冲区的原则,stdout是使用行缓冲(前提带 \n ),所以此时该fprintf函数打印的数据符合要求,所以直接就被写入到了文件对象中,然后我们再调用fork()函数创建子进程,那么此时就会因为接口都调用完了,数据也被写入完了,所以fork()函数没有现象,但是如果你是向一个重定义文件中写入,那么此时就会因为,C库中缓冲区对于普通文件的刷新策略是全缓冲(就是等buff数组满了再刷新到文件对象中),write因为是系统接口,所以同理,会直接将数据写入到文件对象,所以此时因为fprintf中的数据因为全缓冲,不能写入文件对象,所以当调用fork()函数创建了一个子进程之后,该进程也会执行对应的打印,所以导致最后出现了三条语句。
综上:缓冲区的设计非常的高级!
北京时间:2023/3/29/0:01