理解C语言中的缓冲机制

7

我很难理解缓冲区的深度,特别是在C编程中。我已经对这个主题进行了长时间的搜索,但直到现在还没有找到令人满意的结果。

我会更具体地说明一下:我确实理解它背后的概念(即不同硬件设备之间协调操作并最小化这些设备速度差异),但我希望能够更全面地解释这些和其他可能的缓冲区原因(而且我所说的“全面”是指越长越深入越好)。同时,给出一些关于如何在I/O流中实现缓冲的具体示例也将非常有帮助。

另一个问题是,我注意到我的程序没有遵循某些缓冲区刷新规则,就像以下简单片段一样,听起来很奇怪:

#include <stdio.h>

int main(void)
{
    FILE * fp = fopen("hallo.txt", "w");

    fputc('A', fp);
    getchar();
    fputc('A', fp);
    getchar();

    return 0;
}

该程序的目的是演示当第一个getchar()被调用时,即将到来的输入会立即刷新任意流,但事实上我尝试了很多次并进行了许多修改,这种情况并不经常发生,对于stdout(例如使用printf()),流会在没有请求输入的情况下自动刷新,从而否定了这个规则。因此,我是否理解错误了这个规则或者还有其他要考虑的? 我正在Windows 8.1上使用Gnu GCC。更新:我忘记问了,我在一些网站上看到人们将字符串字面值称为缓冲区,甚至将数组称为缓冲区; 这正确吗,还是我错过了什么?请解释这一点。

2
就示例而言,您并不是写入“stdout”,而是写入任意文件。 - Aneri
抱歉,我的错,我改了它。 - Lockon2000
针对您最后一个问题(来自更新); 是的,数组(特别是字符数组)被称为缓冲区,但这与I/O缓冲不同,尽管I/O缓冲通常使用缓冲区来保存数据,直到写入或读取。 - Jonathan Leffler
我应该补充一点,C标准并没有关于标准输入和标准输出之间任何同步的规定。这意味着我提到的任何刷新行为在C中都没有标准化。我在POSIX中也没有找到任何提及它的内容。 - Jonathan Leffler
@jonathanleffler 我可以理解为什么数组,特别是字符数组有时被称为缓冲区,但为什么像字符串字面值这样的东西也被称为缓冲区呢?因为它们不是设计成临时存储数据的内存,只能短时间保存数据,因为它们属于静态存储类类型。 - Lockon2000
显示剩余4条评论
4个回答

18
计算机科学中,“缓冲区”这个词被用于许多不同的事物。更普遍的意义上,它是指任何存储数据的临时内存,直到它被处理或复制到最终目的地(或其他缓冲区)。
正如您在问题中暗示的那样,有许多类型的缓冲区,但可以广泛分为以下几类:
1. 硬件缓冲区:存储数据以便移动到硬件设备之前的缓冲区。或者是存储接收自硬件设备的数据,直到应用程序对其进行处理的缓冲区。这是必要的,因为I/O操作通常具有内存和时间要求,而这些要求由缓冲区满足。想象一下DMA设备直接读写内存,如果内存没有正确设置,则系统可能会崩溃。或者声音设备必须具有亚微秒的精度,否则它将工作不良。
2. 缓存缓冲区:这些缓冲区是将数据分组写入/从文件/设备中,以便性能得到改善的缓冲区。
3. 助手缓冲区:您可以将数据移入/从此类缓冲区,因为对于您的算法来说,这样做更容易。
第2种情况是您的FILE*示例。想象一下,对“写入”系统调用(Win32中的WriteFile())的调用需要1毫秒仅用于调用加上1微秒每字节(请忍耐,实际情况更为复杂)。那么,如果您执行以下操作:
FILE *f = fopen("file.txt", "w");
for (int i=0; i < 1000000; ++i)
    fputc('x', f);
fclose(f);

如果没有缓冲,这段代码将需要 1000000 * (1毫秒 + 1微秒),约为1000秒。但是,使用10000字节的缓冲区,只会有100个系统调用,每个系统调用写入10000字节。那只需要 100 * (1毫秒 + 10000微秒),仅需0.1秒!

请注意,操作系统将进行自己的缓冲,以便使用最有效的大小将数据写入实际设备。这将同时是硬件和缓存缓冲区!

关于清空缓冲区问题,文件通常在关闭或手动清空时才被清空。一些文件,如stdout是行刷新,也就是说,每当写入'\n'时,它们都会被刷新。此外,stdin/stdout是特殊的:当您从stdin读取时,stdout会被刷新。其他文件不会受到影响,只有stdout。如果您正在编写交互式程序,这很方便。

我的第三种情况是当您执行以下操作时:

FILE *f = open("x.txt", "r");
char buffer[1000];
fgets(buffer, sizeof(buffer), f);
int n;
sscanf(buffer, "%d", &n);

您可以使用缓冲区来保存从文件中读取的一行数据,并从该行数据中解析数据。是的,您可以直接调用fscanf()函数,但在其他API中可能没有等效的函数,此外,这种方式能够更好地控制:您可以分析行的类型、跳过注释、统计行数……

或者想象一下,您每次只接收一个字节,比如从键盘输入。您将只需在缓冲区中累积字符,并在按下Enter键时解析该行。这就是大多数交互式控制台程序所做的。


