文件描述符
背景补充
- 文件=内容+属性,属性又被称为元数据
- 访问一个文件,必须把对应的文件打开,需要将其加载到内存中
- 如果一个文件没有被打开,那就说明其在磁盘上
- 是谁打开文件呢?用户通过bash启动进程来打开文件,在C语言中有着
fopen函数进行打开文件,然而操作系统是软硬件资源的管理者,所以fopen必然会通过操作系统来打开文件 - 所以操作系统内一定存在大量被打开的文件,如此多的文件必定需要被管理,管理的方式就如同前文所说的一样:先描述再组织 ,那么一定存在一种数据结构来描述文件,就如同
PCB一样 - 进程有
task_struct,进程也会打开文件,所以研究文件就是研究进程与文件的关系
C语言库中的文件操作

打开文件必须要知道文件的路径+文件名,这就是为什么进程有cwd的原因之一,cwd意味当前工作目录,当只添加了文件名时,会在默认路径寻找该文件,默认路径就是cwd,如果用chdir将当前路径更改了,那么默认路径也会跟着改变
默认打开的文件
进程启动时默认打开三个输入输出流,分别是stdin,stdout,stderr,分别对应标准输入,标准输出,标准错误,其实就是打开三个文件,分别是键盘文件,显示器文件,显示器文件。
本质上向显示器打印就是向stdout文件写入,因为stdout也是FILE*类型。
文件的系统调用

open函数在man手册的第二页,说明其是系统调用,pathname就是要打开的文件路径+文件名,flags表示打开文件的方式,mode表示新建一个文件时所设置的权限。
打开文件的方式是通过位图标记的形式,以下的宏是一个数字,只有一个比特位是1。

系统调用与C语音库函数的最大区别就是返回值,open返回值是文件描述符 ,

当我们使用C语言以'w''打开文件时,代码为fopen("log.txt",'w');用系统调用的函数代码为int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);如果是'a'那只用将O_TRUNC换为O_APPEND
为什么需要进行封装系统调用呢?不同系统的系统调用肯定是不一样的,通过语言将所有系统的系统调用进行封装后,就具有可移植性与跨平台性了,一旦有了这些性质,语言的使用者与竞争力就会大大增强了。如何做到跨平台性呢?对于C/C++这种编译性语言,采用条件编译的方式封装多种操作系统的系统调用。
文件描述符
文件描述符就是字面意思,是描述一个文件的,不论上层语言封装后是FILE*还是其他的,操作系统只认文件描述符fd,进行打印打开文件的fd,发现是从3开始的。
如同背景所说的一样,操作系统内部存在着多个被打开的文件,自然也需要管理,会通过双链表形式去管理struct file,其中直接或间接包含文件的属性与内容,在task_struct内一定会有一个文件描述符表的指针来指向文件描述符表,在文件描述符表的内部也一定会有一个指针数组,这些指针数组存储的是指向struct file的指针,数组的下标就是文件描述符

那么既然fd是数组下标,那么为什么是从3开始的呢?数组的0、1、2又是什么呢?
如前面所说,进程会默认打开标准输入、标准输出、标准错误,分别对应的就是0、1、2。
文件描述符的分配规则
众所周知,文件=内容+属性,磁盘里的文本当需要被读取时,操作系统会将属性与内容加载到内存中,内容在文件内核缓冲区里,struct_file中会存着指向属性与内容的指针,所以write函数根本就不是写入到文件中,本质是拷贝函数,把数据从用户空间拷贝到对应的文件内核缓冲区中,至于什么时候写入到磁盘文件,有OS自行决定。
文件描述符的分配规则为:给新打开的文件分配fd,从文件描述符表数组中寻找最小的没有被使用的下标。
如果将0关闭,那么再次打开一个文件,该文件的fd就是0,而由于上层是将0作为标准输入,那么输入时该文件的内容就会被输入,而不是通过终端,输出也一样。这就和重定向的作用类似。
dup2

在man手册的2中有一个函数为dup2,作用是改变文件的fd。
dup2(fd,1)即为将1号指定的文件改变为fd,就可以用来进行重定向。比如dup(fd,0)就是输入重定向,改为1就是输出重定向,但不会改变fd。所以重定向的本质就是通过dup2函数进行更改文件描述符指向。
父子进程与文件的关系
当父进程进行fork创建子进程时,子进程会拷贝父进程的文件描述符表,就是指针的浅拷贝,不需要在拷贝文件,比如输入输出文件,此时父子进程均指向相同的文件。所以父子printf时会同时向显示器打印。虽然子进程并没有显式的打开0,1,2,但是子进程已经默认打开了,是通过继承父进程方式来的,如果子进程进行关闭0号文件,是不会影响父子进程的输入的!因为在file是有一个引用计数的,只有该引用计数为0,才会进行关闭,所以父子使用共同的文件是通过引用计数的方式使用的。
所以创建子进程时,子进程是如何看待父进程打开的文件?子进程会拷贝父进程的PCB,文件描述符表,对于共同的文件,通过引用计数,所以当子进程进行关闭共同文件时,是不会影响该文件的,只有当引用计数为0时才会影响。
然而当子进程进行关闭该文件时,子进程确实无法使用该文件了,但是父进程仍然可以使用。比如进程进行关闭输入文件,但是bash仍然可以正常的输入,只是该进程无法输入了,核心原因是,文件描述符表是进程私有的,每个进程都有自己独立的文件描述符表,当进程进行close时,第一步会将该表中的这一项删掉,并将该文件的引用计数--,但是其他进程并没有close并且该文件的引用计数不为0,所以可以正常使用。
所以父进程如果进行重定向了,之后进行fork创建子进程,该子进程也是被重定向了。