关于流和缓冲区的理解

时间:2022-06-14 05:55:05

0. 序曲

写这篇短文的起因是,前两天想去天大的acm在线系统找几道题做做。为什么呢?因为本人天大毕业,这个天大呢可是中国最早的大学,原名北洋大学堂,这可绝对是货真价实的第一所大学。给大家推荐推荐啊,学风那是相当的好。

扯多了,还是回到本来的话题上。上了acm系统之后,就先看了1001。那道题的意思是输入一些正整数(以EOF结束),把对应的字符输出。这个简单,程序很快就出来了:


 

#include <stdio.h>
int main()
{
int c;
while(scanf("%d", &c) != EOF)
{
putchar(c);
}
return 0;
}

 


程序运行,输入103 102 105 107<enter>

输出gfik。

当时运行完之后马上想,为什么不是输入一个数字马上输出一个字符呢,因为看程序确实是这样的逻辑,只要不是EOF,就会输出。又一想,对了,是缓冲的问题。想起来APUE里边说得stdin应该是行缓冲的,另外,可以用setbuf,setvbuf设定流的缓冲。于是想将stdin设成无缓冲的。于是程序变成这样:


#include <stdio.h>
int main()
{
int c;
setbuf(stdin, NULL);
while(scanf("%d", &c) != EOF)
{
putchar(c);
}
return 0;
}


可是编译运行,还是老样子,没有变化。想了想,没想出是啥原因,于是开始google和APUE。终于算是明白了些,整理在这儿。


声明:

本文很大部分内容来自APUE--UNIX环境高级编程。

1. 缓冲类型。

标准库提供缓冲是为了减少对read和write的调用。提供的缓冲有三种类型(整理自APUE):

  • 全缓冲。

在这种情况下,实际的I/O操作只有在缓冲区被填满了之后才会进行。对驻留在磁盘上的文件的操作一般是有标准I/O库提供全缓冲。缓冲区一般是在第一次对流进行I/O操作时,由标准I/O函数调用malloc函数分配得到的。

术语flush描述了标准I/O缓冲的写操作。缓冲区可以由标准I/O函数自动flush(例如缓冲区满的时候);或者我们对流调用fflush函数。

  • 行缓冲

在这种情况下,只有在输入/输出中遇到换行符的时候,才会执行实际的I/O操作。这允许我们一次写一个字符,但是只有在写完一行之后才做I/O操作。一般的,涉及到终端的流--例如标注输入(stdin)和标准输出(stdout)--是行缓冲的。

  • 无缓冲

标准I/O库不缓存字符。需要注意的是,标准库不缓存并不意味着操作系统或者设备驱动不缓存。

ISO C要求:

  • 当且仅当不涉及交互设备时,标准输入和标准输出是全缓存的。
  • 标准错误绝对不是全缓存的。

但是,这并没有告诉我们当标准输入/输出在涉及交互设备时,它们是无缓存的还是行缓存的;也没有告诉我们标准错误应该是行缓存的还是无缓存的。不过,大多数实现默认的缓存类型是这样的:

  • 标准错误总是无缓存的。
  • 对于所有的其他流来说,如果它们涉及到交互设备,那么就是行缓存的;否则是全缓存的。

2. 改变默认缓存类型

可以通过下面的函数改变缓存类型(摘自APUE):

void setbuf(FILE *restrict fp, char *restrict buf);
int setvbuf(FILE *restrict fp, char *restrict buf, int modesize_t size);

这些函数必须在流打开之后、但是未对流做任何操作之前被调用(因为每个函数都需要一个有效的文件指针作为第一个参数)。

利用setbuf,可以打开或者关闭缓存。为了打开缓存,buf参数必须一个大小为BUFSIZ的缓存,BUFSIZ是定义在stdio。h中的常量。&amp;lt;&amp;lt;ISO/IEC 9899&amp;gt;&amp;gt;要求:BUFSIZ至少为256。如果要关闭缓存,可以将buf设成NULL。

利用setvbuf,我们可以设定缓存类型。这是通过mode参数指定的。

关于这两个函数,可以看下表(摘自APUE):


Function

mode

buf

Buffer and length

Type of buffering

setbuf

 

non-null

user buf of length BUFSIZ

fully buffered or line buffered

NULL

(no buffer)

unbuffered

setvbuf

_IOLBF

non-null

user buf of length size

fully buffered

NULL

system buffer of appropriate length

_IOFBF

non-null

user buf of length size

line buffered

NULL

system buffer of appropriate length

_IONBF

(ignored)

(no buffer)

unbuffered

需要注意的是:如果在函数内为流分配了自动变量作为缓存,那么在退出之前需要将流关闭。因此最好让系统自己分配缓存,这些缓存在流关闭的时候会自动被释放。


3.如果清理输入缓存

关于这点可以参看comp.lang.c FAQ的Question12.26b:

Q: If fflush won't work, what can I use to flush input?

A: It depends on what you're trying to do. If you're trying to get rid of an unread newline or other unexpected input after calling scanf (see questions 12.18a-12.19), you really need to rewrite or replace the call to scanf (see question 12.20). Alternatively, you can consume the rest of a partially-read line with a simple code fragment like

while((c = getchar()) != '\n' &amp;&amp; c != EOF)
/* discard */ ;

(You may also be able to use the curses flushinp function.)

There is no standard way to discard unread characters from a stdio input stream. Some vendors do implement fflush so that fflush(stdin) discards unread characters, although portable programs cannot depend on this. (Some versions of the stdio library implement fpurge or fabort calls which do the same thing, but these aren't standard, either.) Note, too, that flushing stdio input buffers is not necessarily sufficient: unread characters can also accumulate in other, OS-level input buffers. If you're trying to actively discard input (perhaps in anticipation of issuing an unexpected prompt to confirm a destructive action, for which an accidentally-typed ``y'' could be disastrous), you'll have to use a system-specific technique to detect the presence of typed-ahead input; see questions 19.1 and 19.2. Keep in mind that users can become frustrated if you discard input that happened to be typed too quickly.

References: ISO Sec. 7.9.5.2
H&amp;S Sec. 15.2

4. 几点需要注意的地方

  • 对输入流进行fflush操作是无定义的。
  • 无缓存并不意味着一个个的那样处理输入,而是说当操作系统返回它们时,对于标准库函数来说它们是立即可用的。因为还可能有操作系统级甚至是硬件级的缓存,这些并不是setbuf可以控制的。
  • 另外可以参考这里(我就是最先从这里开始看的)。还有这里。我从后面那个链接摘录一些重要的下来:

setbuf() has to do with the delivery of bytes between the
C library FILE* management layer and the OS I/O layer.

Calls to fread(), fgets(), fgetc(), and getchar() work within
whatever FILE* buffered data is available, and when that data
is exhausted, the calls request that the FILE* buffer be refilled
by the system I/O layer.

When full buffering is turned on, that refill operation results in the
FILE* layer requesting that the operating system hand it a full
buffer's worth of data; when buffering is turned off, that
refill operation results in the FILE* layer requesting that the
operating system return a single character.

...setting an input stream to be unbuffered
does NOT tell the operating system to tell the device driver
to go into any kind of "raw" single-character mode. There are
system-specific calls such as ioctl() and tcsetterm() that
control what the device driver will do.