GNU/Linux C 库I/O缓冲机制 | 8月更文挑战

192 阅读4分钟

这是我参与8月更文挑战的第14天,活动详情查看:8月更文挑战

0.引子

对于使用C语言开发者着而言,I/O库可能是最为经常的使用的程序库,当你需要调试的时候可能最为经常使用的就是printf打印了。可是不知道大家,是不是遇到过这样一个问题,那就是我明明打印了,为什么终端里没有输出呢?为什么日志没有存到文件里?我也遇到过这样的问题,当时也没有搞清楚问题的原因。今天,在读UNIX高级环境编程的fork一节时,意外的收获了对于该问题最终的解释,一切的原因都是因为I/O标准库的缓冲机制,下面就来详细介绍一下,I/O标准库的缓冲机制。

1.细节

在Linux系统下,write函数是无缓冲的,I/O标准库是带缓冲的,并且,I/O在不同的情境下,缓冲方式也是不同。首先通过一个例子说明一下,I/O标准库的缓冲机制。

#include "apue.h"

#if defined(MACOS)
#define _IO_UNBUFFERED	__SNBF
#define _IO_LINE_BUF	__SLBF
#define _IO_file_flags	_flags
#define BUFFERSZ(fp)	(fp)->_bf._size
#else
#define BUFFERSZ(fp)	((fp)->_IO_buf_end - (fp)->_IO_buf_base)
#endif

void	pr_stdio(const char *, FILE *);

int
main(void)
{
	FILE	*fp;

	fputs("enter any character\n", stdout);
	if (getchar() == EOF)
		err_sys("getchar error");
	fputs("one line to standard error\n", stderr);

	pr_stdio("stdin",  stdin);
	pr_stdio("stdout", stdout);
	pr_stdio("stderr", stderr);

	if ((fp = fopen("/etc/passwd", "r")) == NULL)
		err_sys("fopen error");
	if (getc(fp) == EOF)
		err_sys("getc error");
	pr_stdio("/etc/passwd", fp);
	exit(0);
}
	
void
pr_stdio(const char *name, FILE *fp)
{
	printf("stream = %s, ", name);

	/*
	 * The following is nonportable.
	 */
	if (fp->_IO_file_flags & _IO_UNBUFFERED)
		printf("unbuffered");
	else if (fp->_IO_file_flags & _IO_LINE_BUF)
		printf("line buffered");
	else /* if neither of above */
		printf("fully buffered");
	printf(", buffer size = %d\n", BUFFERSZ(fp));
}

==*注意,==*在打印缓冲区状态之前,先对每个流执行I/O操作,第一个I/O操作通常造成为该流分配缓冲。结构成员_IO_file_flags、_IO_buf_base、_IO_buf_end和常量_IO_UNBUFFERED、_IO_LINE_BUFFERD是Linux中GNU标准I/O库定。应当了解,其他UNIX系统可能会有不同的标准I/O库实现。 如果运行两次上面的示例程序,一次使三个标准流与终端相连接,另一次使它们重定向到普通文件,则所得结果是:

	$./buf 							stdin、stdout、stderr都连至终端
	
	enter any character

	one line to standard error
	stream = stdin, line buffered, buffer size = 1024
	stream = stdout, line buffered, buffer size = 1024
	stream = stderr, unbuffered, buffer size = 1
	stream = /etc/passwd, fully buffered, buffer size = 4096

	$./buf < ./buf.c > std.out 2> std.err   	三个流都重定向,再次运行该程序
			
	enter any character
	stream = stdin, fully buffered, buffer size = 4096
	stream = stdout, fully buffered, buffer size = 4096
	stream = stderr, unbuffered, buffer size = 1
	stream = /etc/passwd, fully buffered, buffer size = 4096

从中可以见,该系统默认的情况是:

  1. 当标准输入、输出连至终端时,它们时行缓冲的。航缓冲区长度是1024字节。(行缓冲区的输出条件是:缓冲区满或者遇到换行符,这就是我们为什么在printf的末尾需要添加'\n'的原因了)。注意,这并没有将输出入、输出的长度限制为1024字节,这只是缓冲区的长度。如果要将2048字节的行写到标准输出,则要进行两次write系统调用
  2. 当将这两个流重定向到普通文件时,它们就会变成是全缓冲的,其缓冲区长度是该文件系统优先选用的I/O长度(从stat结构中得到的st_blksize,也就是文件系统块的大小)。
  3. 标准出错在任何情况下都是无缓冲的,而对于普通文件按照系统默认是全缓冲的。
  4. 对于全缓冲而言,缓冲区清空或者说冲刷的时机是缓冲区满或者手动调用I/O标准库的fflush函数,该函数会将标准I/O库所使用的缓冲区的内容全部输出,但是并不代表这些内容已经写到文件系统中了,为了确保内容全部保存到物理文件中,需要再次调用sync或者fsync函数。
  5. 文章开头说write系统调用是无缓冲的,为什么这么设计呢?其实,这是Unix系统的设计精髓”机制与策略分离原则“,Unix提供了write这种与文件系统或者设备通信的机制,而标准I/O库就是基于该机制实现的带缓冲的策略。
  6. 通过上面的分析文章开头所讲的那些奇怪的问题相信大家都应该理解是什么原因了,所以看似简单的问题,其实背后隐藏了太多太多的学问了,以后不能轻视任何一个看似简单的"小问题”了,以小见大。