理解重定向之前:先理解一个叫做文件描述符的具体操作。
文件描述符,也是在上篇文章提到的,
在描述进程属性的PCB对象中存在着一个struct file*
的指针,该指针指向一个指针数组,指针数组的每一个指针又指向对应的文件。
这个文件描述符,就是该指针数组的下标!
Linux的内核是这样实现的:
打开一个文件时,会在指针数组中找最近的一个空位置,存储该文件的地址。返回的文件描述符就是这个位置的下标。
先看下面的例子:
8 #define filename "log.txt"
9 int main()
10 {
11 int fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);
12
13 if(fd < 0)
14 {
15 perror("open fail");
16 return 1;
17 }
18 const char* msg = "hello linux\n";
19 int cnt = 5;
20 while(cnt)
21 {
22 write(1,msg,strlen(msg));
23 cnt--;
24 }
25 printf("%d\n",fd);
26 }
从上面的代码不难看出,这里是打开一个log.txt的文件,并循环打印hello linux到显示器中。
运行结果也是在我们意料之中的。
当我在开头加上:
在main函数内的第一行加上:
close(1);
这段代码时:
会看到两个现象:
1.由于已经把1号文件,即标准输出流关闭了,所以fd文件描述符没有在屏幕上打印,是可以理解的。
2.在重新运行该程序前,我已经把log.txt文件删除,运行程序后,重新出现了log.txt文件并且在该文件中出现了本来应该打印在显示器文件的内容!!!
而这个现象,就是所谓的输出重定向!!!
原理很简单:
在执行了close(1)后,1下标的文件描述符对应的指针就被置空了,同时维护stdout的引用计数也减减了,此时1号下标就是空的!!!
而在此之后,创建的新文件,会先从指针数组中查找最近的第一个空位置,并重新将该位置的指针指向新的文件,返回该下标!!!
也就是说,新打开的文件占用的是1号下标,返回的文件描述符是1!!!
这就解释了为什么本应该向显示器打印的内容,打印到了log.txt文件中,因为是该log.txt文件占用了1号下标的空间!!!
总结:所谓的输出重定向,就是将原来的文件向对应的文件数组中对象的地址做一次地址的拷贝!
把oldfd拷贝到newfd中,最终两个文件fd都是oldfd。
举个简单的例子:
假设1号文件描述符存储的是text.txt file*
dup2(3,1);
就是将3号这个文件描述符拷贝到1号文件中覆盖,拷贝完成后,1位置下标和3位置下标对应的文件描述符都是3。
最终两个文件描述符都是3。
假设test.c文件有如下内容
int main()
{
fprintf(stdout,"hello stdout\n");
fprintf(stdout,"hello stdout\n");
fprintf(stdout,"hello stdout\n");
fprintf(stdout,"hello stdout\n");
fprintf(stderr,"hello stderror\n");
fprintf(stderr,"hello stderror\n");
fprintf(stderr,"hello stderror\n");
fprintf(stderr,"hello stderror\n");
return 0;
}
编译test.c文件生成test文件后。
执行如下命令:
./test 1>log.txt 2>&1
该代码的意思是:
./test
可执行程序本应该向1号文件描述符对应的文件,即显示器文件中打印内容的,可是被重定向到了log.txt文件中,也就是log.txt文件对应文件描述符的内容拷贝到了1号文件的下标中。
所以./test 运行起来的四条printf语句会打印到log.txt文件中。
而2>&1
的意思是:将1号文件描述符中的指针内容拷贝给2好文件描述符中,本来2号文件描述符也是指向显示器文件的,至此也同样指向了log.txt文件。
所以log.txt文件会同时出现stdout和stderror两份打印内容。
./test >log.txt 2>&1
上面的命令还能这样写。也就是把1省略。因为默认就是打印到stdout上的。
输入重定向与输出重定向相反,默认从某一个文件中读取。
最简单的例子是:
cat < log.txt
从log.txt文件中读取到stdout显示器文件中。
进程替换的本质只是将进程在物理内存中的代码和数据提换成磁盘文件中特定可执行程序的代码和数据。
替换前后并未创建新的进程,且替换后,只是修改了一下页表的映射关系,修改一下进程的虚拟地址空间中的几个参数值。
而重定向的本质是对文件描述符表中的指针进行拷贝,修改的文件描述符表的内容,与进程替换没有任何关系。
两者各司其职,产生了解耦关系。
未来该找进程的文件描述符表中的某一个下标文件写入时就写入,内存管理想向特定文件写入就写入,与文件描述符表的管理没有任何关系。
我们平时按的键盘,看的显示器,网卡,声卡,磁盘等等,这些在Linux下都是文件,也就是我们平时所说的外设。
这些外设也是文件。
所以也有文件所有的读方法,写方法,其他的文件属性等等。
当我们想从键盘中输入一些东西时,首先要打开键盘文件。
打开前操作系统会做好一系列准备工作。
创建进程的PCB结构体对象task_struct
,该结构体对象中能找到管理文件的文件结构对象(files_struct)
。
同事,这些外设本来就提供有属于它们独有的访问文件的读写方法等。
并且,每一个文件在被打开时,都会创建属于自己的文件对象struct file
,这个文件对象中保存该文件的各种属性信息,其中就有两个函数指针,分别是读指针和写指针,指向该文件的读写方法。
在文件结构对象表files_struct
中有一个文件描述符数组,该数组存储的是各个文件的文件描述符。
通过这些文件描述符即可找到对应的每一个被打开的文件。
而这一系列准备工作完成后,假设进程开始调度键盘文件。
该进程就是read系统调用。
read这个进程被运行起来,其内部有这样一条核心代码:
ssize_t read(int fd)
{
task_struct->files_struct->fd_arr[fd]->f_opes->write();
}
首先进程去到自己的task_struct
PCB对象中找到文件对象表,在该表中获取到文件描述符,通过文件描述符找到了对应的文件对象,再通过文件对象中的一个指针找到描述对应文件的读写方法结构体,然后通过该结构体内的读写方法指针获取到对应的读写方法,再由该方法调用到对应的外设!
所以这就是我们之前经常所说的Linux下一切皆文件的原因!!!
这整个过程,就非常像C++中的多态!!!
所以可以肯定的是,C++的多态是抄C语言内核的,必定是这样!
任何一门语言要支持面向对象,它的底层一定会支持这样的思想结构。
Linux下一切皆文件的本质就是面向对象的过程,对每个对象分层,串联起来就能实现调用不同的外设,就能实现不同的功能!!!