理解fflush()的必要性及其相关问题

18

下面是使用 fflush() 的示例代码:

#include <string.h>
#include <stdio.h>
#include <conio.h>
#include <io.h>

void flush(FILE *stream);

int main(void)
{
   FILE *stream;
   char msg[] = "This is a test";

   /* create a file */
   stream = fopen("DUMMY.FIL", "w");

   /* write some data to the file */
   fwrite(msg, strlen(msg), 1, stream);

   clrscr();
   printf("Press any key to flush DUMMY.FIL:");
   getch();

   /* flush the data to DUMMY.FIL without closing it */
   flush(stream);

   printf("\nFile was flushed, Press any key to quit:");
   getch();
   return 0;
}

void flush(FILE *stream)
{
     int duphandle;

     /* flush the stream's internal buffer */
     fflush(stream);

     /* make a duplicate file handle */
     duphandle = dup(fileno(stream));

     /* close the duplicate handle to flush the DOS buffer */
     close(duphandle);
}

我知道的关于fflush()的一切就只有它是一个库函数用来刷新输出缓冲区。我想知道使用fflush()的基本目的在哪里,我可以在哪里使用它。最主要的是,我想知道使用fflush()会有什么问题。


1
当您使用printf()而没有任何换行符时,它可能不会立即打印。如果您知道您的程序可能随时崩溃,您可以使用fflush()(或换行符...)。我认为这不是常见用法。 - Elazar
我能想到的唯一问题是,过于频繁地执行此操作会影响性能。此外,您应该知道关闭文件或程序终止意味着自动刷新。一个运行时间很短的程序很少需要显式调用 fflush() - thejh
stdio 的整个目的是为 read()write() 系统调用提供应用程序端的缓冲。任何时候使用缓冲都需要刷新操作。唯一可识别的问题是在需要时忘记使用它,或者可能过度使用它,例如在循环内部而不是在循环结束时使用。 - user207421
3个回答

58

很难确定“使用fflush过度可能存在的问题”。根据您的目标和方法,各种事情都可能成为或变成问题。更好的方法可能是看一下fflush的意图。

首先要考虑的是fflush仅在输出流上定义。输出流将“要写入文件的东西”收集到一个较大的缓冲区中,然后将该缓冲区写入文件。这种收集和稍后写入的点是为了提高速度/效率,有两种方式:

  • 在现代操作系统上,跨越用户/内核保护边界会受到一些惩罚(系统必须更改CPU中的某些保护信息等)。如果您进行大量的OS级写入调用,则对于每个调用都要付出代价。如果您将大约8192个单独的写入收集到一个大缓冲区中,然后进行一次调用,则可以消除大部分开销。
  • 在许多现代操作系统上,每个OS写入调用都会尝试以某种方式优化文件性能,例如,通过发现您已将短文件扩展为更长的文件,并且最好将磁盘块从磁盘上的A点移动到B点,以便长数据可以连续地适合。 (在较旧的操作系统上,这是一个单独的“碎片整理”步骤,您可能需要手动运行它。您可以将其视为现代操作系统执行动态、即时的碎片整理。)如果您编写了500字节,然后又写入了200字节,然后是700字节等等,它会做很多这样的工作;但是,如果您进行一次大的调用,例如8192字节,操作系统可以一次分配一个大块,并将所有内容放在那里,而无需重新碎片整理。
