【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】

文章目录

  • 一、C文件接口中的那些事儿
  • 二、接口介绍
  • 三、文件描述符fd
  • 四、重定向
  • 五、缓冲区


一、C文件接口中的那些事儿

众所周知,Linux是用C语言写成的,那在这篇文章的开头,自然要先对C语言中的文件操作进行一个概括!

写文件:

直接看一个例子:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第1张图片

再创建一个对应的Makefile文件:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第2张图片

运行程序之前,当前路径下文件如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第3张图片

运行结果如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第4张图片

读文件:

【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第5张图片

执行结果如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第6张图片

注意,与写操作不同,当文件不存在时,读操作将失败!

上面所演示的写操作中,实际上是覆盖式的写操作,即每次向文件中写入时,都会先清空文件。与之对应的有追加写操作,即"a",在每次向文件中写入时,不清空文件原有内容,而是在后面接着写入。这里不再演示。

除此之外,C语言中还有格式化的输入和格式化的输出(fprintf、fscanf),用法大同小异,这里不再赘述。

总结一下C语言中对文件的操作
r: Open text file for reading.
The stream is positioned at the beginning of the file.
r+: Open for reading and writing.
The stream is positioned at the beginning of the file.
w: Truncate(缩短) file to zero length or create text file for writing.
The stream is positioned at the beginning of the file.
w+: Open for reading and writing.
The file is created if it does not exist, otherwise it is truncated.
The stream is positioned at the beginning of the file.
a: Open for appending (writing at end of file).
The file is created if it does not exist.
The stream is positioned at the end of the file.
a+: Open for reading and appending (writing at end of file).
The file is created if it does not exist. The initial file position
for reading is at the beginning of the file,
but output is always appended to the end of the file.


二、接口介绍

在C语言学习阶段,相信大家都知道一个概念:
C程序默认会打开三个输入输出流,分别是stdin,stdout和stderr(标准输入,标准输出和标准错误)
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第7张图片
可见,它们的类型都是FILE*,即它们都是C语言提供的文件。

而在体系结构层面看,上面三个流分别对应的硬件设备是键盘,显示器,显示器。也就是说,当向这三个文件中写入时,就是向对应的硬件设备中写入。
例:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第8张图片

结果如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第9张图片

也可以用输出重定向将信息写入一个具体的文件:
在这里插入图片描述

既然stdout和stderr所对应的硬件设备都是显示器,那么将上面代码中的stdout换成stderr所得到的结果是否相同呢?

答案是是的
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第10张图片
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第11张图片

但是,当用stderr向文件中重定向时,却不能将信息写入到文件中
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第12张图片

为什么会出现这种现象?

答:因为">"叫做输出重定向,只能把stdout中的内容重定向到其他文件中。同时也证明了stdout和stderr根本就不是一个东西。

实际上,fputs可以向一切文件或者硬件设备中写入信息。这也反映出一个本质----一切皆文件,包括硬件设备。

这里需要明确一个概念:操作者对一切文件的操作最终都要访问到硬件,而硬件的管理者是操作系统,所以,所有的语言上的对文件的操作都要贯穿操作系统,所以就不得不使用操作系统的调用接口。

下面来介绍几个接口:

open:

【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第13张图片

先看一个没有第三个参数的例子(在此之前,现将之前创建的file.txt删掉):
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第14张图片

关于O_WRONLY和O_CREAT具体是什么,后文中会给出解释。

结果如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第15张图片

可以看到,当不给定文件的权限属性即第三个参数时,新创建的文件的权限完全是乱的。

关于文件权限,之前的文章中已经说过,这里不再赘述,直接给一个设定了权限的例子:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第16张图片
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第17张图片

由此可见,当直接调用系统接口时,我们需要给出特定的文件权限。但我们使用C语言中的函数时,却不用关心这一点。这是因为C语言中的各种函数时操作系统的调用接口经过不同的封装而来的。

上述代码中的O_WRONLY和O_CREAT是什么?为什么要将它们进行按位或运算?第二个参数是int类型,这个值又跟它们按位或的结果有什么关系?

