目录
- 了解重定向与缓冲区
- 重定向的本质
- dup、dup1、dup2函数
- 缓冲区刷新策略
- stderr
了解重定向与缓冲区
想要了解重定向,先观察下面代码现象
int main()
{close(1);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);printf("fd:%d\n", fd);fprintf(stdout, "fprintf: fd:%d\n", fd);fflush(stdout);close(1);return 0;
}
可以发现,本应该在屏幕中显示的文本确写进了文档里面。
1.为什么log.txt文件的文件描述符是1?
这跟文件描述符的分配规则有关。每个进程新打开一个文件,其内核会给返回一个:files_struct数组中,当前没有被使用的最小的一个下标作为文件描述符。每个进程会默认打开三个文件:stdin、stdout、stderr,分别对应的文件描述符为0、1、2.又因为上面代码关闭了1(stdout),在打开新文件log.txt时,内核会根据分配规则,把整数1作为该文件的描述符。
2.为什么向stdout写入的数据会在log.txt中?
我们注意到代码printf()
和fprintf()
明明是向显示器即stdout文件输入信息,最后却发现写入到log.txt
里面去了,本来想打印在屏幕上面却打印在了文件里面,这不就是典型的重定向现象吗。这是为什么呢?
在C语言中,stdout
是个FILE*
结构体类型,其封装的文件描述符默认为1。printf()和fprintf(stdout)实际上是在向文件描述符为 1 的文件写入数据。底层的系统调用write()
只认文件描述符。此时文件描述符为 1 的文件是log.txt,所以数据会写入到该文件中。
再观察下面代码:
int main()
{close(1);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);printf("fd:%d\n", fd);fprintf(stdout, "fprintf: fd:%d\n", fd);//fflush(stdout);close(1);return 0;
}
我们把刷新缓冲区的代码注释掉,发现,文件中不存在文本数据了。
我们知道fflush()
的作用是刷新缓冲区。出现上述代码现象的原因可能是因为数据在**close(1)**之前还在缓冲区内。
这里需要知道的是fflush()
的作用是将用户级缓冲区的数据刷新到内核级的缓冲区。我们已经知道什么时内核级缓冲区,那什么是用户级缓冲区呢?
用户级缓冲区:
我们知道stdout
是一个struct FILE*
的结构体指针,这个结构体中不仅存在保存文件描述符的变量,而且存在一段空间,用作 缓冲区,也就是说,我们在语言层面上也有一个自己的缓冲区,我们使用库函数printf()
或者fprintf()
向文件写入数据时,会先加载到这个用户级缓冲区里面,并不会直接加载到内核级缓冲区中。所谓的用户级缓冲区其实就是结构体中的一个指针,指向malloc的一段空间。一旦我们用fflush将用户级缓冲区里的数据流刷新到内核级缓冲区之后,剩下的工作就是内核去完成的了,我们就不需要关心了。
所有上述代码没有写入log.txt是因为我们关闭了文件,数据就丢失了,进程结束前自动刷新缓冲区也没有用了。
那么为什么要给程序留一个用户级缓冲区呢?
用户级缓冲区通常用于提高数据传输的效率,减少应用程序与操作系统之间的频繁交互。不必要每次数据交互都访问内核缓冲区,而是可以先存在一个区域中,达到一定量了之后再刷新到内核缓冲区。
对于操作系统来说,如果没有用户级缓冲区,我们每次向文件读写数据都要访问内核,间接加剧了内核访问磁盘的次数。对于用户来说,每次读写操作都要等待操作系统响应,这样无疑会降低用户的体验。所以用户级缓冲区可以提高用户的体验,也可以提高数据传输的效率,
内核级缓冲区于用户级缓冲区的区别:
- 作用位置和范围
用户级缓冲区位于进程的地址空间,连接的是用户程序和内核缓冲区。内核级缓冲区位于操作系统内核的地址空间,连接的是操作系统内部和磁盘文件。 - 性能控制
用户级缓冲区的具体实现方式由用户决定,比较灵活,效率也跟具体的实现方式有关。而内核级缓冲区的实现方式受到操作系统设计影响,一般实现方式是固定的,比较稳定可靠。 - 访问权限
用户级缓冲区由进程程序管理和控制,应用程序可以直接访问和操作用户级缓冲区,而无需进行系统调用。内核级缓冲区由操作系统内核管理和控制。只能通过系统调用来访问和操作。
重定向的本质
第一个代码的行为和重定向的行为一样,其实重定向差不多就是这样,重定向的本质就是改变文件描述符的内容,文件描述符的内容就是文件的地址,重定向就是改变fd_array[fd]的指向。
dup、dup1、dup2函数
close()函数补充:
close会不会直接关闭文件,取决于是否还有其它文件描述符指向该文件 。 这里采用了引用计数的原理。每个被打开的文件都会有一个计数器记录该文件被引用的次数。每多一个描述符指向该文件,该文件的引用计数器就会加一。反之,就会减一。一旦引用计数器为0,表示没有可用的描述符指向该文件,该文件也就才能真正地关闭。
所以close(fd)的本质,是清空fd再使文件fd的引用计数器减一。
重定向并不需要向第一个代码那样麻烦,系统调用提供了函数帮助我们进行重定向。
只介绍dup2
#include<unistd.h>
int dup2(int oldfd,int newfd);
int dup2(int oldfd,int newfd);
函数是Unix/Linux系统中常用的系统调用函数,跟dup类似,用于复制文件描述符,并将其指定为新的文件描述符。但不同的是,dup2函数可以将一个已存在的文件描述符复制到另一个文件描述符上,并允许自定义新文件描述符的编号。这在需要重定向文件描述符或管理多个文件描述符的场景中非常有用。
就是将newfd的文件描述符与oldfd保持一致。newfd = oldfd。两个文件描述符都指向了同一个。
char* filename = "log.txt";
int main()
{int fd = open(filename, O_RDWR | O_CREAT, 0666);close(1);dup2(fd, 1);return 0;
}
上述代码就是将文件描述符 1 和 fd 都指向了log.txt。
缓冲区刷新策略
缓冲区其实就是一段内存空间,存在的目的是为了给上层提供高效的IO体验,间接提高整体的效率
缓冲区的刷新不同的情况是不一样的。
- 立即刷新,即
fflush()
- 行刷新,显示器就是行刷新,所以一旦我们写入的字符串有
\n
就会立刻输出 - 全缓冲,普通文件使用的是全缓冲策略,在缓冲区写满才进行刷新。
- 特殊情况,进程退出,系统自动刷新。
目前说的都是针对的用户及缓冲区。
需要知道的是,创建子进程后,缓冲区的内容子进程同样继承
int main()
{fprintf(stdout, "hello fprintf\n");const char* str = "hello write\n";write(1, str, strlen(str));fork(); //创建子进程return 0;
}
进行重定向后:此时是向 普通文件 中打印内容,因为普通文件是写满后才能刷新,并且 fprintf 有属于自己的缓冲区,这就导致 fork()
创建子进程后,父子进程的 fprintf()
缓冲区中都有内容,当程序运行结束后,统一刷新,于是就是打印了两次 hello fprintf
注:系统级接口是没有自己的缓冲区的,直接冲刷至内核级缓冲区中,比如 write()
,所以创建子进程对 write()
的冲刷没有任何影响
stderr
我们知道stderr和stdout同样都是映射到显示器,之所以这样设计,是因为要打印错误信息,当向stderr流输入信息的时候,我们可以将这个重定向到我们自己的文件中,这样设计可以将错误信息输出到我们自己的文件中,方便我们debug。perror()
函数的本质就是向文件标识符2中打印错误信息。
./a.out 1>ok.log 2>err.log //1重定向到ok.log,2重定向到err.log
./a.out 1>all.log 2>&1 //1和2全部重定向到all.log