glibc的三种缓冲策略
很多朋友可能在初次学习 C 语言时,如果是使用 Unix 环境,可能遇到过如下的问题
1 |
|
欸我运行完了怎么没输出呢?Google 一番才知道,这是由于所谓缓冲(Buffer)机制导致的
这个时候,你需要在 格式化串的结尾加一个\n
,就可以看到输出了
这是因为,标准输出是基于行缓冲(Line Buffered) 策略的,它遇到\n
时,会将缓冲区内容打包送出,这才可以看到要输出的字符串
除了行缓冲之外,glibc
还有两种策略,详见下节
三种缓冲策略
根据 Buffering Concepts (The GNU C Library)
存在三种缓冲策略,
unbuffered
不存在缓冲概念,字符一个一个被传输
line buffered
行缓冲,遇到换行符时,将缓冲区内容发出
fully buffered
全缓冲,当缓冲区内容到达指定大小时,将缓冲区内容发出
并且
Newly opened streams are normally fully buffered, with one exception: a stream connected to an interactive device such as a terminal is initially line buffered.
新打开的流应该都是全缓冲的,然而有一个例外:该流与互动式设备(如终端),那么它就是行缓冲的
缓冲区的刷新
刷新一个被缓冲的流是指,将缓冲区中堆积的字符发送给文件。有许多情形下,刷新操作将自动进行:
- 准备进行输出,并且缓冲区已满
- 流关闭时
- 程序调用
exit
时 - 行缓冲策略下,遇到
\n
时 - Whenever an input operation on any stream actually reads data from its file.
如果你想主动刷新,那么请调用fflush(FILE *stream)
注意:fflush
通常用于刷新输出流,对于输入流的操作请详阅 manual page
缓冲策略的控制
在你打开一个流之后,并且在对它操作之前,你可以通过setbuf
系列函数显式指定他的缓冲策略,以及控制其缓冲区大小
Macro: int _IOFBF
用于说明流为全缓冲
Macro: int _IOLBF
用于说明流为行缓冲
Macro: int _IONBF
无缓冲
Macro: int BUFSIZ
用于描述缓冲区大小,这个值被保证至少为
256
。这个宏是基于不同操作系统而设定的,所以最好使用它来作为setvbuf
的函数实际上,你还可以通过
fstat
来获取文件的st_blksize
,即块大小,使用它来作为流的缓冲大小
改变流的缓冲策略
1 | int setvbuf(FILE *restrict stream, char *restrict buf, int mode, size_t size); |
缓冲策略的检测
Function: int __flbf (FILE *stream)
如果流为行缓冲,则返回一个非 0 值,否则返回 0
Function: size_t __fbufsize (FILE *stream)
返回指定流的缓冲区大小
Function: size_t __fpending (FILE *stream)
返回指定流的缓冲区中内容大小,以字节为单位。此函数不应用于目的为读的流。
glibc
似乎只提供了行缓冲的检测方法
为什么我写这个
你可能会说,我擦,就这点事你把全村人叫过来?
其实我是遇到了另一个问题,看代码,这个 空文件 logtest.log
会被写入什么内容?
1 |
|
如果你以为是hello world\n
,那你就错了
答案是hello world\nhello world\n
,如果你不知道为什么,请看下方解析:
fork
会复制(准确来说是 copy-on-write)父进程的内存空间,其中包括了 输出缓冲区- 新打开的流应该都是全缓冲的,然而有一个例外:该流与互动式设备(如终端),那么它就是行缓冲的
- 程序调用
exit
时,会自动刷新缓冲区,将内容全部推到流中
你觉得这个字符串是在父进程写入,并且子进程什么都没干,是因为默认了fp
是行缓冲
的,但其实它不是
真实的情况是:
- 父进程将
hello world\n
写入了缓冲区,因为fp
是全缓冲的,所以字符串并没有立刻输出到文件 fork
之后,子进程没事情可以干,于是隐式调用了exit
,将复制来的缓冲区内容推到了fp- 父进程等待子进程结束,随后显式调用
exit
,将缓冲区中的内容推到fp
所以文件里有两次输出
其实所谓
exit
的隐式调用,是因为main
本身是这样被执行的
- _start
- __libc_start_main
- main
main 的执行方式可以简化为
1 exit(main(argc, argv));所以无论是
- 在
main
中显式调用exit
- 使用
return
(包括编译器自动加上的情况)- 返回值为
void
(最后会返回一个随机数字,不太清楚原理)进程都会调用
exit
收尾,缓冲区一定会被刷新一个很不错的参考: