1. 缓冲区
1.1 概念
执行下列代码:
int main()
{
close(1);//重定向
int fd = open("log.txt",O_CREAT|O_WRONLY,0644);
printf("hello\n");
return 0;
}
查看log.txt发现里面打印了hello,但是如果把fd关闭:
int main()
{
close(1);重定向
int fd = open("log.txt",O_CREAT|O_WRONLY,0644);
printf("hello\n");
close(fd);
return 0;
}
会发现log.txt里面已经没有内容。这是为什么?
这就要涉及到缓冲区的问题:
1.2 C缓冲区
平时我们输入的内容其实都是输入到了C缓冲区里,而不是直接刷新。
而这个刷新的时间,就是进程退出的时候,会将C缓冲区的数据到OS缓冲区。
所以对于上面的例子来说,所谓的打印不过是把数据放到了C缓冲区中;随后再通过系统调用接口刷新到内核缓冲区,最后再刷新到外设中。
- 对于这个过程有几点值得提出:
- 这个所谓的C缓冲区在哪里?
存在于FILE指针结构体中。
之前说到FILE结构体里封装了fd,用于描述文件位置;其实里面还封装了有关维护C缓冲区的内容。
- 这个C缓冲区刷新到OS缓冲区的过程是如何实现的?
这其实相当于对一个文件进行写入,那么就肯定要用到fd,并且这个刷新过程是由固定的刷新策略决定的。
1.3 刷新策略
1.3.1 三种刷新方式
- 立即刷新(不缓冲,有数据就马上刷新);
- 行缓冲(通过\n区分,比如显示器打印);
- 全缓冲(缓冲区满了才刷新,比如磁盘)。
这几种方式对于OS缓冲区和硬件都适用。
而对于重定向:显示器重定向到文件中,其实就是说刷新策略由行缓冲变成全缓冲。
- 解析
而对于一开始的现象,可以这么解释:
- 如果有重定向
能显示出来是因为重定向到了log.txt文件内,策略变成了全缓冲,那么数据刷新过程就是先刷新到了C缓冲区,进程结束后(这是条件)就会刷新到OS缓冲区然后显示在文件内;这是没有close(fd)的时候;
但是有close(fd)以后,内容还在C缓冲区,由于全缓冲,还未刷新到OS缓冲区内(进程未结束),就没有显示内容。
- 如果没有重定向
如果没有重定向,无论有没有close(fd)都会输出,因为没有重定向就是打印到显示器,采用行刷新,只要C缓冲区识别到了\n就可以直接刷新。
而对于无法显示内容,我们可以采用刷新的方式将内容从C缓冲区刷新到OS缓冲区:
fflush(stdout);
运行下列代码:
int main()
{
const char* a = "hello a\n";
write(1,a,strlen(a));
printf("hello printf\n");
fprintf("hello fprintf\n");
close(fd);
return 0;
}
将可执行文件重定向到任一文件中,会发现:只有"hello a"被打印在了文件中,而其他都没有。
都是一起重定向到文件中,为什么其他两个没有显示?
这是close(1)的问题:由于重定向到文件中,那就是全缓冲策略,和前面一样,由于进程还没关闭就关掉了标准输出流1,导致内容在C缓冲区中刷新不出来。
那为什么"hello a"可以?
因为write是写方式,这是系统调用,不需要通过C缓冲区,自然也就不存在“缓冲区还未满,等待进程结束再刷新到OS缓冲区”的过程。
从上面的例子可以知道,平时我们打开文件以后需要关闭对应的文件描述符,就是因为不这样做数据刷新不出来。
1.3.2 系统调用的问题
观察下列代码
int main()
{
const char* a = "hello a\n";
write(1,a,strlen(a));
printf("hello printf\n");
fprintf("hello fprintf\n");
fork();//创建子进程
return 0;
}
可以看见最后创建了子进程。正常运行没有问题,都打印在了显示器;但是将其可执行程序重定向到任一文件后,发现不仅有"hello a",还有两倍的"hello printf"和"hello fprintf"。这又是为什么?
这是因为C函数打印本质上是在向stdout打印,而stdout是FILE*类型,会找到FILE结构体,从而找到其封装的对应缓冲区信息再找到对应缓冲区buffer,然后进行写入。
又因为这是父进程的缓冲区(子进程一模一样拷贝了一份),这个缓冲区是用户层提供,也就是C缓冲区,并且是重定向到文件里,刷新策略是全缓冲,所以等到进程结束后就会刷新到文件里。
但是这个过程相当于子进程对共有的内容进行了修改,就会引发写时拷贝,那么子进程也会刷新一次,导致父子进程都刷新一次就打印了两次。
本质还是因为刷新策略改变了,所以同样的采用强制刷新的方式可以只打印一次,这是因为直接将内容刷新到OS缓冲区,进程结束后C缓冲区里已经没有内容,也就不会引发写时拷贝,最后只打印OS缓冲区里的一次。
可是为什么write只有一份?
这就验证了平时我们说的缓冲区其实是用户层的C缓冲区,而不是存在于操作系统内核里,否则的话全部都会刷新两次。
- 类比
C++打印中的std::endl其实也是行缓冲的道理,endl作用就是刷新缓冲区的数据到显示器中。