2.7 流缓冲
每一个流都有一个输入输出缓冲区。写入流的字符并不立即写到文件中,而是先在缓冲区中聚集为一块,然后异步地以块为单位传送到文件。类似地,从流读出的字符也不是逐个地从文件中读出,而是以块为单位从文件读到缓冲区,然后从缓冲区传送给进程。这种处理方式称为缓冲。
采用缓冲的目的是为了减少调用低级I/O函数(如read()和write())的次数,因为这些真正读写文件的函数是系统调用,它们是较费时间的操作。例如,对于存储在硬盘上的文件,当进程用read()或write()读写数据时,设备驱动程序必须将数据在文件中的地址转换成硬盘的物理磁道号、卷宗号以及扇段号。之后设备必须移动磁头至相应的卷宗并等待磁盘的相应扇段旋转至磁头之下。一切准备好了之后才能从磁盘开始读写数据。显然,每读写一个或几个字符便导致执行这一串的动作是极不合算的。利用缓冲处理则不必为每读写一个字符而频繁地与外部设备打交道,同时还可以实现异步I/O,即在CPU运行程序的同时从外设传输数据,从而提高输入输出的效率。
标准I/O库函数自动地为我们管理缓冲区,使得我们无须过问何时该从文件中读一块数据至缓冲区和与特定设备有关的细节问题。
流有三种不同的缓冲类型:
1)全缓冲。在这种情况下,真正的I/O操作每次以整个缓冲区为单位读写数据,缓冲区的大小一般为BUFSIZ。对于输出,只有当缓冲区满了时才传送它至文件;对于输入,每次从文件读入数据直至缓冲区满为止。磁盘文件一般是全缓冲的。
2)行缓冲。在这种情况下,仅当在输入或输出中遇到换行符时才执行真正的I/O操作。行缓冲一般用于终端之类交互设备的流。例如,如果我们用fputc()输出15个非换行字符,然后输出一个换行符,则只有当最后这个输出换行符的fputc()被调用后,前面输出的 15个字符才能真正出现在终端上。
3)无缓冲。流不设置缓冲区,字符单个地读出或写入。
UNIX系统对新打开的流采用如下默认缓冲类型:
标准错误流总是无缓冲的。这是为了使得错误信息能及时显示出来。这意味着如果用fputc()输出15个字符至代表错误流的终端,则每一个字符都将在函数被执行后立即出现在终端上。
其他的流若引用交互设备则是行缓冲的,否则是全缓冲的。
这种自动默认选择给予输入输出文件或设备一种最方便的缓冲方式。不过,如果不满意这种默认缓冲的话,也可以用如下函数设定自己的缓冲区及希望的缓冲类型和大小。
#include <stdio.h>
void setbuf(FILE stream, char buf);
int setvbuf(FILE stream, char buf, int type, size_t size);
这两个函数必须在流已打开后且先于其他任何操作执行之前调用。
setbuf()用于打开或关闭流stream的缓冲。为了打开缓冲,参数buf必须指向一个长度为BUFSIZ的缓冲区。BUFSIZ 是系统定义的宏常数,它的值至少为256。通常在此函数调用之后流将变成全缓冲的,但如果流是与终端设备相连的话,则有的系统将改变它为行缓冲的。为了关闭缓冲,参数buf必须是NULL。
用setvbuf()可以明确地指定想要的缓冲类型。缓冲类型由参数type指定,它可取如下三种值之一,它们都是定义在中的常数。
_IOFBF 全缓冲
_IOLBF 行缓冲
_IONBF 无缓冲
如果指定无缓冲类型,setvbuf()将忽略参数buf和size;否则buf和size 可以任选地指定缓冲区及其大小。
如果用NULL作为buf的值,setvbuf()会自动地为流分配适当大小的缓冲区。所谓适当大小是指与此流相连文件的stat结构成员st_blksize指定的值(4.1.1节)。如果系统不能为流确定这个值(例如,当流与设备或管道相连时),则分配BUFSIZ长度的缓冲。当流被关闭时,这样分配的缓冲区将被自动释放。否则,buf应当是至少能容纳size个字符的一个数组。setvbuf()使用此数组作为流缓冲区,并释放标准I/O库原来分配的缓冲区。对于这个数组我们应当注意以下两点:
只要流是打开的,就不能释放该数组的空间。通常应当静态地分配此数组,或者用malloc()为它分配空间。用自动数组作为缓冲区是不好的,除非在退出说明该数组的程序块之前关闭文件。
流I/O函数将这个数组用于内部目的。当流正用它作为缓冲目的时,我们不能直接访问该数组的内容。
setbuf()实际上是setvbuf()的特例,它等价于
setvbuf(stream, buf, buf?_IOFBF:_IONBF, BUFSIZ);
术语刷新表示将缓冲区中的数据写出到文件中。通常,缓冲区中的数据在下述情况下会自动刷新:
1)当流被关闭时。
2)当调用exit()终止程序时(5.4节)。
3)若流是行缓冲的,当写出一换行符时。
4)当企图输出而缓冲区已经满了时。
5)无论何时对流的输入操作导致它实际从文件读数据时。
例如,在多数系统上,行缓冲区的大小通常是固定的,因此,如果在输出换行符之前一次输出的字符太多以致缓冲区满了时,尽管还未输出换行符,系统也会自动刷新缓冲区中的内容。这是上述第4种情形的例子。第5种情形的一个例子是:当用printf()输出不带换行符的一个字符串至终端之后,若紧接着调用从终端读数据的函数,则也导致缓冲区的输出立即被写到终端。这就是为什么用printf()输出不带换行符的字符串时,有时候它能立即出现在终端上(因为其后跟有输入操作),而有时候它却必须使用fflush()才行。
如果想在其他时刻刷新缓冲区的内容,则要显式地调用fflush()函数。
#include <stdio.h>
int fflush(FILE *stream);
fflush()刷新流stream的缓冲区。如果stream是一空指针NULL,fflush()将刷新所有已打开输出流的缓冲。
虽然标准I/O库函数自动地为我们管理I/O缓冲区,但让人感到迷惑然而也最简单的问题却常常是由缓冲引起的。例如,当设计用流进行输入输出的用户界面时,就必须了解流缓冲是怎样工作的,否则可能会发现输出(如显示程序进展或提示性的消息)并不像所预期的那样,甚至出现其他未曾料到的行为。
例2-8 程序2-8是一个由于未注意到缓冲的作用而导致输出行顺序不对的例子。
程序2-8 未注意缓冲作用导致错误之例
#include "ch02.h"
#include "y_or_n_ques.c"
int main()
{
int answer;
printf ("1: This is a buffer test program. ");
// fflush(stdout);
fprintf(stderr,"2: --test message\n");
answer = y_or_n_ques("3: Hello, Are you a student?");
if(answer == 1) / 响应回答 /
printf("4: Hope you have high score.");
else
printf("4: Hope you have good salary.");
// fflush(stdout); */
fprintf(stderr,"5: bye!\n");
}
这个程序简单地提出一个问题,然后对回答做出反应。程序的本意是想按照打印语句中第一个数字编号的顺序输出信息,但为了表现输出被缓冲的情况,我们在printf中故意没有加上换行符,并且在其后紧接着加入了向标准错误流输出的语句。由于标准错误流是无缓冲的,这使得它的输出将先于printf的输出而出现在终端上。运行这个程序有如下结果:
$ a.out
2: --test message
1: This is a buffer test program. 3: Hello, Are you a student? n
5: This is last line.
4: Hope you have good salary. $
这种结果不是我们预期的,我们原本希望按程序执行顺序输出每一行。为了如我们所愿,应当在输出中加入适当的换行符,或者在适当位置加入fflush()调用,例如,去掉程序中对fflush()调用的注释。