答:在操作系统中规定了一些像O_WRONLY这样的值为二进制表示中只有一个比特位为1的值,而第二个参数为"flag"顾名思义就是一个标志位。而我们通常的习惯是用1表示进行某操作,而用0表示不进行某操作。这里的O_WRONLY也是这样的思想。但与我们平常使用的标志位不同的是,由于这里一个参数可能表示要进行多种操作(如上面的例子),因此也需要多种标志。所以将不同的二进制表示中只有一个比特位为1的数进行按位或运算,得到的就是有多个比特位为1的值,如此就可以用一个值表示多种操作了

O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写

下面来介绍open函数的返回值:

对代码稍作改动:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第18张图片
结果如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第19张图片

显然现在还看不出来什么,也不知道返回值为什么是3,接下来再对代码稍作改动:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第20张图片

结果如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第21张图片
可以看到,这些返回值是连续的。
open函数返回值为-1时,表示打开文件失败,大于0时表示打开文件成功。但是上面的结果为什么是从3开始的?0、1、2去哪儿了?

答:其实0、1、2代表的分别是stdin、stdout和stderr。这些文件像数组一样整齐地排列着,而且0、1、2这三个文件是默认打开的。
具体的细节下文中详细介绍。

下面来捋清一个思路:
所有的文件操作实际上都是进程对文件的操作,而进程要读取文件就必须要把文件内容加载到内存中。可是通常一个进程会打开多个文件,也就是说进程和打开的文件之间的数量关系是1:n。

那么操作系统如何管理多个被打开的文件呢?

答:和PCB类似,操作系统会用一个结构体来存储每一个文件的相关信息,当要对文件进行操作时,只需要对结构体进行操作即可。


三、文件描述符fd

既然操作系统要对打开的文件进行管理,当OS中有多个进程运行,每一个进程又都打开了多个文件,那么如何区分每一个进程要操作哪些文件呢?

答:在PCB中,有一个结构体----struct files_struct,这个结构体中有一个指针数组----struct files* fd_array[],里面存储的是每一个被进程打开的文件的地址,这些文件地址按照下标从小到大排列在指针数组中。而每一个文件被打开时,都会创建一个结构体----struct file,指针数组中存储的就是这些结构体的地址。而进程默认会打开三个输入输出流----stdin、stdout和stderr,它们占据了数组中0、1、2的下标的位置。这也是上面例子中open返回值从3开始的原因。

【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第22张图片

文件描述符fd的本质就是数组下标!!!

如何理解Linux中“一切皆文件”???

答:在系统层面,为了管理硬件,file结构体里面的IO函数根据不同的硬件调用不同的驱动层面的IO函数,实现用文件的角度来管理所有硬件。

【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第23张图片
linux一切都可以是文件,并不是说实体是文件,而是以文件的方式去做管理,以文件的角度去看待硬件和软件。这是一种多态思想!

前面说过,文件描述符在管理文件的结构体中是以指针数组的数组下标的形式存在的,而且下标为1和2的指针指向的文件对应的硬件设备都是显示器。那么可不可以直接向1、2对应的文件中写入数据呢?

答案是可以的,如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第24张图片
结果如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第25张图片

再将其向2对应的文件中写入,所得结果是一样的,这里不再粘贴代码和结果。

同样的,文件描述符为0的文件对应的硬件设备是键盘,那么也可以用类似的方法从键盘中读取数据:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第26张图片

结果如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第27张图片
上面的代码中,关闭0和2的结果是一样的,那它们的区别是什么呢?

答:标准错误不能在输出重定向时不起作用。因为顾名思义输出重定向是对stdout起作用的。关于重定向,下文中给出解释。

文件描述符的分配规则:

前面用代码得出的现象是,进程每次打开的文件返回的fd是从3开始往后排序的,因为前面已经有三个标准输入输出流了。
那么如果把前面三个文件描述符对应的文件关掉一个,新打开的文件的文件描述符会有不同吗?

答案是会的,如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第28张图片
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第29张图片

而当我们关掉2所对应的文件后,执行结果如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第30张图片
所以我们不难得出一个结论:

给新文件分配的fd是从file_array[]数组中,最小的,没有被使用的数组下标


四、重定向

在上面的代码中,我们并没有关掉1所对应的文件,因为那是标准输出,若把它关掉了,最后就不会有任何东西显示到显示器上。那接下来再看这样一段代码:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第31张图片
结果如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第32张图片

结果显示,执行文件后确实没有任何东西输出到显示器中,但是file.txt中却出现了本该出现在显示器中的内容,为什么呢???

答:在上面的程序中,我们使用了printf,而printf的本质就是向stdout中写入数据,而stdout是一个FILE*类型的指针。在C语言阶段相信大家也都了解过,FILE是一个结构体,而这个结构体里就包含了打开的文件对应的fd。而在上面的代码中,我们将1对应的文件关闭,这是1这个文件描述符就被file.txt占有了。所以当printf去调用系统接口时,就自然向file.txt中写入了数据。这就是重定向

常见的重定向有:>, >>, <
>(输出重定向):

在这里插入图片描述
跟上面的代码相同,可以理解为将echo这个文件对应的fd给了file.txt,所以内容自然也就输出到后者了。

>>(追加重定向):

跟追加写入类似,就是重定向到一个文件中时,并不会清空该文件原有的内容,代码实现如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第33张图片
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第34张图片
操作符实现如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第35张图片

<<(输入重定向)

stdin对应的是显示器,输入重定向就是将指定文件的内容输出到显示器上。代码实现如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第36张图片
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第37张图片

操作符实现:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第38张图片

如何证明stdin、stdout和stderr的FILE结构体中包含文件描述符0、1、2?

在typedef struct _IO_FILE FILE; 在/usr/include/stdio.h这个文件中有个struct _IO_FILE结构体:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第39张图片
可见里面确实有文件描述符。
接下来再用代码证明一下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第40张图片

【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第41张图片

使用dup2系统调用:

如果我们每次进行重定向,都要先关闭一个文件,未免有点麻烦,所以介绍一下dup2这个接口:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第42张图片

代码如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第43张图片

结果如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第44张图片


五、缓冲区

在引入缓冲区概念之前,先看一段代码:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第45张图片
结果如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第46张图片

结果显示,最后显示器和file.txt文件中都没有任何数据。
按照前面的思路,显示器中没有数据是因为将原本应该输出到显示器中的数据重定向到了file.txt中。
可是,file.txt中也没有写入数据,为什么呢?

接下来解释缓冲区的概念:
前面说到,FILE这个结构体中包含了文件描述符_fileno。除此之外,其中还包含了C语言中的文件缓冲区。而我们向文件中写入数据时,这些数据会被拷贝到C语言中的文件缓冲区
又因为OS的层状结构,用户对硬件设备进行操作都需要经过OS的系统调用接口。
当程序接收到某种信号后,系统调用接口会将C语言中的缓冲区中的内容加载到OS的内核缓冲区,然后再加载到对应的硬件外设中。

而用户和OS进行交涉时的缓冲方法有以下三种:
①无缓冲
②行缓冲(遇到\n就进行缓冲区刷新)比如向显示器中写入
③全缓冲(当缓冲区存满或进程退出时进行缓冲区刷新)比如向文件中写入

而上面代码中,1所对应的显示器本该是行缓冲,但我们将其重定向到了fd,也就是将其变为向文件中写入,对应的缓冲方法也就变成了全缓冲。这些数据本该在进程退出后刷新到文件中,但我们在代码的最后关闭了文件对应的fd,这也就意味着OS并没有来得及将C语言中的缓冲区中的内容加载到OS的内核缓冲区中,自然也就没有刷新缓冲区,自然也就没有写入。

将最后的close去掉之后结果如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第47张图片

下面再看一段代码:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第48张图片
然后将其重定向到file.txt中,结果如下:
【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】_第49张图片

可以看到,C语言接口中的数据都出现了两次,但是系统接口只出现一次,为什么呢?

因为fork创建了子进程,发生了写时拷贝,导致重定向时,写入了两次数据。而系统接口与C语言接口不同,它不用将用户层面的缓冲区中的数据加载到OS的内核缓冲区,所以只出现一次。

补充:若要强制刷新缓冲区,可在代码结束之前使用fflush()函数,这里不再演示了。

你可能感兴趣的:(Linux重难点,linux,服务器,运维,c语言,c++)