个人主页:chian-ocean
文章专栏-Linux
前言:
文件缓冲区是操作系统用来提高文件I/O操作效率的内存区域。在处理文件的读写操作时,缓冲区起着重要的作用。缓冲区通常是在内存中分配的一块空间,用于临时存储即将读取或写入的数据
缓冲区的层次
语言级缓冲区(Language-Level Buffer)
- 位置:用户空间,由程序语言的标准库管理。
- 管理:由编程语言的标准库(如C语言的
stdio.h
)提供。 - 作用:程序通过语言级缓冲区来缓存文件I/O操作,避免频繁的系统调用。语言级缓冲区的管理和使用通常对开发者透明,程序员可以通过特定的
API
进行设置(如缓冲区类型、大小等)。 - 示例:
- C语言的
stdio
缓冲区:如fread()
、fwrite()
、printf()
等函数使用缓冲区来提高文件I/O的效率。数据会先存储在缓冲区,直到缓冲区满或手动刷新时,才会写入磁盘。 - 行缓冲(Line Buffered)、**全缓冲(Fully Buffered)和无缓冲(Unbuffered)**是不同的缓冲策略。
- C语言的
- 优点:
- 提高I/O操作的效率。
- 程序员可以控制缓冲区的大小和刷新策略,灵活性较高。
C语言缓冲区
在C语言中,FILE*
是一个指向 FILE
结构体的指针,FILE
结构体在标准库中用于表示一个打开的文件。FILE*
用于在文件操作函数中引用文件对象,它是C语言标准库中的一个重要数据类型,用来管理文件的输入输出。
FILE
的内部实现(简化):
typedef struct {
unsigned char *ptr; // 当前缓冲区位置
int cnt; // 剩余字符数
unsigned char *base; // 缓冲区起始地址
int flag; // 文件状态标志
int fd; // 文件描述符
int charbuf; // 单字符缓冲区
int bufsize; // 缓冲区大小
unsigned char *tmpfname; // 临时文件名
} FILE;
- 可以发现在FILE结构体中存在缓冲区。
- 这是存在于语言级别的缓冲区,然后在于系统或者硬件级交互。
操作系统级缓冲区(System-Level Buffer)
- 位置:操作系统内核中的内存区域。
- 管理:由操作系统内核管理。
- 作用:操作系统使用缓冲区来缓存来自文件系统的数据和元数据(如i-node、块设备缓存、页缓存等)。系统级缓冲区通常用于减少与磁盘的直接交互,优化磁盘I/O操作和文件系统性能。
- 示例:
- 页缓存(Page Cache):操作系统将文件的数据缓存在内存中,后续对该文件的访问会直接从内存缓存中获取,避免频繁访问磁盘。
- 块设备缓存(Block Cache):磁盘数据的块缓存,在块设备(如硬盘)和内存之间进行高效的数据传输。
- 文件缓冲区(Buffer Cache):用于存储文件的元数据,如文件系统的超级块、目录项、i-node等。
- 优点:
- 提高文件系统的性能,减少对磁盘的访问频率。
- 优化I/O操作,提升系统效率。
当然还存在**硬件级缓冲区(Hardware-Level Buffer)**和 应用级缓冲区(Application-Level Buffer) 在这里面只谈上面两个。
缓冲区类型
缓冲区是计算机中用于存储临时数据的内存区域,通常在输入输出(I/O)操作中发挥重要作用。缓冲区的类型可以根据其工作原理和使用场景的不同来分类,常见的缓冲区类型包括全缓冲、行缓冲和无缓冲三种。每种类型的缓冲区在处理数据时的策略和应用场景不同。
全缓冲(Fully Buffered)
- 描述:在全缓冲模式下,数据会先存储到缓冲区中,直到缓冲区满或文件关闭时,才会一次性写入目标设备(如磁盘)。如果读取数据,缓冲区的数据会一次性读取。
- 应用场景:适用于需要高效进行大量数据写入的场景,例如文件的读写操作。使用全缓冲可以减少磁盘I/O的次数,从而提高程序性能。
- 优点:
- 减少了磁盘操作的次数,减少I/O的延迟。
- 缺点:
- 如果程序崩溃,缓冲区中的数据可能未被写入磁盘,导致数据丢失。
示例:
- 文件写操作、网络数据传输。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
// 打开名为 "log.txt" 的文件,模式是写入("w")。如果文件不存在,会创建新文件。
FILE* fp = fopen("log.txt", "w");
if(fp == NULL) // 如果文件打开失败,返回错误并退出程序
{
perror("fopen"); // 打印打开文件失败的错误信息
return -1; // 返回 -1,表示程序错误退出
}
const char* msg = "hello linu\n"; // 定义要写入文件的字符串,其中的换行符在文件中显示为换行
int cnt = 5; // 定义写入的次数(5次)
// 循环 5 次,每次写入 msg 字符串到文件中,并且间隔 2 秒
while(cnt--)
{
// 将 msg 字符串写入文件,每次写入时不会自动换行,换行符是通过 msg 字符串内的 "\n" 实现的
fprintf(fp, "%s", msg);
sleep(2); // 程序暂停 2 秒,模拟延迟
}
fclose(fp); // 关闭文件,保存写入的内容
return 0; // 程序成功结束,返回 0
}
打开文件:
fopen("log.txt", "w")
:以写入模式打开文件log.txt
。如果文件不存在,将会创建新文件。如果打开文件失败(例如没有写入权限),则返回NULL
,并通过perror("fopen")
打印错误信息。
定义字符串:
const char* msg = "hello linu\n";
:定义了一个字符串msg
,它的内容是"hello linu"
和换行符(\n
)。换行符会在文件中起到换行的作用。
写入文件:
- 在
while(cnt--)
循环中,使用fprintf(fp, "%s", msg);
将msg
字符串写入文件log.txt
。由于msg
字符串中包含了换行符,文件中的每次写入都会在hello linu
后换行。
暂停与延迟:
sleep(2)
:在每次写入后,程序暂停 2 秒。这样文件写入的间隔就会是 2 秒。
关闭文件:
fclose(fp);
:在写入完成后关闭文件,确保所有内容被保存。
启用两个终端监控 log.txt
,文件的写入。
while true ;do cat log.txt ;echo "----------------";sleep 1;done
- 这个脚本会每 1 秒读取一次
log.txt
文件的内容,并将内容输出到终端。输出后会显示一行分隔符----------------
,然后暂停 1 秒,再继续下一轮读取和显示。循环将会持续进行,直到用户手动中断。
代码在执行的时候在
log.txt
前面10秒中是不会显示结果的。直至进程结束。会在文件中打印出5层循环的
"hello linux"
。
行缓冲(Line Buffered)
- 描述:在行缓冲模式下,缓冲区的数据会在遇到换行符(
\n
)时刷新,即当输入或输出一个完整行的数据时,缓冲区会立即写入或读取数据。这种模式在需要逐行处理的应用程序中非常有用。 - 应用场景:通常用于交互式程序或需要实时输出的场景。例如,命令行程序、日志记录等。
- 优点:
- 适合实时输出,数据能及时反映到屏幕或文件中。
- 缺点:
- 可能会导致频繁的I/O操作,特别是对于大数据量的处理时,性能较差。
示例:
- 打印输出、命令行交互。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
// 打开名为 "log.txt" 的文件,模式是写入("w")。如果文件不存在,会创建新文件。
FILE* fp = fopen("log.txt", "w");
if(fp == NULL) // 如果文件打开失败,返回错误并退出程序
{
perror("fopen"); // 打印打开文件失败的错误信息
return -1; // 返回 -1,表示程序错误退出
}
// 设置文件流的缓冲方式为行缓冲,并指定缓冲区大小为 1024 字节
setvbuf(fp, NULL, _IOLBF, 1024);
const char* msg = "hello linux\n"; // 定义要写入文件的字符串,末尾带有换行符
int cnt = 5; // 定义写入的次数(5次)
// 循环 5 次,每次写入 msg 字符串到文件中,并且间隔 2 秒
while(cnt--)
{
// 将 msg 字符串写入文件,并且格式化输出。由于设置了行缓冲,遇到换行符时会刷新缓冲区
fprintf(fp, "%s\n", msg);
sleep(2); // 程序暂停 2 秒,模拟延迟
}
fclose(fp); // 关闭文件,保存写入的内容
return 0; // 程序成功结束,返回 0
}
- 在原来的基础上修改缓冲模式
”setvbuf(fp, NULL, _IOLBF, 1024)“
再次打开监控
while true ;do cat log.txt ;echo "----------------";sleep 1;done
无缓冲(Unbuffered)
- 描述:无缓冲模式下,每次写入操作都会立即发生,数据不会存储到缓冲区中。每次进行读取或写入操作时,都会直接与设备进行交互。这种模式用于对数据一致性有严格要求的应用程序。
- 应用场景:适用于需要即时写入或读取数据的场景,如日志文件、实时数据流、错误处理等。
- 优点:
- 数据实时写入,避免了数据丢失。
- 缺点:
- 性能较差,因为每次I/O操作都需要进行系统调用,增加了操作延迟。
示例:
- 错误日志记录、实时数据采集。
总结:
- 全缓冲:适用于大批量的读写操作,减少I/O次数,提升性能。
- 行缓冲:适用于逐行输出或交互式应用,能够保证数据的实时性。
- 无缓冲:适用于对数据一致性有严格要求的场景,但会影响性能。
多进程下文件缓冲区
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
// 打开文件 "log.txt",如果文件存在则清空,如果不存在则创建该文件并以可写方式打开,权限为0666(所有用户可读写)
int fd = open("log.txt", O_TRUNC | O_CREAT | O_WRONLY, 0666);
const char *fstr = "hello fwrite\n"; // 用于fwrite的字符串
const char *str = "hello write\n"; // 用于write的字符串
// 将标准输出(stdout)的文件描述符重定向到刚才打开的文件(fd)
dup2(fd, 1); // 1是标准输出的文件描述符,这里将标准输出重定向到fd,即“log.txt”
// 以下函数的输出将不会显示在控制台,而是写入到“log.txt”文件中。
printf("hello printf\n"); // 输出“hello printf”到log.txt
fprintf(stdout, "hello fprintf\n"); // 输出“hello fprintf”到log.txt
fwrite(fstr, strlen(fstr), 1, stdout); // 输出“hello fwrite”到log.txt
// 使用低级I/O函数write,输出字符串到log.txt
write(1, str, strlen(str)); // 输出“hello write”到log.txt
fork(); // 创建一个新的进程(子进程)。子进程会继承父进程的标准输出重定向。
return 0; // 程序结束
}
分析
文件打开与 dup2
重定向:
- 使用
open
打开文件log.txt
,并设置文件的权限。如果文件已存在,使用O_TRUNC
将其清空;如果不存在,则创建文件。 dup2(fd, 1)
将标准输出(stdout
)的文件描述符(1)重定向到刚才打开的文件fd
,这样后续的输出都将写入到文件中。
输出重定向:
printf
、fprintf
、fwrite
和write
等函数的输出将不再显示在控制台,而是写入到log.txt
文件中。
进程分叉:
fork()
调用会创建一个新的进程。子进程会继承父进程的文件描述符,因此子进程的输出也会写入到log.txt
。
write
是系统调用,直接在操作系统的缓冲区上直接写入。printf
fprintf
fwrite
是C语言的库函数调用,会写入语言级别的缓冲区。- 最后进程退出的时候C语言缓冲区进行刷新。