1. 文件描述符
1.1 文件描述符是什么
什么是文件描述符呢?
我们先来看之前所介绍的系统级别的文件操作函数open
:
在上图中,我们可以看到open
有一个整型的返回值,而这个返回值,实际上就是文件描述符。
在Linux中,由进程打开文件,研究这些进程打开的文件,本质是研究文件与相应进程的关系。所以,在Linux中,会存在一个结构体用以描述文件:struct file
。每一个被打开的文件,都会对应有一个struct file
,这些struct file
会被像双链表一样链接起来,就如task_struct
一样。
而文件是由相应进程打开的,因此在描述进程的task_struct
中,肯定要有记录其打开文件的变量,如下所示:
files
这个指针指向一个files_struct
的结构体,而在这个结构体中,存在一个指针数组,而这个指针数组中存放的就是一些struct file*
类型的变量,因此进程可以通过数组中存储的指针,找到其所打开的文件。
整体的关系可以如上图所示。
因此,文件描述符实质上就是上图中file* fd_array[]
这个数组的下标。需要说明的是,操作系统识别进程打开的文件,是只通过这个文件描述符,即fd来识别的。
C语言中的FILE
结构体用以描述文件,本质上是对struct file
的又一层封装,其中会存在文件描述符。
1.2 文件描述符如何分配
既然文件描述符就是数组的下标,那么文件描述符如何分派呢?
我们来看下面的测试程序:
上述程序的输出结果为:3
。
这有点奇怪,数组下标不都是从0开始的吗?这说明,0,1,2下标处,肯定对应的是别的文件。
实际上,一个进程启动时,会默认打开三个文件:标准输入stdin
,标准输出stdout
和标准错误 stderr
。这三个文件,分别对应的就是数组下标0,1,2。
实际上,文件描述符是这样来分配的:返回从数组下标0开始,往后找到的第一个为空的数组下标处,即作为相应的文件描述符。
我们可以来测试一下,将标准输入文件关闭掉,然后再打开一个文件,此时这个文件的文件描述符应为0。
我们同样可以验证,进程启动时会默认打开stdin stdout stderr
这三个文件,并且分配文件描述符0,1,2.
上述程序的输出结果为:
特别地,在上述代码中,我们拿到这三个文件的文件描述符是通过C语言中FILE这个结构体得到的,而在Linux中,文件描述的结构体是struct file,由此可见,C语言不仅对操作系统的接口做了封装,对操作系统的数据结构也会做封装。
2 重定向
什么是重定向呢?
正常情况下,我们输入是从标准输入中读取,而输出则是向标准输出中输出。重定向的核心就在于,使得输入不再从标准输入中读,输出不再向标准输出中输出。
2.1 输出重定向
我们先来看下面的示例:
我们来看上述程序的运行结果:
很奇怪,并没有显示出我们想要打印出的字符串。
printf函数默认是向标准输出中打印,实质上,我们前面讲过,操作系统层面,识别进程打开的文件,仅通过文件描述符实现。C语言中的printf本质是对系统调用write的封装,而write写入到哪里,正是通过文件描述符进行判定的。
因此,printf实质上是对该进程中,文件描述符为1的文件中写入,虽然我们先关闭了标准输出文件,但是新打开的文件,自动分配了文件描述符1,因此此时就会向这个新打开的文件中写入了。
我们查看一下log.txt中的结果,进行验证:
2.2 输入重定向
输入重定向与输出重定向是类似的。
C语言中的scanf默认是从标准输入中读取,实际上是从文件描述符为0的文件中读取。
我们通过下述代码,实现输入重定向:
log.txt中的内容为hello linux,所以输入重定向读取一行字符串内容后,最终输出的结果也应为hello linux。
最终输出结果如下所示:
2.3 使用dup2进行重定向
重点关注上述的dup2函数,这是一个可以实现重定向的系统调用。其原理是,让 newfd 变为oldfd 的拷贝。
比如说,我们要实现输出重定向,如果使用dup2系统调用,就不用先关掉标准输出文件,而是直接让原本存储标准输出文件的下标1处,变为存储我们要重定向到的文件。
以下,是使用dup2进行输出重定向和输入重定向的代码示例:
输出重定向:
输入重定向:
3. 文件、父子进程和进程替换
我们知道,进程打开文件,进程与文件之间是存在紧密联系的。
父进程创建子进程,子进程会继承父进程的代码和数据,子进程的task_struct也几乎是对父进程的拷贝,那么对于父进程打开的文件,子进程如何看待呢?
子进程会继承父进程打开的文件,即一个文件会对应多个进程,某文件在父进程中是打开的,那么这个文件在相应的子进程中也是打开的。
并且,在描述文件的结构体内部,存在一个引用计数,当一个文件对应多个进程时,引用计数为所对应的进程数,当一个进程关闭该文件时,引用计数便会减去1,直到引用计数为0时,该文件才会真正关闭。这也是进程具有独立性的一种体现——一个进程关闭某文件,并不会影响另一个进程对该文件的打开。
那么,进程替换中,会影响进程打开的文件吗?
答案是,不会的。进程替换,实质上并未新创建进程,而是对当前进程的代码和数据进行替换,主要更改的是进程地址空间、页表和物理内存这三者中相关映射,而进程task_struct中的其余内容并未有什么变化。
因此,某个进程在进程替换前有怎样的文件关系,在进程替换后,依然又怎样的文件关系,这是不会发生变化的。