本文已收录至《Linux知识与编程》专栏!
作者:ARMCSKGT
演示环境:CentOS 7
前面我们介绍了文件描述符这个概念,关于文件描述符需要介绍的还有另一个知识,那就是重定向,重定向可以让我们指定程序向某一个文件流中输出数据或读取数据,操作系统只需要关心文件描述符即可;当我们在IO时,操作系统也并不是将数据直接写入文件,而是先写入缓冲区以提高效率;关于以上的概念,我们接下来将会逐一介绍!
文件描述符
重定向
我们在学习C/C++语言时,涉及三个文件流:stdin,stdout,stderr
他们的作用分别是:
- stdin:标准输入流,从键盘等输入设备中读取数据
- stdout:标准输出流,向显示器等输出设备输出数据
- stderr:标准错误流,向显示器等输出设备输出错误和异常问题
对应在C/C++语言中的:
- stdin标准输入流:scanf / cin
- stdout标准输出流:printf / cout
- stderr标准错误流:perror / cerr
可能会有小伙伴疑问到:标准输出和标准错误都是输出,为什么不把标准输出和标准错误合并起来?
注意: 标准错误与我们普通输出不同,我们平时程序在运行时,可能会形成日志,如果把日志信息和程序输出一起打印会非常杂乱,将他们分开输出可以让我们更好的管理数据!
重定向原理
我们知道,操作系统在管理被打开文件时会使用文件描述符,所以操作系统作为上层并不关心关心底层中具体操作的是哪一个文件(fd_array[] 中存储的对象),操作系统只知道从0号文件描述符下的文件读取数据,1号下输出数据,2号输出错误信息等等;所以我们可以调换这些文件描述符下的文件,让操作系统进程向我们指定的文件中输入和输出信息即可,这就是重定向!
重定向命令
在Linux系统中可以使用以下命令重定向
> #重定向输出流 < #重定向输入流 >> #重定向输出流(覆盖写入) # 例如 $ ./exe > txt #将程序exe的stdout(1文件描述符文件流)重定向到txt文件 $ ./exe < file #将程序exe的stdin(0文件描述符文件流)重定向到file文件 $ ./exe >> txt #将程序exe的stdout重定向到txt文件并清空txt文件原有的内容
输入重定向演示:
#include
#include using namespace std; int main() { string str; while(cin>>str) { cout<<str<<endl; } return 0; } 总结三个命令的功能:
- < : 输入重定向,将0号文件描述符下的文件流替换为命令右边的文件
- >:输出重定向,将1号文件描述符下的文件流替换为命令右边的文件
- >>:与 > 功能相同,但是写入文件前会清空文件中的所有数据
如果想要清空某一个文件,只需要 > file 即可,这个指令默认打开一次file文件并清空文件内存,但什么都不做!文件描述符重定向:
如果不指定文件描述符,则重定向默认对0和1号文件描述符做修改,如果我们想让其他文件流也重定向,那么需要使用文件描述符重定向!#include
using namespace std; int main() //cout和cerr交替打印 { cout<<"stdout标准输出:1"<<endl; cerr<<"stderr标准错误:1"<<endl; cout<<"stdout标准输出:2"<<endl; cerr<<"stderr标准错误:2"<<endl; cout<<"stdout标准输出:3"<<endl; cerr<<"stderr标准错误:3"<<endl; return 0; } 如果我们重定向输出到txt文件中,则只能重定向cout,cerr全部打印到屏幕上了
此时我们需要使用文件描述符重定向:$ ./exe 1>txt 2>log #将1文件描述符重定向为txt文件,2文件描述符重定向为log文件 $ ./exe >txt 2>log #当然也可以这样写,因为输出重定向默认是1,可以省略,但是不好看
重定向后,程序没有任何输出,而cout(标准输出)和cerr(标准错误)分别输出到对应的文件!
当然,文件描述符之间也是可以重定向的:#将文件描述符2中的文件流重定向为1文件描述符的文件流 $ ./exe >txt 2>&1 #两者效果相同 但一般使用本行这种写法 $ ./exe >txt 2<&1
以上述代码为例:
重定向函数
我们在写程序时,常用的重定向函数是:
#include
int dup2(int oldfd, int newfd) 函数解析:
- 参数
–oldfd:新的文件描述符
–newfd:旧文件描述符
功能是将newfd文件描述符下的文件流重定向为oldfd下文件描述符的文件流,最终重定向完,文件描述符只剩下oldfd,因为newfd已经被oldfd覆盖了 !- 返回值:重定向成功返回newfd,失败返回-1
#include
#include #include #include #include #include using namespace std; int main() { int txtfd = open("./txt",O_WRONLY|O_TRUNC|O_CREAT); int logfd = open("./log",O_WRONLY|O_TRUNC|O_CREAT); int d1 = dup2(txtfd,1); //将txt文件重定向到stdout int d2 = dup2(logfd,2); //将log文件重定向到stderr assert(d1>0 && d2>0); cout<<"stdout标准输出:1"<<endl; cerr<<"stderr标准错误:1"<<endl; cout<<"stdout标准输出:2"<<endl; cerr<<"stderr标准错误:2"<<endl; cout<<"stdout标准输出:3"<<endl; cerr<<"stderr标准错误:3"<<endl; close(txtfd); close(logfd); return 0; }
我们可以发现,重定向后,运行exe程序没有任何现象,而txt和log文件中存放了cout和cerr的输出信息!
在开发大型项目时,将错误信息单独剥离出来是一件很重要的事,学会重定向很重要!
缓冲区
缓冲区是什么?
操作系统的IO少不了缓冲区这个buffer,缓冲区相当于一个buffer数组,是内存上的一小部分空间,配合不同的刷新策略,起到提高 IO 效率的作用!
缓冲区理解:
缓冲区就好比快递站,当我们寄快递时,快递站不会立即将我们的快递装车运输,而是等快递数量达到一定时才进行一次运输!
这是因为快递车一次运输的时间非常长,如果每一个快递就运输则时效性会大大降低!
操作系统中的IO工作也存在这样的问题,所以需要缓冲区先存储数据,然后一次性刷新到磁盘!
所以,对于CPU来说,磁盘非常慢,如果频繁刷新数据,则在磁盘IO上会浪费大量时间,不如先存放到内存上再一次性刷新到磁盘上,毕竟内存上的速度还是CPU相对可以接受的!我们对比一下IO和不IO下对一个数++的区别:
#include
#include using namespace std; int main() { int num = 1; clock_t t = clock(); for (int i = 0; i < 100000; ++i) cout << num++ << endl; cout << "IO时间:" << clock() - t << endl; t = clock(); for (int i = 0; i < 100000; ++i) num++; cout << "非IO时间:" << clock() - t << endl; return 0; }
对一个数++十万次,需要IO下接近10秒才结束,而不需要IO则不足1秒!这里说明,在频繁IO的场景下,我们可以借助缓冲区来暂时储存数据,然后一次性刷新到磁盘!
缓冲区示例:
#include
#include #include #include #include #include using namespace std; int main() { int fd = open("./file",O_WRONLY|O_CREAT|O_TRUNC,0664); assert(fd!=-1); //检查文件是否打开成功 //例如像这样分多个模块需要写入的,如果直接刷新则需要分三次进行写入,而写入缓冲区则一次刷新即可 char str[100] = {0}; snprintf(str,sizeof(str),"%s:%d-","Linux file buffer write: ",sizeof(str)," bit"); write(fd,str,sizeof(str)); //一次刷新 close(fd); return 0; }
缓冲区刷新策略
缓冲区有多种刷新策略,比如 C语言 中 scanf 的缓冲区刷新策略为:遇到空白字符或换行就截断,因此在输入时需要按一下回车,缓冲区中的数据才能刷新至内核缓冲区中,而 printf 的刷新策略为行缓冲,即遇到 \n 才会进行刷新!
刷新策略:
- 行缓冲:遇到 \n 才进行刷新,一次刷新一行
- 全缓冲:缓冲区存满了才进行刷新
- 无缓冲:没有缓冲区
在Linux中,一般而言,显示器的刷新策略为行缓冲,而普通文件的刷新策略为全缓冲!
全缓冲:
#include
#include using namespace std; int main() { while(true) { cout<<"Hello Word"; usleep(10000); } return 0; } 行缓冲:
#include
#include using namespace std; int main() { while(true) { cout<<"Hello Word"<<endl; //endl换行,相当于\n usleep(10000); } return 0; }
内核缓冲区与普通缓冲区
我们使用C/C++语言上打开一个文件,C/C++库会为我们配置一个文件缓冲区,而这个只是语言层级上的缓冲区,我们现在知道文件打开是通过系统调用,而在系统中也会为打开的文件配置缓冲区!
对于库中的缓冲区我们称为普通缓冲区,而对于系统调用打开的文件缓冲区是内核缓冲区!
所以我们真正的IO流程是:数据先写入普通缓冲区(库缓冲区),然后通过库中的刷新策略刷新到内核缓冲区,最终遵循操作系统的刷新策略,刷新到磁盘上!
图片出自:Linux 实现原理 — I/O 处理流程与优化手段
理解缓冲区:
我们用代码理解一下缓冲区冲刷机制!#include
#include #include #include using namespace std; int main() { const char* s1 = "Hello Word!\n"; const char* s2 = "Hello Linux!\n"; fprintf(stdout,"%s",s1); //向stdout中写入 write(1,s2,strlen(s2)); fork(); //子进程 return 0; } 我们直接运行程序:
这里并没有什么问题,我们重定向输出到其他文件流中:
重定向运行后发现,相当于直接运行,file文件流中多了一句 Hello Word! 而且文件中的字符串输出顺序与直接运行的结果相反,这里就涉及内核缓冲区和普通缓冲区的知识了!
注意:当我们直接运行程序时,程序是向屏幕打印,此时使用的fprintf是C/C++库提供的函数和普通缓冲区,屏幕文件默认行缓冲,所以一旦我们写入的字符串有 \n 就会立刻输出;而write是系统调用。系统调用函数本身没有缓冲区,使用文件内核缓冲区,当我们写入后直接冲刷到文件内核缓冲区!
当我们重定向后,文件file属于普通文件,C/C++库缓冲区默认刷新策略是全缓冲,即缓冲区写满了才刷新到文件,而write则直接冲刷到文件内核缓冲区,策略随操作系统而定!
在进行fork之前,进程都不会冲刷普通缓冲区,所以fork时缓冲区的内容(Hello Word!)并没有冲刷到文件,而是在缓冲区中,此时父子进程各有一份未刷新的缓冲区!
当父子进程结束时,同时冲刷普通缓冲区写入文件内核缓冲区中,此时造成的现象就是率先写入了Hello Linux! 在文件内核缓冲区中,随后父子进程同时冲刷普通缓冲区中的Hello Word!到文件内核缓冲区中,所以我就内核缓冲区中写入数据的顺序是 Hello Linux! -> Hello Word! -> Hello Word!,最终操作系统将我就内核缓冲区中的数据冲刷到文件,就有了三条字符串!这里需要说明的是:普通缓冲区的数据需要刷新到文件内核缓冲区才能被操作系统冲刷到文件中;当我们调用语言所提供的库函数时,根据语言的规定会先写入语言提供的普通缓冲区中,而普通缓冲区需要冲刷到文件内核缓冲区中最终由操作系统冲刷至文件中!当我们使用系统调用时,是直接写入文件内核缓冲区,没有写入普通缓冲区的中间环节!
本节介绍了关于文件描述符的重定向知识,现在我们知道了我们不仅可以向屏幕打印字符,向键盘提取字符,也可以向其他文件输出和提取,因为Linux下一切皆文件!我们还学习了缓冲区的知识,知道了缓冲区的意义,这将对于我们以后的程序设计有极大的帮助!
本次
如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢!
其他文章阅读推荐
Linux<文件理解和系统调用> -CSDN博客
Linux<进程控制> -CSDN博客
Linux<进程地址空间> -CSDN博客
Linux<环境变量> -CSDN博客
Linux<进程初识> -CSDN博客
欢迎读者多多浏览多多支持!