因此,为您提供C库及其stdio流实现的人会在您的操作系统上执行适当的操作以找到“相对最佳”的块大小,并将所有输出收集到该大小的块中。 (数字4096、8192、16384和65536通常是不错的选择,但实际上取决于操作系统,有时还取决于底层文件系统。请注意,“更大”并不总是意味着“更好”:一次以4 GB的块来流式传输数据可能比以64 Kbytes的块更慢)。
但这会带来一个问题。假设您要写入一个文件,例如具有日期和时间戳和消息的日志文件,并且您的代码稍后将继续向该文件写入,但现在,它想暂停一段时间并让日志分析器读取当前日志文件的内容。一种选择是使用fclose关闭日志文件,然后使用fopen再次打开它以便稍后添加更多数据。但更有效的方法是将任何待处理的日志消息推送到底层的OS文件中,但保持文件打开。这就是fflush所做的。
缓冲还会带来另一个问题。假设您的代码存在某些错误,并且它有时会崩溃,但您不确定它是否即将崩溃。并且假设您已经编写了某些非常重要的内容,这些内容必须传输到底层文件系统中。在调用可能会崩溃的代码之前,您可以调用fflush将数据推送到操作系统中。(有时这对于调试很有帮助。)假设您在一个类Unix系统上,拥有一个fork系统调用。此调用会复制整个用户空间(克隆原始进程)。stdio缓冲区在用户空间中,因此克隆进程具有相同的缓冲但尚未写入的数据,这是在fork调用时原始进程所具有的。在这里,解决问题的一种方法是在进行fork之前使用fflush推出缓冲数据。如果在fork之前完成所有输出,则没有任何数据需要复制;新克隆的进程将不会尝试写入已缓冲的数据,因为它已不存在。

添加越多的fflush,就越容易破坏收集大块数据的原始想法。也就是说,你正在做一个取舍:大块更有效,但会导致其他问题,所以你做出了决策:“在此处减少效率,以解决比纯效率更重要的问题”。你调用fflush。
有时问题只是“调试软件”。在这种情况下,您可以使用诸如setbuf和setvbuf之类的函数来更改stdio流的缓冲行为。这比频繁调用fflush更方便(需要较少甚至不需要代码更改-您可以使用标志来控制设置缓冲调用),因此这可能被认为是“使用(或过度使用)fflush的问题”。

2

嗯,@torek的回答几乎完美,但有一点不太准确。

首先要考虑的是fflush仅在输出流中定义。

根据man fflush,fflush也可用于输入流:

对于输出流,fflush()通过流的底层写函数强制写入给定输出或更新流的所有用户空间缓冲数据。 对于输入流,fflush()会丢弃从底层文件获取但尚未被应用程序使用的任何缓冲数据。 流的打开状态不受影响。因此,在输入时,fflush只是将其丢弃。

这里有一个演示来说明它:

#include<stdio.h>

#define MAXLINE 1024

int main(void) {
  char buf[MAXLINE];

  printf("prompt: ");
  while (fgets(buf, MAXLINE, stdin) != NULL)
    fflush(stdin);
    if (fputs(buf, stdout) == EOF)
      printf("output err");

  exit(0);
}

7
在同一个手册页的后面,它说:“标准没有为输入流指定行为。大多数其他实现的行为与Linux相同。” - Alok--
1
我还要注意一点,Linux扩展是一个很好的能力,但在BSD中,我们将其放入非标准的fpurge函数中,该函数在输入和输出流上都有定义。 - torek
C语言规范中有这样一段话:"如果流指向一个输出流或者最近的操作不是输入的更新流,fflush函数会将该流中未写的数据传递给主机环境以便写入文件;否则,其行为是未定义的。" 调用fflush(input_stream)将产生未定义的行为。 man手册只适用于一部分编译器,而非C语言整体。 - chux - Reinstate Monica
如果打印失败,则if (fputs(buf, stdout) == EOF) printf("output err");很有问题,随后对同一流进行的打印是可疑的。将打印输出到stderr可以更好地被注意到。 - chux - Reinstate Monica

0

fflush() 清空与流相关的缓冲区。例如,如果您在非常短的时间内(毫秒级别)让用户输入一些数据并将一些内容写入文件,则写入和读取缓冲区可能会保留一些“剩余物”。然后调用 fflush() 来清空所有缓冲区,并强制标准输出确保下一个输入是用户按下的。

参考:http://www.cplusplus.com/reference/cstdio/fflush/


在使用它时可能会出现什么问题? - karan
fflush()是标准的C函数,在Linux上得到支持。 - P.P
3
这个答案完全错误,并暗示 fflush 可以用于输入。事实并非如此。 - R.. GitHub STOP HELPING ICE
它并不是建议使用Oo,而是说它会清空与流相关的缓冲区,以确保您的下一个输入正确。 - Chaos

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接