glibc的三种缓冲策略

很多朋友可能在初次学习 C 语言时,如果是使用 Unix 环境,可能遇到过如下的问题

1
2
3
4
5
6
#include <stdio.h>
int main()
{
printf("Hey");
return 0;
}

欸我运行完了怎么没输出呢?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
2
3
4
5
6
7
8
9
10
11
12
13
14
int setvbuf(FILE *restrict stream, char *restrict buf, int mode, size_t size);
void setbuf(FILE *restrict stream, char *restrict buf);
void setlinebuf (FILE *stream)
// Open a Stream, fully buffered by default
FILE *fp = fopen("log.log", "w");

// Set it unbuffered
// equivalent to
// setbuf(fp,NULL);
setvbuf(fp, NULL, _IONBF, 0);


// set it line buffered
setlinebuf(fp);

缓冲策略的检测

  • Function: int __flbf (FILE *stream)

    如果流为行缓冲,则返回一个非 0 值,否则返回 0

  • Function: size_t __fbufsize (FILE *stream)

    返回指定流的缓冲区大小

  • Function: size_t __fpending (FILE *stream)

    返回指定流的缓冲区中内容大小,以字节为单位。此函数不应用于目的为读的流。

glibc似乎只提供了行缓冲的检测方法

为什么我写这个

你可能会说,我擦,就这点事你把全村人叫过来?

其实我是遇到了另一个问题,看代码,这个 空文件 logtest.log会被写入什么内容?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{

int pid;

FILE *fp = fopen("logtest.log", "w");
fprintf(fp, "hello world\n");
pid = fork();
if (pid == 0)
{
;
}
else
{
wait(NULL);
exit(1);
}

}

如果你以为是hello world\n,那你就错了

答案是hello world\nhello world\n,如果你不知道为什么,请看下方解析:

  • fork会复制(准确来说是 copy-on-write)父进程的内存空间,其中包括了 输出缓冲区
  • 新打开的流应该都是全缓冲的,然而有一个例外:该流与互动式设备(如终端),那么它就是行缓冲
  • 程序调用exit时,会自动刷新缓冲区,将内容全部推到流中

你觉得这个字符串是在父进程写入,并且子进程什么都没干,是因为默认了fp行缓冲的,但其实它不是

真实的情况是:

  1. 父进程将hello world\n写入了缓冲区,因为fp全缓冲的,所以字符串并没有立刻输出到文件
  2. fork之后,子进程没事情可以干,于是隐式调用了exit,将复制来的缓冲区内容推到了fp
  3. 父进程等待子进程结束,随后显式调用exit,将缓冲区中的内容推到fp

所以文件里有两次输出

其实所谓exit的隐式调用,是因为main本身是这样被执行的

  1. _start
  2. __libc_start_main
  3. main

main 的执行方式可以简化为

1
exit(main(argc, argv));

所以无论是

  • main中显式调用exit
  • 使用return(包括编译器自动加上的情况)
  • 返回值为void(最后会返回一个随机数字,不太清楚原理)

进程都会调用exit收尾,缓冲区一定会被刷新

一个很不错的参考:

链接、装载与库 — 运行库 | Technology Blog (markrepo.github.io)

Author

BakaFT

Posted on

2022-10-01

Updated on

2023-12-28

Licensed under

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×