🚩重定向是什么?
在上篇博客[文件描述符]中我曾提到了一个有意思的证明:进程在最开始运行的时候,首先打开了三个文件,分别是标准输入流、标准输出流、标准错误输出流。证明的时候我是把标准输出留给关闭了,然后紧接着创建的文件就会占用已关闭的标准输出流,使得本该流向显示器的数据流向了新创建的文件。先不谈底层的原理,就只看表象,就像是使数据流的方向从一个方向,指向了另一个方向,完成了数据流的方向重定向。现在再次理解重定向就好理解得多了:重新锚定方向
🚩dup系列函数实现重定向
就如我上面提到的证明过程,虽说最后也实现了重定向的操作,但是这都是我们手动一步一步设计的环节,先关闭再创建,并且是重定向哪个,哪个就要关闭,关闭和创建之间,不能有其他文件的创建,否则就会把关闭的文件给占用掉了,从而导致定向到了错误的地方。
晕😵~,感觉好麻烦。但是别担心,操作系统给我们提供了函数接口,帮我们在文件管理的层面直接解决这个问题。
上图是dup系列的重定向函数,同样的,也都是系统提供给我们的函数,属于直接对内核数据进行修改。
由于看着实在是太多而且还是英文,这里就由我一步一步从dup开始解析吧。
🍁dup
头文件:unistd.h
参数:oldfd–旧的文件描述符(意味着最终要指向的文件,用old来描述确实很奇怪,但是没办法,将就着理解叭)
返回值:在成功的情况下,返回新文件描述符。如果出现错误,则返回-1,并适当设置errno。
返回的文件描述符重新指向了oldfd指向的文件,这个新的文件描述符是没有被使用的最小的文件描述符。
⌨整点代码测试:
#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define PATH_NAME "log.txt"
using namespace std;
int main()
{
umask(0);
int oldfd = open(PATH_NAME, O_CREAT | O_RDWR | O_TRUNC, 0600);
if (oldfd < 0)
{
cerr << strerror(errno) << endl;
exit(2);
}
cout << "oldfd: " << oldfd << endl;
int newfd = dup(oldfd);
cout << "newfd: " << newfd << endl;
const char *str = "Hello, Kangkang, this is Michael\n";
char output[1024];
write(newfd, str, strlen(str));
lseek(oldfd, 0, SEEK_SET);
ssize_t size = read(oldfd, output, sizeof(output) - 1);
if (size)
{
output[size] = '\0';
cout << output;
}
else
{
cout << "do nothing" << endl;
}
close(oldfd);
close(newfd);
return 0;
}
💻:
首先以读写的方式打开一个文件log.txt,返回一个文件描述符oldfd,接着调用dup返回一个新的文件描述符,这个文件描述符按道理讲也是指向log.txt的,接着我们以新的文件描述符去从log.txt中读取,事实证明确实可以,dup的功能得到证实。
🍁dup2
dup虽然可以完成重定向,但是使用起来也不是那么方便,因为它只能重定向未被使用的最小的文件描述符,对已有的文件不能完成重定向。因此为了解决这个问题,又提供了dup的进阶版–dup2,可以实现指定的文件描述符newfd的重定向。
头文件:unistd.h
参数:
oldfd–最终指向的文件的 文件描述符
newfd–需要被重定向的文件描述符
newfd与oldfd一样的时候,什么都不做,直接返回newfd
返回值:在成功的情况下,返回新文件描述符。如果出现错误,则返回-1,并适当设置errno。
这里的newfd就是我们用户指定的需要被重定向的文件描述符。
⌨整点代码测试:
#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define PATH_NAME "log.txt"
using namespace std;
int main()
{
umask(0);
int oldfd = open(PATH_NAME, O_CREAT | O_RDWR | O_TRUNC, 0600);
if (oldfd < 0)
{
cerr << strerror(errno) << endl;
exit(2);
}
cout << "oldfd: " << oldfd << endl;
int newfd = dup2(oldfd, 1); //使得标准输出流指向log.txt
if (newfd == -1)
{
cerr << strerror(errno) << endl;
exit(2);
}
//以下的cout全部都是向log.txt中输出数据
cout << "newfd: " << newfd << endl;
const char *str = "Hello, Kangkang, this is Michael\n";
cout << str;
//使得标准输入流指向log.txt
newfd = dup2(oldfd,0);
if (newfd == -1)
{
cerr << strerror(errno) << endl;
exit(2);
}
//重新设置文件偏移量,从文件的起始位置开始
lseek(oldfd,0,SEEK_SET);
char output[1024];
cin >> output; //仅仅从文件log.txt中读取一次数据
cout << output << endl; //再次将其放入文件中
close(oldfd);
close(newfd);
return 0;
}
💻:
之所以只有一个"newfd:"被读取到了,是因为cin提取流默认读到空格’ ‘或者换行符’\n’都会停止读取。并且我们发现上面的代码我们使用了lseek来改变文件的偏移量,改变了oldfd的也会导致newfd的文件偏移量发生同步变化,这也就是说新的文件描述符不仅仅只指向了oldfd,并且还共享文件偏移量和文件的状态。
讲真的,一般情况下,我们用到最多的就是指定文件的重定向dup2,毕竟重定向一般都是有需求了才会重定向的叭🤔。
🍁dup3
dup3和dup2功能相似,只是多了一个参数:flags,并且要宏定义_GNU_SOURCE,这样就可以设置flags为库里宏定义好的O_CLOEXEC,那这样做的意义是什么呢?
🔺首先我们得清楚O_CLOEXEC的作用是干嘛的:
由于在调用exec系列函数([进程替换]((1条消息) 进程控制–Linux_皮皮蜥的博客-CSDN博客))时,由于进程里的程序被替换,文件描述符就会被释放,但是文件表项却没被释放,有点像内存泄漏,相当于栈上的指针被释放了,但是指针指向的堆上的空间并没有被释放。也就是说,假如我们知道了替换程序前的文件描述符,就可以在替换后的程序中继续对之前打开的文件进行操作,这意味着新的程序会继承被替换的程序的文件表项(文件指针数组)。
但是我们替换程序的时候一般的需求都是使用新的程序,很少需要用到之前已打开的文件,这就意味着我们需要去关闭那些我们在新的程序中用不到的文件(闲置文件)。更严重的情况是如果不关闭某些文件,由于替换之后文件描述符的丢失,会造成一些文件始终无法关闭的情况,造成不可预测的结果。但是如果已经打开了好多文件,手动去一个一个的关闭肯定很麻烦,于是我们可以在open这些文件的时候在open的flags里也加上O_CLOEXEC使得在进程替换时能够直接识别到文件描述符中有O_CLOEXEC开启的标志,在进程替换之前就把各个相应的文件全部自动关闭,省心省力。
那么绕了一圈,在dup3中设置flags的用途又是干嘛的呢?
由于给newfd加了O_CLOEXEC标志,进程替换前会将newfd会被自动关闭,也就是把重定向给关闭了!然后替换的新的程序继承的文件描述符就没有相关的重定向了。
⌨整点代码测试一下:
#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define PATH_NAME "log.txt"
using namespace std;
int main()
{
umask(0);
int fd =open(PATH_NAME,O_CREAT | O_RDWR | O_TRUNC,0600);
if(fd<0)
{
cerr << strerror(errno) << endl;
exit(2);
}
const char *str = "Hello, Kangkang, this is Michael";
//一旦发生进程替换就把对应的重定向newwfd关闭,相当于cout无法被正常使用了(1所对应的文件指针就变成nullptr)。
dup3(fd,1,O_CLOEXEC);
cout<<str<<endl;
execlp("ls","ls","-al",nullptr);
cerr<<"something wrong"<<endl;
close(fd);
return 0;
}
💻:
观察到发生了写入错误,并提示错误的文件描述符,这不就是因为fd:1被关闭了吗?并且由于文件异常关闭,导致log.txt中并没有数据写入。这里我是为了方便观察才使用的标准输出重定向做的例子,实际使用的时候基本都不会出现这种进程异常结束的现象,只会单纯地把重定向取消罢了。
🚩总结
重定向的知识并不简单,首先得清楚文件描述符的概念,其次得知道进程替换的具体应用细节。并且还得熟练掌握dup函数族,实际应用还是比较重要的。
我在整理这篇博客的时候也是又回头看了一下才疏通了知识脉络,希望正在学习的你能够从这篇文章中有所收获吧,有问题欢迎留言或私信,我们一起学习进步🐾