文章目录
- 文件内核对象
- fd的分配问题
- 重定向的现象
- dup2
- 重定向的使用
- 标准输出和标准错误
前面对于文件有了基本的认知,那么基于前面的认知,本篇总结的是文件重定向的含义极其本质
文件内核对象
首先理解一下file
内核对象是什么,回顾一下下面这张图
站在用户的角度,对于文件的操作有这些诸如read
、write
这些系统调用,也有这些系统调用进行了一定的封装后诞生的C
语言库函数,这些系统调用的内部会在内存中进行一系列操作
首先,在进程运行起来后首先会创建出进程的PCB
,这是一定的,其次,进程的PCB
中有这么一块专门的区域名字叫作fd_array[]
,它的本质是一个指针数组,它会指向一个一个结构体,而每一个结构体中存储的是关于这个文件的信息,这个结构体叫做struct file
,而前面所说的文件描述符fd
,本质上就是这个指针数组的下标,操作系统默认会为用户打开三个文件,分别是标准输入,标准输出,标准错误,关于这三个文件的用处就是本文要重点解析的内容,也是理解文件重定向所必须要理解的内容核心
而这个file结构体就是用来管理这些系统调用中需要的一些操作,以读写函数为例,假设现在用户使用了读的系统调用,那么落实到内存中,进程的PCB
就会根据在系统调用中这个fd
索引,找到对应的文件结构体,进而就能找到这个文件的信息,而接下来关于这个文件的一系列操作,都要借助文件缓冲区来帮助,还是以读这个系统调用为例,当现在需要进行读取数据的时候,由冯诺依曼体系可以知道,CPU
不会和外设打交道,也就是说在磁盘中的文件信息是不能直接和CPU
进行交互的,因此要首先加载到内存中,因此就会加载到文件缓冲区中,而此时进程就会从文件缓冲区中读取所需要的信息,进而给用户或者是其他函数一个反馈
写数据也是一个道理,但是不管是写数据还是读数据,由于冯诺依曼体系的原因,是一定要先加载到内存中去的,无论读写,都需要加载到文件缓冲区
因此可以得到一个观点:在应用层对于数据的读写,本质上是将内核缓冲区的数据进行来回拷贝
由此结束了关于file
内核对象的理解,下面开始进行下一个模块的理解
fd的分配问题
来看示例代码
void testfd0()
{
char buffer[1024];
ssize_t s = read(0, buffer, 1024);
buffer[s - 1] = 0;
printf("%s\n", buffer);
}
调用了系统调用接口,从标准输入流中读取了信息,再进行输出
从中可以得出一个结论:进程默认打开了012三个文件,用户可以直接使用012进行数据的访问
// fd的分配原则
void testfd1()
{
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
printf("fd -> %d\n", fd);
close(fd);
}
输出:
[test@VM-16-11-centos File]$ ./myfile
fd -> 3
这是符合预期的,因为系统会默认打开三个文件,分别占用0,1,2
这三个位置,那如果把这三个文件关掉一个呢?
void testfd2()
{
close(0);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
printf("fd -> %d\n", fd);
close(fd);
}
输出:
[test@VM-16-11-centos File]$ ./myfile
fd -> 0
由此可以推测出,文件描述符的分配规则是:寻找最小的,没有被使用的数据的位置,分配给指定的打开文件
重定向的现象
关于什么是重定向,用下面的demo
来做一个示范
// 重定向
void redir1()
{
close(1);
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
if(fd < 0)
{
perror("open fail\n");
exit(1);
}
printf("fd-> %d\n", fd);
printf("stdout->fd: %d\n", stdout->_fileno);
fflush(stdout);
close(fd);
}
运行结果是,在屏幕上没有任何信息,但在log.txt
中居然出现了输出信息
那么这是为什么呢?从代码的目的来看,代码一开始就关闭了标准输出流文件,其次又打开了一个新的fd
文件,那么根据前面fd
的分配规则,这个新打开的fd
的下标就是1
,因为标准输出的下标是1
,这是可以确定的
其次,代码的下一步是打印了一些信息,但是这些信息都被打印到了文件中,但是理想状态下,信息应该要打印到显示器上,这说明了此时printf
这个函数的打印发生了一些变化,原本它的打印目标是显示器,现在却打印到了文件中,而从输出的第二条信息也能看出,现在要打印一下stdout
的文件描述符是多少,此时打印出来的结果是1
,而我们新打开的文件的下标也是1
,那么可不可以说,这个新打开的文件描述符取代了原来的stdout
呢?
答案确实是这样,与其说stdout
在系统中只认文件描述符,不如说它只认1
,在打印的时候它只会找1
,哪里有1
它就打印到哪里,在正常的逻辑中,stdout
对应的文件描述符是1
,而1
这个文件描述符会在进程启动的时候就自动打开标准输出文件,因此在进程执行到printf
这样的函数的时候,就会把信息打印到显示器上,这样就能让我们看到显示器上的内容,这是符合预期的
但是上面的示例代码中,却有了一个偷梁换柱感觉的操作,把原本文件描述符为1的文件换成了log.txt
,此时再打印的时候,进程只会机械性的去寻找文件描述符为1
的文件,因此就忽略了这个文件本身其实已经不是它了,而这恰恰也证明了在之前就一直输出的一个观点:Linux下一切皆文件,不管是显示器还是键盘还是网卡等等外部设备,操作系统都有自己的方法能把它变成内存中的一部分,这个方法前面也有提及,就是VFS
技术
用下面的这张图来说明一下刚才上面代码的一系列操作原理:
重定向的本质,就是修改特征文件fd的下标内容
上层的fd
不变,变化的是底层fd指向的内容,也就是所谓文件描述符级别的数组内容的拷贝
那这样的写法还是太奇怪了,每次都要把一个文件关闭再打开一个新的文件,作为系统理应给操作者提供这样替换文件描述符的系统调用,事实上也确实提供了这样的系统调用
dup2
DUP(2) Linux Programmer's Manual DUP(2)
NAME
dup, dup2, dup3 - duplicate a file descriptor
SYNOPSIS
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <fcntl.h> /* Obtain O_* constant definitions */
#include <unistd.h>
int dup3(int oldfd, int newfd, int flags);
DESCRIPTION
These system calls create a copy of the file descriptor oldfd.
dup() uses the lowest-numbered unused descriptor for the new descriptor.
dup2() makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following:
* If oldfd is not a valid file descriptor, then the call fails, and newfd is not closed.
* If oldfd is a valid file descriptor, and newfd has the same value as oldfd, then dup2() does nothing, and returns newfd.
After a successful return from one of these system calls, the old and new file descriptors may be used interchangeably. They refer to
the same open file description (see open(2)) and thus share file offset and file status flags; for example, if the file offset is modi‐
fied by using lseek(2) on one of the descriptors, the offset is also changed for the other.
The two descriptors do not share file descriptor flags (the close-on-exec flag). The close-on-exec flag (FD_CLOEXEC; see fcntl(2)) for
the duplicate descriptor is off.
简单来说,就是用oldfd
去替换newfd
,保留下来的是oldfd
,那么上面的代码就可以被改良成这样:
void redir2()
{
int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
if(fd < 0)
{
perror("open fail\n");
exit(1);
}
dup2(fd, 1);
printf("fd-> %d\n", fd);
printf("stdout->fd: %d\n", stdout->_fileno);
fflush(stdout);
close(fd);
}
显然这是可行的
重定向的使用
重定向其实并不陌生,在之前的学习中已经用过重定向,只是那是还没有建立起来一个基础的概念,先看下面的指令演示
[test@VM-16-11-centos File]$ echo "hello linux" > log.txt
[test@VM-16-11-centos File]$ echo "hello linux"
hello linux
这段指令的含义就是,把hello linux重定向到log.txt中,其实这样的操作符还有下面的这几种,一一进行介绍
>
这个操作符表示的是输出重定向,意思就是把内容输出到某个文件中,有些类似于以w
的方式打开一个文件并进行写入
>>
这个操作符表示的是追加重定向,意思就是把内容追加输出到某个文件中,有些类似于append
的方式进行写入
<
这个操作符表示的是输入重定向,表示把原来的内容输入输入到某个文件中,相当于是替换了标准输入流的文件
标准输出和标准错误
前面的知识已经足以理解为什么要有标准输入和标准输出,但是还有一个问题有待解决,标准错误的意义是什么呢?难道标准输出的信息还不够吗?
答案是确实不够,在对于大型项目的时候,会有很多的输出信息,这些输出信息有些是正常信息,有些是异常信息,而对于开发者来说他们需要的是错误信息,因此对于如何获取错误信息就显得至关重要,于是标准错误流信息就诞生了,对于正常来说可能没有太多的感觉,但是实际上,没有感觉的原因是因为标准错误和标准输出的文件对象都是显示器,而实际上这是可以被替换的,基于这个原理可以做出下面的测试代码
void teststderr()
{
printf("this is normal message\n");
perror("this is error message\n");
}
[test@VM-16-11-centos File]$ make
gcc -o myfile myfile.c -std=c99
[test@VM-16-11-centos File]$ ./myfile 1>out.txt 2>error.txt
利用上面的原理可以写出这样的测试代码,把正确信息存储到一个文件中,把错误信息存储到另外一个文件中,这样就能知道哪里是错误哪里是正确了
关于其他重定向
1>&2
意思是把标准输出重定向到标准错误2>&1
意思是把标准错误输出重定向到标准输出