五、重定向
在谈论重定向之前我们先来谈论一下C语言中的FILE
我们使用C语言进行打开文件时,系统都会给我们一个FILE
指针那这个FILE
指针是什么呢?是谁给我们提供的呢?
答案是:是C语言给我们提供的,这个FILE
其实就是一个C库给我们封装的一个结构体,而且这个结构体内部一定要有文件描述符fd
,因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd
访问的。所以C库当中的FILE
结构体内部,必定封装了fd
。
在C库的内部源代码中有这样一些源代码:
//将 _IO_FILE 重命名为FILE typedef struct _IO_FILE FILE; //在/usr/include/stdio.h struct _IO_FILE { int _flags; // ...... struct _IO_FILE *_chain; int _fileno; //封装的文件描述符 //...... };
通过这段源代码,我们知道FILE
内部有一个叫 _fileno
的文件描述符,那么我们就可以将stdin
stdout
stderr
的文件描述符打印出来,看看与我们上面的结论是不是一样的。
打印三个标准流的文件描述符
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #define LOG "mylog.txt" int main() { //打印出文件标识符 printf("%d\n", stdin->_fileno); printf("%d\n", stdout->_fileno); printf("%d\n", stderr->_fileno); close(fd); return 0; }
结果和我们以前给的结论是一样的。
重定向的原理
输入重定向
看下面一段代码,我们就可以尝试如果我们关闭1
号文件描述符,然后我们再打开一个文件,之后我们向stdout
里面输入一些数据,看一看会发生什么?还是打印到显示器上面吗?
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #define LOG "mylog.txt" //#define N 64 int main() { //关闭标准输出流 close(1); int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC); if(fd < 0) { perror("open fail:"); exit(-1); } printf("you can see me?\n"); printf("you can see me?\n"); printf("you can see me?\n"); printf("you can see me?\n"); return 0; }
答案是并没有打印到显示器中,而是打印到了文件中,相信有了前面的基础你已经明白了,我们将stdout
关闭后,新打开的文件占据了1
号文件描述符,而我们的printf
函数只认识1
号文件描述符,所以向1
号文件描述符指向的文件输入内容,就导致数据输入到了文件里面!
追加重定向
追加重定向的原理很简单,我们只需要将文件的打开方式加上O_APPEND
去掉O_TRUNC
。
例如对于刚才的文件进行重定向:
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #define LOG "mylog.txt" //#define N 64 int main() { close(1); int fd = open(LOG, O_WRONLY | O_CREAT | O_APPEND); if(fd < 0) { perror("open fail:"); exit(-1); } printf("this is append\n"); printf("this is append\n"); printf("this is append\n"); return 0;
输入重定向
同理,我们把0
号文件标识符给关闭,然后打开我们的新文件进行scanf
,那么我们应该会从新打开的文件中读取数据,我们看一看结果:
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #define LOG "mylog.txt" //#define N 64 int main() { close(0); int fd = open(LOG, O_RDONLY); if (fd < 0) { perror("open fail:"); exit(-1); } int a; char c; scanf("%d %c",&a, &c); printf("%d %c\n", a, c); return 0; }
结果是符合我们的预期的!
重定向的原理:在上层无法感知的情况下,在操作系统内部,更改进程对应的文件描述符表中,特定下标的指向!!!
根据这些原理我们来实现一个需求:将标准输出流与标准错误流的信息进行分流。
分析:我们知道标准输入流与标准输出流其实打开的是都是显示器文件,如果我们直接用标准输出标准错误流一起使用,就会导致错误信息与正确信息混合在一起,导致我们难以找到错误所在。
我们可以使用重定向进行分流,我们先关闭1
号文件描述符,然后新打开一个文件normal.txt
,然后关闭2
号文件描述符,再打开一个新的文件error.txt
这样我们再使用标准输入或标准错误流时,信息会被写入两个不同的文件中,我们关心错误信息就可以打开error.txt
进行查看,关心正确信息,就可以打开normal.txt
进行查看。
原本不分流时:
#include<stdio.h> int main() { fprintf(stdout, "stdout->normal\n"); fprintf(stdout, "stdout->normal\n"); fprintf(stdout, "stdout->normal\n"); fprintf(stdout, "stdout->normal\n"); fprintf(stderr, "stderr->error\n"); fprintf(stderr, "stderr->error\n"); fprintf(stderr, "stderr->error\n"); return 0; }
错误信息与正确信息混在一起!!
进行分流:
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> int main() { umask(0); close(1); open("normal.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); close(2); open("error.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); fprintf(stdout, "stdout->normal\n"); fprintf(stdout, "stdout->normal\n"); fprintf(stdout, "stdout->normal\n"); fprintf(stdout, "stdout->normal\n"); fprintf(stderr, "stderr->error\n"); fprintf(stderr, "stderr->error\n"); fprintf(stderr, "stderr->error\n"); close(1); close(2); return 0; }
分流完成!
系统调用dup2
其实呢,对于上面的操作我们手动关闭其实是有一些不方便的,对于上面的操作Linux
给我们提供了一个系统调用dup2
,它的作用就是用第一个标识符里面的地址覆盖第二个的标识符中的地址。从而达到重定向的目的。
- 第一个参数:要保留的参数。
- 第二个参数: 要被覆盖的参数。
- 返回值: 成功就返回第二个文件表示符,失败就返回 -1,并设置错误码。
对于上面的分流代码我们就可以:
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> int main() { umask(0); int fd1 = open("normal.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); int fd2 = open("error.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); int n1 = dup2(fd1, 1); int n2 = dup2(fd2, 2); //将dup2的返回值打印进文件中 printf("%d %d\n", n1, n2); fprintf(stdout, "stdout->normal\n"); fprintf(stdout, "stdout->normal\n"); fprintf(stdout, "stdout->normal\n"); fprintf(stdout, "stdout->normal\n"); fprintf(stderr, "stderr->error\n"); fprintf(stderr, "stderr->error\n"); fprintf(stderr, "stderr->error\n"); close(fd1); close(fd2); return 0; }
六、缓冲区的理解
我们以前学习C语言的文件操作时,我们都知道FILE
里面应该是有缓冲区的,现在我们学习操作系统时我们又知道操作系统内核里面也是有缓冲区的,那这两个缓冲区是一样的吗?
对于这个问题我们现在不好回答,我们只能先给出结论:是不一样的,FILE
是C库提供给我们的一个结构体,里面的缓冲区对应的是用户态的缓冲区,linux
内核中的缓冲区,对应的是内核态的缓冲区。
我们先看下面的代码,根据现象我们来分析问题,最后再来理解一下缓冲区。
#include<stdio.h> #include<unistd.h> #include<string.h> int main() { printf("printf : hello world!\n"); const char* str = "write: hello world!\n"; write(1, str, strlen(str)); //创建子进程 fork(); return 0; }
结果:
我们发现当我们直接运行和重定向后的结果是不同的,而且printf()
会比write
多一次打印,这时为什么呢?
其实呢这与C库的缓冲区有关系!缓冲区在哪里?在你进行fopen打开文件的时候,你会得到FILE结构体,缓冲区就在这个FILE结构体中!!
C库会结合一定的刷新策略,将我们缓冲区中的数据写入给操作系统(通过write (FILE->fd,xXXX) ) ;
- 无缓冲
- 行缓冲 (显示器采用的刷新策略: 行缓冲)
- 全缓冲 (普通文件采用的刷新策略:全缓冲)
通过这张图片我们就能很好的知道,为什么会出现上面的情况了。
在运行时,printf
函数使用的是显示器文件,所以代码运行后立即就被C库的刷新到了操作系统内核里面的缓冲区了,write
函数本身就是向操作系统内核里面写入数据,因此也将数据写入到操作系统内核里面的缓冲区了,fork
之后FILE
里面的缓冲区的数据内容要被清空,但是FILE
的缓冲区里面本身就没有数据,无法输出数据了,进程也结束了。
但是,当变成重定向时,由于printf
函数使用的文件变成了普通文件了,数据的刷新方式变成了全缓冲,所以printf
代码运行之后数据被暂存到了FILE
的缓冲区里面了,而write
函数写的数据向系统内核里面直接写入了数据,程序运行完毕,FILE
内部的缓冲区要被清洗(此时缓冲区里面有数据),但是进程从一个变成了两个,要清洗两次缓冲区,于是log.txt
里面就有了两次printf
打印的内容。
为什么C库的
FILE
里面要有缓冲区呢?
答案是:节省调用者的时间! 如果我们想直接把数据写到操作系统内核中就需要调用系统调用,而系统调用的使用代价是要比普通函数大的多的,因此为了尽量少的使用系统调用,尽量一次IO能够读取和写入更多的数据,所以
FILE
内部才有了缓冲区。