目录
[2. 输出重定向](#2. 输出重定向)
一、共识原理
1.文件=内容 + 属性
我们关注文件,不仅要关注它的内容,也要关心它的属性,一个文件即使没有内容,它的大小也不是空的,因为该文件的属性也是会占用空间的。
2.文件分为打开的文件和没打开的文件
3.打开的文件
谁在打开文件?我们在代码中写一个fopen,fwrite,最终都会变成进程,因此是进程在打开文件。
研究打开的文件---本质是研究进程和文件的关系!文件被打开,必须被加载到内存!内容和属性都被加载到内存。
进程:打开的文件 = 1 : n。一个进程是可以打开多个文件的,因此进程和打开的文件关系是1:n的
操作系统内部,一定存在大量的被打开的文件!OS要不要管理这些打开的文件呢?-怎么管理???-先描述在组织,在内核中,一个被打开的文件都必须有自己的文件打开对象,包含文件的很多属性。
4.没打开的文件:在哪里放着呢?在磁盘上。我们最关注什么问题?没有被打开的文件非常多,我怎么找到我要打开的文件?因此文件必须被分门别类的归置好---方便我们快速的进行增删查改---快速找到文件。
二、回顾C语言文件函数
1.fopen
fopen的第一个参数是文件名,如果不带路径,默认就在当前路径下打开,如果带了绝对路径,就在绝对路径下打开,第二个参数是以什么方式打开,是读呢?还是写呢?还是追加写?下面我们打开了"log.txt"这个文件,在当前路径下是没有log.txt这个文件的,fopen如果打开不存在的文件会新建在打开,是以读方式打开。果然在我们的当前路径下创建了一个log.txt文件。
下面的问题是,当前路径,什么是当前路径呢?当前路径就是进程的当前路径,如果我们把进程的当前路径修改了,是不是就可以把文件新建到其它目录下呢?那怎么修改进程的当前路径呢?chdir。
可以看到,当把 进程的当前工作目录修改了之后,创建的文件log.txt的路径也随之发生了变化。
2.fwrite
fwrite的第一个参数是写入内容的起始地址,第二个参数是写多少个,第三个是当做一个几部分写入,第四个是要写入那个文件里。
这里我们直接把message当做一个整体写入到fp里,也就是log.txt文件里,这里有一个问题就是strlen(message)需要+1吗?strlen求字符串长度,求到'\0'就结束了,也就是说如果+1就是把'\0'也写入到文件里,这里需要吗?答案是不需要,因为'\0'是C语言的要求,C语言不知道字符串从哪里结束才要'\0',但是和我文件有啥关系?
运行一下,在查看log.txt里的内容,发现果然hello world被写入到文件中了。
下面我们给log.txt里面多加几个2字符,然后在运行一下。
我们发现原来的内容全部没有了,这说明"w"方式,在写入之前,都会对文件进行清空处理!然后再从头开始写。
这个>重定向,是不是就是把log.txt打开,然后以w的方式把"hello world" 写进去呀?因此,我们>前面不加任何东西就是以w方式打开log.txt文件但是不写任何内容,此时就把log.txt清空了。
那如果我们就想要追加写呢?可以以"a"的方式打开,a在文件结尾,追加写。
3.fclose
如果我们把一个文件使用完毕了,就需要使用fclose关闭一下这个文件。它的使用非常简单,把打开的文件指针传进去即可。
下面就有一个问题了,文件是在磁盘上的,磁盘是外部设备,我们上述的fwrite,fopen,包括fclose,其实实在访问硬件,那我们用户能直接访问硬件吗?不能,操作系统不相信任何人,我们要访问硬件,必须通过操作系统提供的系统调用,因此,我们上述写的库函数,一定要封装系统调用!什么printf/fscanf/fwrite/fread......这些库函数,都是封装了系统调用的。下面我们就学习一下这些文件相关的系统调用。
三、文件系统调用
1.open
给我们提供了两个打开文件的系统调用,我们只要学会下面那个参数多的即可,参数少的是参数多的一子集。
第一个参数是要打开的文件路径,如果没有带路径,默认就是进程的当前路径。第二个参数是以什么方式打开,第三个参数是文件的权限,可以设置创建文件的权限。
返回值如果失败返回<0的数,成功,返回>0的数。
我们先来以只读方式打开,只读方式打开传递O_WRONLY这个宏即可。
我们发现我们直接打开文件失败了,这是为啥呢?这是因为如果打开的文件不存在,并不会给新建,因此此时要在传递一个宏O_CREAT, 表示如果文件不存在就创建。此时我们可以看到就创建了log.txt文件。
但是这里细心的同学会发现有问题,就是,我明明文件权限设置的是666呀,文件权限应该是rw-rw-rw,但这里却是rw-rw-r--,不是666而是664,这是因为权限掩码。如果你说我就要创建权限是666的文件呢?可以,有设置掩码的函数umask,直接把掩码设成0即可。
题外话:open的第二给参数是一个整数呀,可是我们给他传递了O_WRONLY 和O_CREAT两个选项,是咋做到的呢? 其实就是简单的位运算。
cpp
#include "stdio.h"
#include "string.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#define ONE (1 << 0) //1
#define TWO (1 << 1) //2
#define THREE (1 << 2)//4
#define FOUR (1 << 3) // 8
void show(int flags)
{
if(flags & ONE) printf("hello funcion1\n");
if(flags & TWO) printf("hello funcion2\n");
if(flags & THREE) printf("hello funcion3\n");
if(flags & FOUR) printf("hello funcion4\n");
}
int main()
{
show(ONE);
printf("----------------------------\n");
show(TWO);
printf("----------------------------\n");
show(ONE | TWO);
printf("----------------------------\n");
show(TWO | THREE);
printf("----------------------------\n");
show(ONE | THREE | FOUR);
printf("----------------------------\n");
return 0;
}
通过位运算,我们就通过给1给标记位传递不同的值,让它表示多种信息。
2.write
第一个参数,就是对应文件的id,就是open的返回值,第二个参数是要写入的起始位置,第三个参数是写入的长度。
下面运行一下,看下结果。我们运行了两次,打印出来的都是一样的,之后我手动往log.txt里面加了一串b,在运行,发现,那一串b并没有被清空,因此,我们可以发现写入的时候是从头覆盖写的,但是并不会对文件做清空处理。
那如果我也想做到清空呢? 我们需要在打开文件的时候在加一个宏,**O_TRUNC,**截断的意思,就是每次打开文件都做清空。
如果我想做到如同C语言中的a一样,追加写呢?还有一个宏,O_APPEND 。如果带了O_APPEND 就要把O_TRUNC 去掉**,**追加和清空是冲突的。
我们现在用的都是系统调用,fopen,就是用open封装的,"w"方式就会被转化成O_WRONLY | O_CAEAT | O_TRUNC.
这都没问题,但是有一个东西我们一直没谈,就是open的返回值可是一个int啊,但是fopen的返回值是一个FILE类型的指针,这两个玩意八竿子都打不着,有啥关系呢?下面我们就要谈谈文件的管理了。
3.访问文件的本质
操作系统里会有很多个进程,每个进程可能要打开很多个文件,这些被打开的文件要不要被被管理起来呢?要,先描述在组织。操作系统用struct file描述一个被打开的文件信息 ,struct file里应该包含什么呢?1.文件在磁盘的什么位置 2.文件的基本属性(权限,大小,读写位置,谁打开的...)3.文件的内核缓冲区 ,总之,这个结构体里包含了文件的大部分信息,类似的,struct file里还有一个strcut* next指针,每打开一个文件,内核创建一个struct file,然后用strcut* next指针链接到一起,此时操作系统要对文件进行增删查改就是对文件链表的增删查改,如果要添加一个文件,就在文件链表里插入,如果要关闭文件,就是把文件的所有属性释放掉,从链表删除,再把数据刷新到磁盘上。
一个进程可能打开多个文件,那些文件是被那个进程打开的呢?我怎么知道,所以,必然要建立进程PCB和打开文件struct file的对应关系。
那怎么建立的呢?在进程PCB里会存在一个指针,struct file_struct *f; 这个指针指向struct file_struct 结构体**,**这个结构体里面会包含一个数组,数组的名字叫做 struct file* fd_arrdy[],这个数组显然是个指针数组,这个数组的下标从0开始,数组每个元素的类型是 struct file*,所以,当我们打开一个文件的时候,操作系统会创建好struct file,然后在这个数组里分配一个下标,把创建好的struct file的地址,填到这个下标上,以后,每个进程就可以根据这个文件描述符表,就能把打开的文件找到。
所以,为啥open的返回值是个整数呢?open会创建一个struct file,然后在当前进程的文件描述符表里找一个没有用过的下标,把创建的struct file的地址填进去,然后把这个数组下标返回给用户,因此,这个int本质就是一个数组的下标。
所以,在我们写的时候,必须得把这个数组的下标传进去,进程通过指针找到文件描述符表,然后在通过这个数组下标,索引到文件的地址,从而往该文件写入。
文件和进程产生关联是通过数组下标关联的,这样就可以做到文件和进程的解耦。
这个文件描述符可还没见过呢,下面我们看看文件描述符是几呢?
我们可以看到是3, 下面我们多打开几个文件看看。
可以看到,是连续的整数。但是问题来了,0、1、2哪里去了呢?
4.stdin&&stdout&&stderror
C程序在默认启动的时候,会打开三个标准输入输出流(文件):stdin(键盘文件),stdout(显示器文件),stderr(显示器文件)。所以我们在打印的时候为啥要包含stdio.h呢?std就是标准的意思,io就是输入输出,是C语言会打开吗?任何语言都会打开这三个文件,这不是C语言的特性,是操作系统的特性,电脑在打开的时候,键盘和显示器文件默认就会打开,进程只需要把打开键盘,显示器文件的地址填入即可。因此0、1、2是被这3个家伙占着呢?怎么验证呢?
我们直接用write往1和2里面写入。1和2是显示器文件哦,直接打印出来了。
下面用read接口验证一下0号,键盘文件。
为啥卡住了呢?因为0是键盘文件,在等待键盘就绪!
现在问题又来了哦,可C语言的返回值是FILE指针啊,这和int有啥关系呢?FILE是C语言的内置类型吗?不是,FILE是C库里封装的一个结构体,这个结构体里面一定包含了该文件的数组下标,下面验证一下。
5.文件的引用计数
关闭文件的系统调用是close,现在我们把下标为1的文件stdout关闭。然后打印了stdout和stderr的fileno,fprintf的用法和printf基本一致,只不过前面加了一个文件描述符而已。我们可以看到,printf没有打印在显示器上打印出来,这肯定和我们关闭close有关,因此,printf底层肯定访问了stdout显示器文件,然后我们把stdout关闭,因此在屏幕上就打印不出来了。但是为啥stderr文件能打印出来呢?stdout和stderr都指向显示器文件,因此显示器文件的引用计数就是2,如果再来一个指向显示器,引用计数就会继续增加。当把stdout关闭,引用计数--变成了1,因此stderr还是能打印出来,关闭文件是把该下标的地址填成NULL。
printf也是有返回值的,其实stdout已经关闭了,但printf以为自己打印成功了,因此就把打印的字符个数13返回了过来。
四、重定向
1.文件描述符的分配规则
我们把2号文件描述符关闭之后,新创建的文件描述符就是2,因此我们可以得知,文件描述符的分配规则就是从0下标开始,寻找最小的没有被使用的数组位置,它的下标就是新的文件描述符。
2. 输出重定向
下面我们把2号文件描述符关闭,然后创建一个文件fd,之后调用write往1号文件里写入5次。
1号文件描述符对应的显示器文件,因此我们把内容写入到了显示器上。
下面我们把1号文件描述符关闭,然后创建一个文件fd,之后调用write往1号文件里写入5次。
我们发现,本来应该向1号文件描述符也就是显示器写入的信息,居然写到了 log.txt里,这是因为我们把1号文件描述符关闭了,然后又创建了一个文件,这个文件根据分配规则,就分配到了1号描述符,然后我们往1号描述符里面写,就写到了文件里。**本来应该往显示器写却写入到了文件里,这就叫输出重定向。**这里我们可以画张图理解一下。
这样写不是不可以,但是要先关一次,然后在打开一个文件,当别人问你为啥这么做的时候,你就要和别人解释半天,有没有一写系统调用能帮我们做这件事呢?打开文件就行了,然后重定向调用函数就行,有这样的接口吗?是有的。
3.重定向系统调用
是有dup,dup2,dup3系统调用的,常用的就dup2,因此我们详细谈谈dup2,dup就是duplicate,复制的意思,参数是2个文件描述符,一个旧的文件描述符,一个新的文件描述符。
那么问题来了,是把旧的文件描述符内容拷贝给新的文件描述符内容,还是新的文件描述符内容拷贝给旧的文件描述符内容呢?这样说吧,dup2之后,2个文件描述符内容全都变成newfd的还是oldfd的?常理来看应该是全都变成newfd吧,但实际结果是全都变成oldfd,这里挺奇怪的是吧,也不懂老外为啥这样起名字。因此如果我们要让1号文件描述符内容是新建的文件描述符fd的内容,要怎么传参呢?就要dup2(fd, 1)这样传参。下面我们来使用一下。
我们用dup2就实现了同样的效果。 上面的代码忘记close了,要记得close文件。
4.追加重定向
上面的代码中,文件是O_APPEND方式打开,我们多运行几次,这个log.txt就会越来越大,这就叫追加重定向。
5
5.输入重定向
read的第一个参数是文件描述符,要读哪个文件,第二个参数是读到哪,第三个是读多少个字节,返回值是实际读了多少个字节。
下面我们读取文件方式改为只读方式,然后读取0号文件,也就是键盘文件,阻塞住了,我们往键盘输入内容,然后回显出来了。
下面我们把0号重定向一下。
我们可以看到本来应该从键盘文件标准输入变读取,变为了从指定的文件读取,这就叫输入重定向。
重定向的本质,就是在内核里对文件的地址做拷贝。
6.1号VS2号
我们直接往1号和2号文件描述符里打印,运行,可以看到没问题,都打印出来了。但是当 ./myfile > normal.txt,也就是把1、2号文件的打印输出重定向到normal.txt文件的时候,为啥2号文件的内容没有重定向到nomral.txt里呢?为啥cat只能看到1号文件描述符重定向的内容呢?这是因为>是把1号显示器的内容重定向到了文件里,和我2号文件描述符有啥关系?
如果我想把正常消息打印到一个文件,错误消息打印到一个文件,该咋办呢?可以进行下图中的操作,这里其实非常直观,就是把1号文件的内容重定向到normal.log,2号文件的内容重定向到err.log里。默认不写的话是把1号重定向到文件。下面就多出来了err.log文件。
那如果我要把1、2的内容重定向到一个文件里咋办呢?
默认不写就是1号文件重定向到all.log文件,指令是从左往右执行的,&1(取地址1)的意思就是把1号文件的内容写入到2号文件里,因为左边的指令已经执行完了,1号文件的地址已经是all.log的地址了,然后把1号文件内容拷贝给2,此时1和2都指向了all.log文件。最后就能都写入到all.log里了。