对 printf 缓存方式的思考 Shepard-Wang

我们知道 printf 默认是 行缓冲 。C stdio 函数的缓冲方式有三种:

  • _IOFBF全缓冲:当缓冲区为空时,从流读入数据。或者当缓冲区满时,向流写入数据。对磁盘文件通常使用全缓存。
  • _IOLBF行缓冲:每次从流中读入一行数据或向流中写入一行数据。涉及终端时(如标准输出和标准输入)
  • _IONBF无缓冲:直接从流中读入数据或直接向流中写入数据,缓冲设置无效。stderr 尽快让用户看到错误。

下面的例子中,我们先调用 printf 再调用 sleep 但是 printf 的数据等到进程结束才会输出

#include<stdio.h>    
#include<unistd.h>    
    
int main(void){    
    
  printf("maomaochong");                                                                                                               
  sleep(3);    
    
  return 0;    
}    

这是因为想让 printf 输出的字符串先缓存到了 C 维护的缓冲区中,满足下列一项条件后,才会将缓存写入对应的文件或流中(缓冲刷新):

  1. 缓冲区填满
  2. 写入的字符中有 \n
  3. 调用 fflush
  4. 调用 scanfgetchar 等输入函数要从缓冲区中读取数据时,也会将缓冲区内的数据刷新
  5. 使用 setvbuf 将缓冲方式改为无缓冲
  6. fclose 会刷新所有写缓冲
  7. 程序结束时

注意:网上有文章说 \r 也会导致缓冲区的刷新

Ubuntu 20.04 下测试并不会刷新,测试代码:

printf("123");
fflush(stdout);
printf("\r");
sleep(2);

可以看到 123 先输出,2 秒后输出的 123 才被 \r 清除。

\r 可以理解为回退行首的操作,会覆盖掉这一行之前的输出。

例 1

使用 setvbuf 调整缓冲方式

setvbuf(stdout, NULL, _IONBF, 0);
printf("123");
sleep(2)

了解了这些概念,接下来讲一下我遇到的问题。

这是一个有关重定向原理的实现

下面的程序从标准输入中读取 4 个字节的字符串,然后写入的标准输出:

int main()
{
   	char buf[1024];
	read(STDIN_FILENO, buf, 4);
	write(STDOUT_FILENO, buf, 4);
}
❯ ./main
hello
hell#   

将 1 号描述符关闭,然后打开一个文件,文件描述符 1 会分配给这个文件。无需修改后面的代码(需要加上 close),程序会变为标准输入读取,写入到从打开的文件中

int main()
{
    close(STDOUT_FILENO);
	int fd = open("new_file", O_RDWR|O_CREAT, 0644);
	if(fd == -1) PRINT_ERR("open");
    
    char buf[1024];
	read(STDIN_FILENO, buf, 4);
	write(STDOUT_FILENO, buf, 4);
}
❯ ./main
hell
❯ cat new_file
hell#  

接下来,我想尝试如果使用 printf 函数会不会也将字符串写入到了文件中:

int main()
{	
	close(STDOUT_FILENO);
	int fd = open("new_file", O_RDWR|O_CREAT, 0644);
	if(fd == -1) PRINT_ERR("open");

	printf("123\n");	

	sleep(5);
	return 0;
}

接下来就出现了问题,printf 并没有立刻输出 123 ,而是等到进程结束时才输出。

这里我就疑惑了,明明字符串有 \n,为什么没有立刻刷新缓存?

注意 这里我们没有写 close ,如果我们加上 close

int main()
{	
	close(STDOUT_FILENO);
	int fd = open("new_file", O_RDWR|O_CREAT, 0644);
	if(fd == -1) PRINT_ERR("open");

	printf("123\n");

	sleep(5);

	close(STDOUT_FILENO);

	return 0;
}

则缓冲区内容在程序结束后也不会写入文件,这是因为程序结束前已经把文件描述符关闭了,自然无法写入。

如果我们手动使用 fflush 刷新缓存,则可以立刻看到字符串写入了文件

int main()
{	
	close(1);
	int fd = open("new_file", O_RDWR|O_CREAT, 0644);
	if(fd == -1) PRINT_ERR("open");

	printf("123\n");
    fflush(stdout);

	sleep(5);
	return 0;
}

文件中没有输出的字符串一定是 C stdio 的缓存没有刷新到操作系统内核中,也就是说 write 函数没有被调用。

我认为可能是 close(1) 操作让 stdout_IOLBF 的属性失效了,所以我尝试为 stdout 重新设置行缓存:

int main()
{	
	close(1);
	int fd = open("new_file", O_RDWR|O_CREAT, 0644);
	if(fd == -1) PRINT_ERR("open");

    setvbuf(stdout, NULL, _IOLBF, BUFSIZ);
	printf("123\n");
    
	sleep(5);
    close(fd);
	return 0;
}

可以看到 printf 输出再 sleep 前写入了文件中。

这里有必要提一句,在文件上看到的内容不一定写到了磁盘中,也可能还在内核的页缓存中。