在上篇文章我们复习了C文件IO操作并且认识了文件相关的系统调用接口。本篇文章我们要引入文件描述符的概念。
0.文件描述符
0.1引入文件描述符
我们在认识open接口时知道了该接口有一个int的返回值,但是当时我们并没有重点介绍这个返回值到底是什么,而这里我们将重点介绍这个返回值。因此我们用man手册查一下open函数的返回值。根据手册描述open函数如果打开或创建成功则会返回一个新的文件描述符,否则失败则返回-1。
那我们写一段代码来验证一下这个返回值,看看效果
#include <stdio.h>#include <string.h>#include <fcntl.h>#include <unistd.h>#include <sys/types.h>#include <sys/stat.h>int main(){ int fd1 = open("log1.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); int fd2 = open("log2.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); int fd3 = open("log3.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); int fd4 = open("log4.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); printf("fd1: %d\n",fd1); printf("fd2: %d\n",fd2); printf("fd3: %d\n",fd3); printf("fd4: %d\n",fd4); return 0;}我们看到文件被创建出来了,输出的fd也都是大于1的整数,但是这里有两个问题:
答:0,1,2被默认打开了。0叫做标准输入(键盘),1叫做标准输出(显示器),2叫做标准错误(显示器)。
On program startup, the integer file descriptors associated with the streams stdin, stdout, and stderr are 0, 1, and 2,respectively. (在程序启动时,与流stdin、stdout和stderr关联的整数文件描述符分别为0、1和2。)
#include <stdio.h>#include <string.h>#include <fcntl.h>#include <unistd.h>#include <sys/types.h>#include <sys/stat.h>int main(){ printf("stdin:%d\n",stdin->_fileno); printf("stdout:%d\n",stdout->_fileno); printf("stderr:%d\n",stderr->_fileno); return 0;}函数对应的接口
数据类型的对应
fopen/fclose/fread/fwrite.....
FILE* ->FILE
open/close/read/write......
fd
因此我们使用的C语言接口一定封装了系统调用接口。
2.fd为什么是0,12,3,4,5......
一般而言,一个进程可不可以打开多个文件?答案当然是可以的,所以在内核中,一个进程:打开的文件 = 1:n
所以系统在运行中有可能会存在大量的被打开的文件,操作系统要对被打开的文件进行管理。那么操作系统如何管理这些被打开的文件呢?--->先描述再组织 因此一个文件被打开在内核中要创建被打开文件的内核数据结构--先描述。 struct file 内部包含了大量的内容和属性。操作系统将多个文件的struct file用链表连接起来,因此对被打开文件的管理转换成了对链表的增删查改!那么进程如何和打开的文件建立映射关系呢?因此我们在task_struct中包含一个struct files_srruct * fs指针指向strcut files_struct内部有一个指针数组,存的就是打开文件的fd_array[ ]. 因此要对文件进行操作时,只需要得到这个数组的下标即可。
而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了fifile结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*fifiles, 指向一张表fifiles_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件
0.2 文件描述符的分配规则
通过上文的了解我们知道了文件描述符是从3开始的,因为0,1,2默认分给了stdin,stdout,stderr.那么当我们关闭0或者2我们再重新创建一个文件时,他的文件描述符会是几呢?
#include <stdio.h>#include <string.h>#include <fcntl.h>#include <unistd.h>#include <sys/types.h>#include <sys/stat.h>int main(){ close(0); int fd = open("myfile.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); if(fd < 0) { perror("open"); return 1; } printf("fd:%d\n",fd); close(fd); return 0;}发现是结果是: fd: 0或者fd:2 可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
0.3 重定向
那如果我们关闭fd为1呢?根据文件描述符的分配规则,新建文件的文件描述符fd会是1吗?我们来看代码:
#include <stdio.h>#include <string.h>#include <fcntl.h>#include <unistd.h>#include <sys/types.h>#include <sys/stat.h>int main(){ close(1); int fd = open("myfile.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); if(fd < 0) { perror("open"); return 1; } printf("fd:%d\n",fd); close(fd); return 0;}此时我们发现什么都没有,这是为什么呢?这时候我们要谈谈重定向了。根据文件描述的分配规则,我们新建的文件描述符fd一定是1,虽然不再指向对应的显示器了,但是已经指向了myfile.txt的底层数据结构了。那么我们如何查看到这个fd呢?这就跟输出重定向相关了,那么我们必须刷新一下才能看到,那么为什么要刷新呢?这是跟缓冲区相关的我们后面解释。
0.3.1 重定向的本质
在上述代码中,我们本来要往显示器上面写,最终却变成了向指定文件中写,这就是输出重定向。
当我们close(1)后,新建一个文件时,根据文件描述符分配规则,1号下标会指向新建的文件,因此凡是往1号文件描述符写的内容都写到myfile当中,而不再写到标准输出了。
如果我们要进行重定向,上层只认0,1,2,3,4,5这样的fd,我们可以在OS内部通过一定的方式调整数组的特定下标的内容(指向),我们就可以完成重定向操作!
0.3.2 dup2 -- 重定向的具体操作
上面的一对数据,都是内核数据结构,只有OS有权限,因此我们用户在使用的时候必须提供接口.因此操作系统提供了dup2
dup2的作用:
makes newfd be the copy of oldfd, closing newfd first if necessary。
dup2的使用:
假设我们要实现刚刚输出重定向的操作,那么根据dup2的描述,他是copy什么呢?参数怎么传?
dup2是copy数组下标对应的内容(文件地址的拷贝)最终的结果是newfd的内容是oldfd的一份拷贝,因此最后只剩下oldfd的内容。因此dup2是将oldfd拷到newfd内。因此如果要输出重定向,是要将fd为3的内容拷贝到fd为1内部。因此最后的内容和fd为3的内容保持一致。
因此输出重定向 dup2(3,1); 我们使用代码来验证一下
#include <stdio.h>#include <string.h>#include <fcntl.h>#include <unistd.h>#include <sys/types.h>#include <sys/stat.h>int main(){ int fd = open("myfile.txt",O_WRONLY | O_CREAT | O_TRUNC,0666); if(fd < 0) { perror("open"); return 1; } dup2(fd,1); printf("文件的fd:%d\n",fd); fflush(stdout); close(fd); return 0;}printf是C库当中的IO函数,一般往 stdout 中输出,但是stdout底层访问文件的时候,找的还是fd:1, 但此时,fd:1下标所表示内容,已经变成了myfifile的地址,不再是显示器文件的地址,所以,输出的任何消息都会往文件中写入,进而完成输出重定向。
追加重定向:
输入重定向:
(本篇完)