了解 C 中的缓冲
Understanding Buffering in C
我真的很难理解缓冲的深度,尤其是在 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
更新:
我忘了问我在一些网站上读到人们如何指代例如字符串文字作为缓冲区,甚至数组作为缓冲区;这是正确的还是我遗漏了什么?
也请说明这一点。
第一个问题有点太宽泛了。很多情况下都会用到缓冲,包括实际使用前的消息存储、DMA 使用、加速使用等。简而言之,整个缓冲的事情可以概括为"save my data, let me continue execution while you do something with the data"。
有时您可能会在将缓冲区传递给函数后对其进行修改,有时则不会。有时缓冲区是硬件,有时是软件。有时它们位于 RAM 中,有时位于其他内存类型中。
所以,请问更具体的问题。作为开始,使用维基百科,它几乎总是有帮助的:wiki
至于代码示例,我没有发现任何提及 getchar
时刷新所有输出缓冲区的信息。文件的缓冲区通常在三种情况下被刷新:
fflush()
或同等学历
- 文件已关闭
- 缓冲区溢出。
由于这两种情况都不适合您,因此不会刷新文件(请注意,此列表中的应用程序终止是 而非 )。
单词缓冲区在计算机科学中用于许多不同的事物。从更一般的意义上讲,它是在数据被处理或复制到最终目的地(或其他缓冲区)之前临时存储数据的任何一块内存。
正如您在问题中暗示的那样,缓冲区有多种类型,但作为一个广泛的分组:
硬件缓冲区:这些是数据在移动到硬件设备之前存储的缓冲区。或者在从 HW 设备接收数据时存储数据的缓冲区,直到它被应用程序处理为止。这是必需的,因为 I/O 操作通常具有内存和时序要求,而这些由缓冲区来满足。想想 read/write 直接到内存的 DMA 设备,如果内存设置不正确,系统可能会崩溃。或必须具有亚微秒精度的声音设备,否则将无法正常工作。
缓存缓冲区:这些缓冲区是在从 file/device 写入 into/read 之前对数据进行分组的缓冲区,因此通常会提高性能。
Helper buffers: 你将数据移动into/from这样的缓冲区,因为它对你的算法来说更容易。
案例 #2 是您的 FILE*
示例。想象一下,对 write 系统调用(Win32 中的 WriteFile()
)的调用仅花费 1ms 的调用时间加上每个字节的 1us(请耐心等待,实际情况更复杂世界)。然后,如果你这样做:
FILE *f = fopen("file.txt", "w");
for (int i=0; i < 1000000; ++i)
fputc('x', f);
fclose(f);
如果没有缓冲,此代码将花费 1000000 * (1ms + 1us)
,即大约 1000 秒。然而,对于 10000 字节的缓冲区,将只有 100 个系统调用,每个 10000 字节。那将是 100 * (1ms + 10000us)
。仅需 0.1 秒!
另请注意,OS 将进行自己的缓冲,以便使用最有效的大小将数据写入实际设备。那将同时是一个硬件和缓存缓冲区!
关于您的刷新问题,文件通常在关闭或手动刷新时刷新。某些文件,例如 stdout
是行刷新的,也就是说,只要写入 '\n'
,它们就会刷新。 stdin/stdout
也很特殊:当您从 stdin
读取时,stdout
会被刷新。其他文件保持不变,只有 stdout
。如果您正在编写交互式程序,那将很方便。
我的案例 #3 是当你这样做时的例子:
FILE *f = open("x.txt", "r");
char buffer[1000];
fgets(buffer, sizeof(buffer), f);
int n;
sscanf(buffer, "%d", &n);
您使用缓冲区保存文件中的一行,然后解析该行中的数据。是的,你可以直接调用 fscanf()
,但在其他 API 中可能没有等效的功能,而且你可以通过这种方式获得更多控制权:你可以分析 if 行的类型,跳过注释,计算行数...
或者假设您一次接收一个字节,例如从键盘接收一个字节。您只需在缓冲区中累积字符并在按下 Enter 键时解析该行。这就是大多数交互式控制台程序所做的。
名词"buffer"实际上指的是一种用法,而不是一个不同的东西。任何存储块都可以用作缓冲区。在这种一般意义上,该术语有意与各种 I/O 函数结合使用,尽管 C I/O 流函数的文档倾向于避免这种情况。以POSIX read()
函数为例,然而:“read() attempts to read up to count bytes从文件描述符 fd 进入从 buf 开始的缓冲区”。 "buffer" 在这种情况下仅表示将记录读取的字节的内存块;它通常实现为 char[]
或动态分配的块。
一个缓冲区尤其与 I/O 结合使用,因为某些设备(尤其是硬盘)以中型到大型块的形式最有效地读取,而程序通常希望以较小的形式使用该数据件。 I/O 的一些其他形式,例如网络 I/O,可能固有地以块的形式出现,因此您必须记录每个完整的块(在缓冲区中),否则会丢失您没有立即准备好的部分消耗。类似的考虑也适用于输出。
至于您的测试程序的行为,您希望演示的 "rule" 特定于控制台 I/O,但只有一个涉及的流连接到控制台。
缓冲区是内存 (RAM) 内的一个简单的小区域,该区域负责在发送到您的程序之前存储信息,只要我从键盘输入字符,这些字符就会存储在缓冲区内一旦我按下 Enter 键,这些字符就会从缓冲区传输到您的程序中,因此在缓冲区的帮助下,所有这些字符都可以立即用于您的程序(防止滞后和缓慢)并将它们发送到输出显示屏
我真的很难理解缓冲的深度,尤其是在 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更新:
我忘了问我在一些网站上读到人们如何指代例如字符串文字作为缓冲区,甚至数组作为缓冲区;这是正确的还是我遗漏了什么? 也请说明这一点。
第一个问题有点太宽泛了。很多情况下都会用到缓冲,包括实际使用前的消息存储、DMA 使用、加速使用等。简而言之,整个缓冲的事情可以概括为"save my data, let me continue execution while you do something with the data"。
有时您可能会在将缓冲区传递给函数后对其进行修改,有时则不会。有时缓冲区是硬件,有时是软件。有时它们位于 RAM 中,有时位于其他内存类型中。
所以,请问更具体的问题。作为开始,使用维基百科,它几乎总是有帮助的:wiki
至于代码示例,我没有发现任何提及 getchar
时刷新所有输出缓冲区的信息。文件的缓冲区通常在三种情况下被刷新:
fflush()
或同等学历- 文件已关闭
- 缓冲区溢出。
由于这两种情况都不适合您,因此不会刷新文件(请注意,此列表中的应用程序终止是 而非 )。
单词缓冲区在计算机科学中用于许多不同的事物。从更一般的意义上讲,它是在数据被处理或复制到最终目的地(或其他缓冲区)之前临时存储数据的任何一块内存。
正如您在问题中暗示的那样,缓冲区有多种类型,但作为一个广泛的分组:
硬件缓冲区:这些是数据在移动到硬件设备之前存储的缓冲区。或者在从 HW 设备接收数据时存储数据的缓冲区,直到它被应用程序处理为止。这是必需的,因为 I/O 操作通常具有内存和时序要求,而这些由缓冲区来满足。想想 read/write 直接到内存的 DMA 设备,如果内存设置不正确,系统可能会崩溃。或必须具有亚微秒精度的声音设备,否则将无法正常工作。
缓存缓冲区:这些缓冲区是在从 file/device 写入 into/read 之前对数据进行分组的缓冲区,因此通常会提高性能。
Helper buffers: 你将数据移动into/from这样的缓冲区,因为它对你的算法来说更容易。
案例 #2 是您的 FILE*
示例。想象一下,对 write 系统调用(Win32 中的 WriteFile()
)的调用仅花费 1ms 的调用时间加上每个字节的 1us(请耐心等待,实际情况更复杂世界)。然后,如果你这样做:
FILE *f = fopen("file.txt", "w");
for (int i=0; i < 1000000; ++i)
fputc('x', f);
fclose(f);
如果没有缓冲,此代码将花费 1000000 * (1ms + 1us)
,即大约 1000 秒。然而,对于 10000 字节的缓冲区,将只有 100 个系统调用,每个 10000 字节。那将是 100 * (1ms + 10000us)
。仅需 0.1 秒!
另请注意,OS 将进行自己的缓冲,以便使用最有效的大小将数据写入实际设备。那将同时是一个硬件和缓存缓冲区!
关于您的刷新问题,文件通常在关闭或手动刷新时刷新。某些文件,例如 stdout
是行刷新的,也就是说,只要写入 '\n'
,它们就会刷新。 stdin/stdout
也很特殊:当您从 stdin
读取时,stdout
会被刷新。其他文件保持不变,只有 stdout
。如果您正在编写交互式程序,那将很方便。
我的案例 #3 是当你这样做时的例子:
FILE *f = open("x.txt", "r");
char buffer[1000];
fgets(buffer, sizeof(buffer), f);
int n;
sscanf(buffer, "%d", &n);
您使用缓冲区保存文件中的一行,然后解析该行中的数据。是的,你可以直接调用 fscanf()
,但在其他 API 中可能没有等效的功能,而且你可以通过这种方式获得更多控制权:你可以分析 if 行的类型,跳过注释,计算行数...
或者假设您一次接收一个字节,例如从键盘接收一个字节。您只需在缓冲区中累积字符并在按下 Enter 键时解析该行。这就是大多数交互式控制台程序所做的。
名词"buffer"实际上指的是一种用法,而不是一个不同的东西。任何存储块都可以用作缓冲区。在这种一般意义上,该术语有意与各种 I/O 函数结合使用,尽管 C I/O 流函数的文档倾向于避免这种情况。以POSIX read()
函数为例,然而:“read() attempts to read up to count bytes从文件描述符 fd 进入从 buf 开始的缓冲区”。 "buffer" 在这种情况下仅表示将记录读取的字节的内存块;它通常实现为 char[]
或动态分配的块。
一个缓冲区尤其与 I/O 结合使用,因为某些设备(尤其是硬盘)以中型到大型块的形式最有效地读取,而程序通常希望以较小的形式使用该数据件。 I/O 的一些其他形式,例如网络 I/O,可能固有地以块的形式出现,因此您必须记录每个完整的块(在缓冲区中),否则会丢失您没有立即准备好的部分消耗。类似的考虑也适用于输出。
至于您的测试程序的行为,您希望演示的 "rule" 特定于控制台 I/O,但只有一个涉及的流连接到控制台。
缓冲区是内存 (RAM) 内的一个简单的小区域,该区域负责在发送到您的程序之前存储信息,只要我从键盘输入字符,这些字符就会存储在缓冲区内一旦我按下 Enter 键,这些字符就会从缓冲区传输到您的程序中,因此在缓冲区的帮助下,所有这些字符都可以立即用于您的程序(防止滞后和缓慢)并将它们发送到输出显示屏