以前写C程序输入输出并没有特意关注过I/O缓冲区的问题。最近在学习fork的时候,发现I/O缓冲区是需要关注的一个细节问题。I/O缓冲区是Unix支持的一项标准,并且得到ISO C标准的支持。
其作用:
标准I/O库提供缓冲的目的是尽可能减少外部设备数据读写read和write的次数,从而加快CPU的工作效率。
缓冲区的大小:
缓冲区的大小等于外部块设备的一个块长大小时效率最好。所谓块设备,就是以一块一块的数据存取的设备;字符设备是一次存取一个字符的设备。磁盘、内存都是块设备,字符设备如键盘和串口。
如果不想使用系统默认的缓冲区大小,可以使用setvbuf函数来自定义缓冲区大小。也可以用setbuf函数开/关缓冲机制。
缓冲区冲洗(flush):
缓冲区是一个数据暂存区,那么冲洗的意思就是“清仓”,将缓冲区当前的内容全部写到外部设备上去。该操作可由标准I/O例程自动flush,或者由程序员调用fflush函数。
程序遇到“\n”(仅限行缓冲),或是EOF,或是缓冲区满,或是文件描述符关闭,或是主动flush,或是程序退出,就会把数据刷出缓冲区。
缓冲类型:
标准I/O提供了三种类型,下面说下他们的概念和常用场合:
1、全缓冲
在I/O操作时,只有当I/O缓冲区被填满时,才进行真正的I/O操作。对于全缓冲的缓冲区可由标准I/O例程自动刷新。还有一种方法就是调用函数fflush进行刷新。
对于驻留在磁盘上的文件,通常由标准I/O库实施全缓冲。
2、行缓冲
在I/O操作时,输入输出遇到换行符'\n'时进行,才进行真正的I/O操作。对于行缓冲,要注意量点:第一,标准I/O每一行缓冲区的最大长度是固定的,所以只要填满了缓冲区,即使没有遇到换行符也要冲洗缓冲区。第二,当使用行缓冲作输出缓冲区时,若输入流是不带缓冲区的流或者一个行缓冲区的流,那么要直接冲洗输出的行缓冲区。前者是因为不缓冲区的输入要求必须马上输出,后者是因为已经输入时做好了缓冲,不必重复缓冲。
当流涉及到一个终端时,比如标准输入stdin和标准输出stdout,通常使用行缓冲。scanf、printf不直接调用系统调用,在用户空间维护一块行缓冲区,在适当的时候调用read、 write读写缓冲区。
3、不带缓冲
底层函数数据读写函数read和write是不带缓冲区的,直接调用系统调用进行外部读写。标准出错流stderr是不带缓冲的,这样能使得错误信息尽快显示。
例子:下面加入sleep的例子有助于看出标准输出行缓冲的作用。
#include <stdio.h>
void main()
{
for(int j=0;j<3;j++)
{
for(int i=0;i<5;i++)
{
printf("-");
sleep(2);
}
sleep(2);
printf("@\n");
}
}
注意:虽然上面说的都是输入输出。但实际上主要是在将标准输出,即内存数据写到外部终端设备。这是因为内存处理速度快,终端显示设备速度慢。与之相对应的情况,从键盘设备读到内存中,之间也有行缓冲区。所区别的是,当冲洗缓冲区的时候,缓冲区的内容会被丢弃,而不是读到内存中。这个并没有成为IOS C标准。所以APUE中没怎么提到。 (源自APUE第3、5章)