前面的文章中我们讲述了C语言中文件相关的操作与系统文件IO的接口,这篇文章中将会讲述文件描述符与重定向的知识。
运行在前文中的系统文件程序,通过观察可以看到图中的数据3非常的奇怪没头没尾的,下面我们就来从这里开始。
通过查看man手册可以看到open函数的返回值为:
发生错误返回-1, 那么正常运行不应该从0开始吗?为什么是3?0、1、2到哪里去了呢?
任何一个进程,在启动的时候默认会打开当前进程的三个文件:
标准输入, 标准输出, 标准错误 -- 本质都是文件
stdin stdout stderr -- 文件在语言层的表现
cin cout cerr
图中与上面的意思相同,即文件在语言层的表现,FILE* 是一个类。
上图中的FILE* 又表示着什么内容?
一个简单的例子来看一下上述的内容:
#include <iostream> #include <cstdio>// 使用c++风格的c语言的头文件 using namespace std; int main() { // Linux下一切皆文件,所以,向显示器打印本质上就是向文件中写入,怎样理解? // c fprintf(stdout, "helo fprintf->stdout\n"); fprintf(stderr, "helo fprintf->stderr\n"); // c++ cout << "hello cout -> cout" << endl; cerr << "hello cerr -> cerr" << endl; }
标准输入 -- 设备文件 -> 键盘文件
标准输出 -- 设备文件 -> 显示器文件
标准错误 -- 设备文件 -> 显示器文件
运行上述代码可以发现正常执行的时候标准输入与标准错误都打印在了显示器上,然而当重定向到log文本文件中就会发现只有标准错误打印到了显示器文件上。
至此,可以得到一个结论:标准输出和标准错误都会向显示器打印,但是其实不太一样。
上述我们一共提到了三个问题,接下来将逐一的解决。
fd = 3?
我们知道了任何一个进程,在启动的时候默认会打开当前进程的三个文件,恰好前文的文件描述符正好是从3开始的,可以大胆猜想标准输入, 标准输出, 标准错误三个的文件描述符分别为012,事实也确实如此。当我们打开多个文件时文件的描述符的就会因此增加:
这种数字的增加让人感觉很像数组的下标,我们得出一个结论文件描述符(open对应的返回值)本质上就是数组下标。
我们根据之前的知识知道,运行代码时他就会变成一个进程拥有PCB,在磁盘中有大量的文件, 打开文件就会被加载到内存中,有了大量的文件就需要使用先描述再组织的方法将其管理起来。内存中就会存在很多的文件对象 -- struct file结构体,里面包含着文件的大部分属性。当进程想要访问文件的时候,只需要找到file就能找到文件的所有内容。文件是操作系统打开的,又是用户让操作系统打开的,最后就是以进程为代表打开文件;因此我们需要有一种方式能够把一个进程和自己打开的文件对应关系维护起来。进程和被打开的文件可以简单的理解为1:n的关系。在操作系统中是这样设计的:在内核汇总定义了struct file_struct结构体,其中有一个数组struct file* fd array[]里面有指向内存中文件结构体的指针,在PCB中有着struct file_stryct* files的指针指向数组存在的那个结构体。
当我们从磁盘中load文件时,操作系统为了管理文件,为文件创建了一个结构体对象,然后,找到那一个进程打开的文件,在其中的数组中寻找一个未使用的下标,将该结构体的地址填入到数组3下标的位置中,因此我们获得了文件描述符fd = 3。
文件结构体对象中主要是文件的各种属性,还有更重要的是每个文件都要匹配一个缓冲区。当我们执行write函数时,进程PCB就会根据上述说的轨迹找到存放文件对象的数组,根据fd的数据找到文件的地址,再将buffer数组中的字符串拷贝到文件的缓冲区中。我们所谓的IO类read,write函数本质是拷贝函数。文件缓冲区什么时候刷新到磁盘中的指定位置,有OS自主决定。
至此第一个问题fd = 3?就暂时解决了。
如何理解一切皆文件
首先,我们有很多的硬件,每个硬件都有自己的匹配的驱动程序,例如键盘就有写入的函数程序...当我们打开键盘的时候都会在内存中创建文件结构体对象,在结构体中就有着对应的函数指针指向硬件中对应的方法。当进程访问各种硬件的时候在进程看来就是访问的各种文件结构体对象。因此可以得出Linux下一切皆文件。
我们使用OS的本质就是通过进程的方式进行OS的访问的!!!
FILE
在操作系统层面,我们必须要访问fd,才能找到文件,任何语言层访问外设或文件必须经历OS。
FILE是什么?与我们刚刚说的内核的struct file有什么关系?
FILE是一个结构体,是C语言给我们提供的。没有任何关系,上下层的关系
在FILE结构体中必定包含了文件描述符,下面我们可以来验证一下:
int main() { printf("%d\n", stdin->_fileno); printf("%d\n", stdout->_fileno); printf("%d\n", stderr->_fileno); FILE* fp = fopen(LOG, "w"); printf("%d\n", fp->_fileno); }
这样,从上图中就可以看出确实是如此。
重定向的本质
先来看一个简单的例子:
fclose(stdin); // 关闭了0号 close(0); 28 close(2); 29 int fd1 = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd2 = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd3 = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd4 = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd5 = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
int fd6 = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666);
printf("%d\n", fd1);
printf("%d\n", fd2);
printf("%d\n", fd3);
printf("%d\n", fd4);
printf("%d\n", fd5);
printf("%d\n", fd6);
可以看到当我们关闭了0号与2号文件,新文件会从小的数依次分配。进程中文件描述符的分配规则:在文件描述符表中,最小的没有被使用的数组元素,分配给新文件。
再看一个有趣的例子:
close(1);
int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC);
printf("you can see me\n");
现将1号文件即标准输出关闭,然后向屏幕打印信息,运行程序会发现下面的现象:屏幕上并没有打印信息,反而信息出现在了log文本文件中,这种情况就与重定向一致。
重定向的原理:在上层无法感知的情况下,在OS内部,更改进程对应的文件描述符表中,特定的下标指向。
同样可以关闭标准输入,达到一个输入重定向的效果:
close(0);
int fd = open(LOG, O_RDONLY);
int a, b;
scanf("%d %d", &a, &b);
printf("a:%d, b:%d\n", a, b);
还有追加重定向:
close(1);
int fd = open(LOG, O_WRONLY | O_CREAT | O_APPEND, 0666);
printf("you can see me\n");
至此,第三个问题为什么会有如下情况就可以得到了解决:重定向只改写了1号文件描述符对应的文件,没有修改2号文件描述符。
我们可以通过重定向的方式将正确输出与错误输出相互分离:
但是这样的操作不是很便捷,这里OS就提供了一个函数可以来进行重定向操作
// close(1)
int fd1 = open(LOG_NORANL, O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd1, 1);
fprintf(stdout, "helo fprintf->stdout\n");
fprintf(stdout, "helo fprintf->stdout\n");
fprintf(stdout, "helo fprintf->stdout\n");
fprintf(stdout, "helo fprintf->stdout\n");
fprintf(stdout, "helo fprintf->stdout\n");
// close(2);
int fd2 = open(LOG_ERROR, O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd2, 2);
fprintf(stderr, "helo fprintf->stderr\n");
fprintf(stderr, "helo fprintf->stderr\n");
fprintf(stderr, "helo fprintf->stderr\n");
fprintf(stderr, "helo fprintf->stderr\n");
fprintf(stderr, "helo fprintf->stderr\n");
使用该函数特别需要注意newfd与oldfd的区别: