1. 什么是缓冲区
在计算机系统中,缓冲区(Buffer) 是一种临时存储数据的区域,主要用于协调不同速度或不同时序的组件之间的数据传输,以提高效率并减少资源冲突。它是系统设计中的重要概念,尤其在I/O操作、网络通信、硬件交互等领域广泛应用。
引入缓冲区机制有以下几方面的意义:
提高数据传输效率
减少数据传输次数:缓冲区可以暂存数据,当数据量达到一定程度时再进行一次性传输,从而减少了频繁的数据传输次数,提高了传输效率。
平衡数据传输速度差异:在不同设备或模块之间进行数据传输时,它们的数据传输速度可能不同。缓冲区可以作为一个中间存储区域,用来平衡这种速度差异,避免数据丢失或传输错误。
增强系统稳定性和可靠性
防止数据丢失:在数据传输过程中,如果接收方不能及时处理数据,缓冲区可以暂时存储这些数据,直到接收方有能力处理为止,从而防止数据丢失。
提供错误处理机制:缓冲区可以在数据传输过程中对数据进行校验和错误检测,当发现错误时,可以采取相应的措施进行纠正或重传,从而提高数据传输的可靠性。
优化系统资源利用
提高CPU利用率:通过使用缓冲区,CPU可以在数据传输的同时进行其他操作,而不必等待数据传输完成,从而提高了CPU的利用率。
合理利用内存资源:缓冲区可以根据实际需要动态分配内存,避免了因数据传输而频繁申请和释放内存,提高了内存的利用率。
支持异步操作
实现异步数据传输:缓冲区可以支持异步数据传输,即发送方可以在不等待接收方响应的情况下继续执行其他操作,从而提高了系统的并发处理能力。
提高系统响应速度:在异步操作中,缓冲区可以暂存请求或响应数据,使得系统能够更快地响应用户的操作,提高了系统的响应速度。
举例来说,菜鸟驿站实际上就是快递系统地一个缓冲区。
快递员要负责配送许多用户的快递,但是等待用户一个一个地来取无疑浪费了快递员的时间,降低了其工作效率。而菜鸟驿站的设立使得快递员直接将数据交付到站点即可离开配送下一个快递。
买家有多件快递,每一个快递被送到之后都需要其下楼来与快递员交互。而菜鸟驿站使得买家可以在所有的快递全部到达之后再一次性取走,减少了跑路的次数,节省了时间。
菜鸟驿站在入库快递时,相当于是对包裹又进行了一次清点。
同时,如果在快递到达之后买家有事无法在短时间内取得,则快递可以被寄放在菜鸟驿站,避免丢失。
2. 缓冲类型
什么情况下会刷新缓冲区,进行系统调用操作呢?
标准I/O提供了3种类型的缓冲区:
全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/O系统调用操作。对于磁盘文件的操作通常使用全缓冲的方式访问。
行缓冲区:在行缓冲情况下,当在输入和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。当所操作的流涉及一个终端时(例如标准输入和标准输出),使用行缓冲方式。因为标准I/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行I/O系统调用操作,默认行缓冲区的大小为1024。
无缓冲区:无缓冲区是指标准I/O库不对字符进行缓存,直接调用系统调用。标准错误流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。
除了上述列举的默认刷新方式,下列特殊情况也会引发缓冲区的刷新:
缓冲区满时。
执行flush语句时。
程序退出时。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main() {close(1);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0) {perror("open");return 0;} printf("hello world: %d\n", fd);close(fd);return 0;
}
根据我们之前学的知识," hello world:1\n " 应该被写入文件 log.txt 中。然而实际上却并没有:
这是因为:(1) 我们将1号描述符重定向到磁盘文件后,缓冲区的刷新方式成为了全缓冲;(2) 被写入文件的数据会经过两个缓冲区,语言层缓冲区和Linux内核缓冲区;(3) 数据一旦到达Linux内核缓冲区就一定会被正确写入文件;(4) printf被调用后," hello world:1\n " 首先以全缓冲方式被存放在语言层的缓冲区,由于数据较少缓冲区未刷新;(5) 当程序退出时,语言层的缓冲区会尝试刷新,但此时 log.txt 已将被关闭了,所以 " hello world:1\n " 未被写入。
遇到这种情况,我们可以在文件关闭之前使用 fflush 函数来强制刷新缓冲区:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main() {close(1);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0) {perror("open");return 0;} printf("hello world: %d\n", fd);fflush(stdout);close(fd);return 0;
}
3. 语言缓冲区
C语言提供的读写函数,实际上是对系统调用 read 和 write 进行了封装,并加上了一层额外的语言层的缓冲区。read 和 write 本身是没有缓冲区的,在进行数据读出和写入时,二者直接与内核缓冲区进行交互。
我们可以来验证这一点:
#include <stdio.h>
#include <string.h>int main()
{const char* msg0 = "hello printf\n";const char* msg1 = "hello fwrite\n";const char* msg2 = "hello write\n";printf("%s", msg0);fwrite(msg1, strlen(msg0), 1, stdout);write(1, msg2, strlen(msg2));fork();return 0;
}
运行结果:
hello printf
hello fwrite
hello write
但如果在运行时进行重定向的话(./test > log.txt):
hello write
hello printf
hello fwrite
hello printf
hello fwrite
进行重定向之后,刷新方式变为全刷新。write 函数直接将数据写入内核缓冲区中,所以不受影响;但是 printf 和 fwrite 写入的数据会先被存放在语言层缓冲区中,等到进程退出时再写入。
在 fork 函数执行之后,语言层缓冲区的数据被复制给了子进程,所以在进程退出时,父子进程先后将自身的缓冲区刷新,导致 printf 和 fwrite 写入的数据被写了两次。
语言级缓冲区实际上存在于 FILE 结构体中,与 printf 封装 write 类似,FILE结构体 封装了 struct file 结构体,并用一个字段来作为缓冲区。