目录
文件描述符的引入
什么是文件描述符
文件描述符的分配规则
重定向
输出重定向
输入重定向
追加重定向
dup2()
文件描述符的引入
上一章,我们讲解了系统接口,了解了open()函数的返回值是一个整数.那么这个整数究竟是什么呢?我们可以用以下代码来看一下这种现象:
int main()
{
umask(0);
int fd1 = open("log1.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
printf("open success, fd: %d\n",fd1);
int fd2 = open("log2.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
printf("open success, fd: %d\n",fd2);
int fd3 = open("log3.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
printf("open success, fd: %d\n",fd3);
int fd4 = open("log4.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
printf("open success, fd: %d\n",fd4);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
}
我们分别打开了4个文件.然后退出make编译然后运行.
我们发现:这些的fd是连续,并且fd是从3开始的,那么0,1,2去哪里了呢?
我们上一章提到了,执行程序时,会默认打开3个输入输出流,分别为:stdin,stdout,stderr.
这三个分别占据了0,1,2的位置.
我们可以使用一个例子来验证它们之间的关联.
int main()
{
umask(0);
//向stdout(显示器)中输出
fprintf(stdout,"hello,stdout\n");
//向fd为1的文件写入
const char* s = "hello,1\n";
write(1,s,strlen(s));
}
我们可以发现,这两条语句都成功的在显示屏中输出了.
这就说明fd为1的文件 确实是对应着显示器.
我们在前一章讲了fopen()这个接口,我们再来看一下
我们别的不再说,我们看它的返回值是FILE*.我们知道这是个指针.
但是这个FILE是什么呢?FILE是一个struct结构体,它的内部有很多和文件相关的成员,它是由C标准库提供的.
C语言 库函数内部一定要调用 系统接口的!在系统角度,系统只认fd,像FILE这些其它变量,系统是不会识别的.所以这个FILE结构体里面一定封装了fd.
话都说到这了,那么stdin,stdout,stderr的类型也是FILE*,它的内部也一定封装了fd.我们可以打印输出来证明一下.
int main()
{
printf("stdin: %d\n",stdin->_fileno);
printf("stdout: %d\n",stdout->_fileno);
printf("stderr: %d\n",stderr->_fileno);
return 0;
}
运行结果图:
结果确实如我们所想的那样.便成功验证了我们刚才所说的.
所以我们最终的结论是:
1.C语言的文件调用接口 和 系统接口是有一定关系的.
2.在用户层面上是stdin,stdout,stderr,在系统层面上只能用数字0,1,2,来表示。这些数字便是文件描述符。
什么是文件描述符
上面一直在说fd,知道它是一个整数,那么它到底是什么呢?
进程要访问文件,首先必须打开文件。
一个进程可以打开多个文件,所以一般而言,进程:文件 = 1:n
文件要被访问,前提是加载到内存中,才能被直接访问.
既然一个进程可以打开多个文件,那么如果多个进程都打开自己的文件呢?
这个时候系统中便会存在大量被打开的文件,所以OS需要把这么多的文件管理起来.
管理方式:先描述 + 再组织。
在内核中,如何看待打开的文件? OS为了管理每一个被打开的文件,需要先将其抽象成struct结构体,然后再描述,这个结构体叫做struct file,大致内容如下:
有很多这样的文件需要组织管理起来,那么采用了双链表进行管理.
但是进程是如何知道哪个是我打开的文件呢,所以现在唯一需要处理的就是进程和文件之间的对应关系.
这里先提前说一句:
操作系统负责处理这些进程文件操作请求,管理文件描述符和维护文件状态,但文件的具体内容和处理是由进程来控制和管理的。
所以不要因为一会操作系统管理,一会进程管理而混淆了.
我们知道文件描述符fd是从0开始递增的,在我们所学到的容器里面,vector容器的下标也是从0开始向后递增的。
所以这个对应关系其实本质上是一个数组,然后这个数组的每个下标指向相应的file_struct,这个数组里的每个下标便叫做文件描述符(fd).这个数组叫做文件描述符表.
它作为一个成员在进程task_struct结构体中.
在task_struct结构体中,我们通过内核源代码可以看到:
内部有一个结构体指针,这个指针指向files_struct这个结构体,然后我们来看一下这个结构体里面有什么内容.
重点看最后一个结构体fd_array,它的类型是一个指针数组类型,它就是文件描述符表,每个元素都是一个指针,指向file结构体,那我们接下来看看这个file里面有什么内容.
这个里面就包含了一个文件的所有内容,包括属性和内容相关的,我们现在知道通过fd_array便可以找到所有的文件即可.
总结一下以上的关系:
进程task_struct结构体中,有一个结构体指针指向了files_struct,file_struct里又有一个结构体数组fd_array,fd_array数组里每个下标便是文件描述符,下标对应的内容便是fd为该下标文件的内容.
然后我们再整体理顺一下之间的关系。以及是如何一步步推导过来的.
文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。
而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件
文件描述符的分配规则
我们就先创建一个新的文件,然后看看它的文件描述符是多少,代码如下:
int main()
{
int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n",fd);
close(fd);
return 0;
}
然后我们退出vim,然后make编译并运行.
可以看到,fd现在是3,这也没有问题,因为0,1,2已经默认被stdin,stdout,stderr占用了.
此时如果我们把fd为0的文件关掉,此时再看一下文件的fd是多少.
close(0);
然后此时运行结果显示,fd为0.
同样地,如果我们把fd为2的文件关掉.
close(2);
此时的文件fd又成了2.
我们大概知道了,新打开文件的fd默认为第一个没有被占用的fd.
所以这里fd的分配规则是:给新文件分配最小的,且没有被占用的文件描述符.
重定向
输出重定向
我们前面只说了关闭fd为0和2的,但是如果关闭fd为1的文件呢?
我们输入以下代码
int main()
{
close(1);
int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n",fd); //stdout->FILE[fileno=1]->log.txt
printf("fd: %d\n",fd);
printf("fd: %d\n",fd);
//close(fd); //后面解释
return 0;
}
然后我们再运行这段代码.
我们发现什么都没有输出,这不应该啊,按理来说,应该输出三个1才对.
但是我们如果输出一下log.txt里面的内容:
我们发现,本应该输出在显示器上的内容输出到了log.txt这个文件中.
当我们不关闭fd=1的文件时,我们再次编译并运行.
此时我们发现正常输出到显示器上了.
以上是因为:printf默认是向stdout(标准输出)里面打印的,即fd为1的文件。但是此时fd=1的文件被关掉,然后被新建立的文件占用了,所以此时printf再输出,便会直接输出到这个文件当中了.
这种原本向显示器中打印的,但是最后却写入(显示)到了别的文件中,这个行为便叫做输出重定向.
下面是一张重定向原理图:
原来fd=1指向的文件是显示器,然后被更改指向myfile,所以之前写的向显示器上输出的内容全部输出到了myfile这个文件中.
所以重定向的本质是:在OS内部,更改fd对应内容的指向.
输入重定向
我们知道了重定向的原理,输入重定向也很好理解.无非是从键读取的内容变成了从别的文件中读取.
先来看这代码:
int main()
{
int fd = open("log.txt",O_RDONLY,0666);
if(fd < 0)
{
perror("open");
return 0;
}
printf("打开的文件fd为:%d\n",fd);
char buffer[64];
fgets(buffer,sizeof(buffer),stdin);
printf("%s\n",buffer);
}
我们先打开了一个文件,然后输出它的fd,接着,我们写了一个fgets,第三个参数为stdin,表示从键盘中读取数据,并将读取的数据输出到buffer中,并输出buffer.
我们可以看到它完整地将我的输入输出了出来.
但是此时如果我们在前面加上close(0),同时为了效果明显,我们在log.txt文件中写入以下内容:
然后我们再运行 代码
我们发现它此时没有卡着等待我的键盘输入,而是直接从文件中读取了。
相当于是把fd=0所指向的键盘文件 变成了log.txt文件.
这个过程便叫做输入重定向.
追加重定向
这个和输出重定向类似,只不过是把open函数中O_TRUNC换成了O_APPEND.
代码如下:
int main()
{
close(1);
int fd = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
if(fd < 0)
{
perror("open");
return 0;
}
fprintf(stdout,"you can see me\n");
}
这段程序本来每次是向显示器中输出“you can see me”,然后经过重定向,最后追加到了log.txt中.
这便是追加重定向.
dup2()
以上的代码只是为了演示重定向的原理,真正使用重定向时,我们并不会那样使用,而且每次使用时都得先关闭文件再打开文件,这样写出来的代码可读性并不高.
所以为了解决这个问题,我们可以使用dup2一个系统调用来解决.
我们先看一下dup2的用法:
然后再看一下解释:
它的意思是newfd是oldfd的一份拷贝,即将oldfd拷贝给newfd. 最终要和oldfd一致.
如果oldfd不是有效的文件描述符,那么调用失败,newfd不会被关闭.
如果oldfd是有效的文件描述符,那么将会把oldfd的值拷贝给newfd。
回到刚开始说的输出重定向.
我们最终改变了fd=1的所指向的内容,即将原来的fd=3的文件指针 拷贝给了 fd=1的文件指针.
最终结果和fd=3一致,正如上面这句话所说“ 最终要和oldfd一致” ,所以fd=3是oldfd,fd=1则是newfd.这样便理清了关系.
知道了这些,我们赶紧来用用dup2.
int main(int argc, char* argv[])
{
if(argc != 2)
{
return 2;
}
int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
if(fd < 0)
{
perror("open");
return 1;
}
dup2(fd,1);//将fd(3)的内容拷贝到fd=1当中
fprintf(stdout,"%s\n",argv[1]);
}
argc和argv是命令行参数,具体可以参照我之前的文章:传送门,里面有详细的解释.
然后此时我们运行程序:
这样便把我们的本该输出到显示器上的内容输出到文件中了.
这样dup2的大致用法也就说完了.
本文章到这里就结束了,如有疑问或者错误的地方,欢迎评论区指正或私信哦.