代码&&现象
先来看一份代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
//C Library
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
const char *s1 = "hello fwrite\n";
fwrite(s1, strlen(s1), 1, stdout);
//system call
const char *s2 = "hello write\n";
write(1, s2, strlen(s2));
return 0;
}
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
//C Library
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
const char *s1 = "hello fwrite\n";
fwrite(s1, strlen(s1), 1, stdout);
//system call
const char *s2 = "hello write\n";
write(1, s2, strlen(s2));
fork();
return 0;
}
第二份代码多了一个fork,导致重定向的时候,tag.txt里C库函数的输出进行了两次。
为什么要存在缓冲区?
我们无论往显示器里读写、文件里读写、网卡里读写等等,都是在访问外设,而一旦访问外设速度就很慢。假设不存在缓冲区的话当前进程在进行IO时只能够当前进程去等待,就会导致当前进程的效率降低;而此时如果当前进程不自己等待,而是将需要IO的数据拷贝到缓冲区里,由缓冲区决定IO的时间、次数等,这样就会提高当前进程的效率。
缓冲区的刷新策略
缓冲区的刷新策略分为:
- 立即刷新——无缓冲
- 行刷新——行缓冲
- 缓冲区满刷新——全缓冲
但有两个特例:
- 用户强制刷新——调用fflush函数
- 进程退出的时候自动刷新
立即刷新策略一般很少用,毕竟这样就跟没有缓冲区一样,想立即刷新的时候我们可以调用fflush函数。
行刷新是对于显示器(人)来说的,因为人的阅读习惯是一行一行读,所以会专门设定一个行刷新策略。
缓冲区满刷新是效率最高的,因为这样IO的次数最少,也是除了显示器外最常用的策略。
C缓冲区&&OS缓冲区
上面说的缓冲区是以C库函数为背景来说的,但其实OS也有一层缓冲区,我们在调用printf等函数时,会将内容拷贝到C库函数的缓冲区里,然后等该缓冲区里的内容刷新的时候其实是拷贝到了OS的缓冲区里,最终由OS的缓冲区决定IO。
C库函数的缓冲区在FILE结构体里,没错,就是我们用fopen打开文件时返回回来的FILE指针对应的结构体里,该结构体还封装了文件描述符fd等等很多东西。
OS的缓冲区我们无法观察到,因为这个极其复杂,OS需要根据整个机器的内存空间等因素综合考虑再决定IO。
原理
这里其实是有一个缓冲区的存在,而根据上面的现象,其实可以锁定缓冲区一定在C库函数的包装中,而不在系统调用函数中,因为C库函数都是调用系统调用函数的。
在未进行重定向时,C库函数和OS函数都往显示器里打印,而显示器采用的是行刷新策略,而我们的代码也添加了\n
,所以当每次执行输出代码的时候总会立即刷新到显示器里,最终fork的时候C库缓冲区已经是空的了,而OS函数根本没有往C库缓冲区里拷贝,而是拷贝到了OS缓冲区里。
当进行重定向的时候,是往文件里写入,采用的刷新策略为缓冲区满刷新,所以即使我们的代码里有\n
,也不会立即写入到文件里,所以fork的时候,C库缓冲区里是有我们的内容的,子进程会拷贝父进程的PCB,父子进程指向同一份数据代码,当有任意一个进程退出时,这时该进程就会刷新缓冲区,而这时就会发生写时拷贝,刷新的内容会拷贝一份再进行刷新,所以最终会刷新两次,但是OS函数没有往C库缓冲区里拷贝,所以也就不参与上面的过程,所以只刷新一次。