我想先确认一些问题,慢速设备是否总是目标设备,例如,在向文件写入时,显然硬盘比处理器和内存慢,但在从硬盘接收输入时,哪个更慢?是难以写入缓冲区还是处理器处理接收到的数据更慢?更明确地说,数据是累积得非常快,然后才以这样的速度进行处理吗?还是相反?希望我的问题足够清晰。 - Lockon2000
@Lockon2000:这取决于您对数据的处理方式。如果您想将新挖掘的比特币写入文件,那么CPU的速度会比硬盘慢。在这种情况下,您不会在写入之前缓冲多个BT,而是立即写入和刷新。当然,在保存之前,仍有缓冲区计算BT。读者也是如此:如果您的CPU丢弃数据,则硬盘速度较慢,但如果您进行了大量计算,则CPU速度较慢。它可以工作,因为任何明智的实现都会限制挂起缓冲区的最大大小并阻止最快的进程。 - rodrigo
从您的最后一次编辑开始,首先:如果只有在遇到换行符或从stdin输入时才刷新stdout(请参见我稍下方的评论),那么为什么在我的系统上,即使没有请求任何输入或显示任何换行符,stdout也会被刷新,就像我之前提到的那样。其次:从您最后几行中,我似乎对某些事情有所误解。如果键盘输入是逐个累积并逐个发送给处理器,因为程序无法处理块,只能处理单个字节,那么这些块在哪里?我的意思是缓冲背后的原因是什么? - Lockon2000
@Lockon2000:关于第一点:很抱歉,但我不明白你在问什么(没有冒犯的意思,但我发现你缺乏标点符号,很难阅读)。第二点:在键盘案例中,需要缓冲区,因为sccanf()需要一个缓冲区。没有函数可以逐字节解析复杂语法。好吧,你可以编写一个,只需在内部进行缓冲即可! - rodrigo
是的,你说得对。我直到现在才注意到这一点,并且以后不会再忽略它了。针对问题,让我们尝试换个方式来表达;如果只有等待来自标准输入的输入(特别是),或遇到换行符时才刷新stdout,那么为什么我的系统会在没有换行符或请求stdout输入的情况下刷新stdout(例如,在printf(“hay”)之后)?请参阅此网站中提到的第一个规则:http://www.gnu.org/software/libc/manual/html_node/Flushing-Buffers.html#Flushing-Buffers - Lockon2000
@Lockon2000:第一个GNU规则只是说缓冲区大小有限,当它满了时,内容会被刷新到目标位置。我认为这很明显,所以没有提到它(我的错,已添加到答案中)。无论如何,缓冲的细节取决于实现,因此您的系统可能会做一些有趣的事情。如果不了解更多细节,就无法确定。 - rodrigo

3
名词“缓冲区”实际上是指一种用法,而不是一个独立的东西。任何一块存储空间都可以作为缓冲区。该术语故意在各种I/O函数中以这种广义意义使用,尽管C I/O流函数的文档往往避免使用它。以POSIX的read()函数为例:“read()尝试从文件描述符fd中读取最多count字节到从buf开始的缓冲区中”。在这种情况下,“缓冲区”仅表示将记录所读取的字节的内存块;通常它被实现为一个char[]或一个动态分配的块。
人们特别在I/O中使用缓冲区,因为某些设备(特别是硬盘)最高效地读取中等到大型块,而程序通常希望以较小的块消耗这些数据。一些其他形式的I/O,如网络I/O,可能天生就会以块的形式出现,因此您必须记录每个完整的块(在缓冲区中),否则将失去您尚未准备好消耗的部分。类似的考虑也适用于输出。
至于您的测试程序的行为,“规则”您希望证明的是特定于控制台I/O,但只有其中一个流连接到控制台。

1
第一个问题有点太宽泛了。缓冲在许多情况下都被使用,包括在实际使用之前的消息存储、DMA使用、加速使用等等。简而言之,整个缓冲事情可以总结为“保存我的数据,让我在你处理数据时继续执行”。
有时您可能会在将缓冲区传递给函数后修改它们,有时不会。有时缓冲区是硬件,有时是软件。有时它们驻留在RAM中,有时在其他内存类型中。
因此,请提出更具体的问题。作为一个起点,使用维基百科,它几乎总是有帮助的: wiki 至于代码示例,我没有找到任何关于在getchar后刷新所有输出缓冲区的提及。文件的缓冲区通常在三种情况下被刷新:
1. fflush()或等效函数 2. 文件被关闭 3. 缓冲区溢出。
由于这些情况都不成立,所以文件不会被刷新(请注意,应用程序终止不在此列表中)。

最后一条规则非常明确地指出,如果我理解正确的话,任何流上的任何输入都应该至少刷新流。 - Lockon2000

0

缓冲区是内存(RAM)中的一个简单小区域,该区域负责在发送到程序之前存储信息。只要我从键盘输入字符,这些字符就会存储在缓冲区中。一旦我按下回车键,这些字符将从缓冲区传输到您的程序中,因此通过缓冲区的帮助,所有这些字符都可以即时提供给您的程序(避免延迟和慢速),并将它们发送到输出显示屏幕。


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