Linux 文件缓冲区:高效数据访问的幕后推手

个人主页: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)**是不同的缓冲策略。
  • 优点
    • 提高I/O操作的效率。
    • 程序员可以控制缓冲区的大小和刷新策略,灵活性较高。

C语言缓冲区

在C语言中,FILE* 是一个指向 FILE 结构体的指针,FILE 结构体在标准库中用于表示一个打开的文件。FILE* 用于在文件操作函数中引用文件对象,它是C语言标准库中的一个重要数据类型,用来管理文件的输入输出。

FILE 的内部实现(简化):

c 复制代码
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的延迟。
  • 缺点:
    • 如果程序崩溃,缓冲区中的数据可能未被写入磁盘,导致数据丢失。

示例

  • 文件写操作、网络数据传输。
c 复制代码
#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,文件的写入。

bash 复制代码
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操作,特别是对于大数据量的处理时,性能较差。

示例

  • 打印输出、命令行交互。
cpp 复制代码
#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)"

再次打开监控

bash 复制代码
while true ;do cat log.txt ;echo "----------------";sleep 1;done

无缓冲(Unbuffered)

  • 描述:无缓冲模式下,每次写入操作都会立即发生,数据不会存储到缓冲区中。每次进行读取或写入操作时,都会直接与设备进行交互。这种模式用于对数据一致性有严格要求的应用程序。
  • 应用场景:适用于需要即时写入或读取数据的场景,如日志文件、实时数据流、错误处理等。
  • 优点:
    • 数据实时写入,避免了数据丢失。
  • 缺点:
    • 性能较差,因为每次I/O操作都需要进行系统调用,增加了操作延迟。

示例

  • 错误日志记录、实时数据采集。

总结:

  • 全缓冲:适用于大批量的读写操作,减少I/O次数,提升性能。
  • 行缓冲:适用于逐行输出或交互式应用,能够保证数据的实时性。
  • 无缓冲:适用于对数据一致性有严格要求的场景,但会影响性能。

多进程下文件缓冲区

cpp 复制代码
#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,这样后续的输出都将写入到文件中。

输出重定向:

  • printffprintffwritewrite 等函数的输出将不再显示在控制台,而是写入到 log.txt 文件中。

进程分叉:

  • fork() 调用会创建一个新的进程。子进程会继承父进程的文件描述符,因此子进程的输出也会写入到 log.txt
  • write是系统调用,直接在操作系统的缓冲区上直接写入。
  • printf fprintf fwrite 是C语言的库函数调用,会写入语言级别的缓冲区。
  • 最后进程退出的时候C语言缓冲区进行刷新。
相关推荐
obboda13 分钟前
使用haproxy实现MySQL服务器负载均衡
服务器·mysql·负载均衡
wanhengidc26 分钟前
怎样分辨是否是高防服务器?
运维·服务器·网络
itachi-uchiha29 分钟前
深入理解 Linux 中的 last 和 lastb 命令
java·linux·服务器
gma99931 分钟前
【GB28181】 SIP信令服务器
运维·服务器
张烫麻辣亮。1 小时前
【教程】使用docker+Dify搭建一个本地知识库
运维·docker·容器
不是笨小孩i1 小时前
如何使用Docker一键本地化部署LibrePhotos搭建私有云相册
运维·docker·容器
还有几根头发呀1 小时前
Ubuntu中dpkg命令和apt命令的关系与区别
linux·运维·ubuntu
applebomb2 小时前
ubuntu下r8125网卡重启丢失修复案例一则
linux·ubuntu·驱动·r8125
IT_张三2 小时前
LVS+Keepalived高可用群集配置案例
运维·服务器·lvs
AF012 小时前
Ubuntu系统上部署Node.js项目的完整流程
linux·ubuntu·node.js