LINUX基础IO [七] - 文件缓冲区的深入理解

目录

引入一些奇怪的现象

缓冲区

stdio缓冲区机制

缓冲区在哪?

为什么要有缓冲区呢?

用户缓冲区在哪?

内核缓冲区在哪?


引入一些奇怪的现象

**现象1:**为什么向文件写入的时候 write会先被打印出来?

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
    const char *fstr = "hello fwrite\n";
    const char *str = "hello write\n";

    // C
    printf("hello printf\n"); // stdout -> 1
    fprintf(stdout, "hello fprintf\n"); // stdout -> 1
    fwrite(fstr, strlen(fstr), 1, stdout); // fwrite, stdout -> 1
    // 操作提供的 system call
    write(1, str, strlen(str)); // 1
    return 0;
}

打印结果

**现象2:**为什么加了fork之后,向文件写入时C接口会被调了两次??且向文件写入时write先被打印?

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
    const char *fstr = "hello fwrite\n";
    const char *str = "hello write\n";

    // C
    printf("hello printf\n"); // stdout -> 1
    fprintf(stdout, "hello fprintf\n"); // stdout -> 1
    fwrite(fstr, strlen(fstr), 1, stdout); // fread, stdout -> 1
    // close(1); // 可选,关闭 stdout

    // 操作提供的 system call
    write(1, str, strlen(str)); // 1

    fork(); // 创建子进程

    return 0;
}

打印结果

**现象3:**close1号文件后,为什么就没有结果了??

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
    // const char *fstr = "hello fwrite";
    const char *str = "hello write";

    // C
    // printf("hello printf"); // stdout -> 1
    // fprintf(stdout, "hello fprintf"); // stdout -> 1
    // fwrite(fstr, strlen(fstr), 1, stdout); // fread, stdout -> 1

    // 操作提供的 system call
    write(1, str, strlen(str)); // 1

    close(1);  // 关闭标准输出(文件描述符 1)

    fork();  // 创建子进程

    return 0;
}

打印结果

带着上面的问题,我们继续往下学习缓冲区

缓冲区

stdio缓冲区机制

stdio缓冲区机制 是C语言标准输入输出库(stdio.h)提供的一种用于提高数据读写效率的机制。缓冲区是一段内存区域,用于临时存储输入输出数据**,以减少对磁盘或终端的直接读写次数,从而提高程序性能**。stdio库中的函数,如printf、scanf、fread、fwrite等,都使用了缓冲区机制

缓冲区的策略包括:

  • 无缓冲------>直接刷新------>fflush函数
  • 行缓冲------>遇到/n刷新------>显示器文件
  • 全缓存------>写满才刷新------>普通文件

无缓冲

  • 在无缓冲模式下,不对字符进行缓冲存储,即每次I/O操作都直接进行
  • 标准错误流(stderr)通常是无缓冲的,以确保错误信息能够立即显示

行缓冲

  • 在行缓冲模式下,当遇到换行符(\n)时,会执行I/O操作
  • 当流涉及终端(如标准输出stdout和标准输入stdin)时,通常使用行缓冲模式
  • 这使得输出能够按行显示,而不是等到缓冲区满时才显示

全缓冲

  • 在全缓冲模式下,当缓冲区被填满时,才会进行实际的I/O操作
  • 默认情况下,对磁盘文件的读写操作采用全缓冲模式
  • 缓冲区的大小通常是固定的,如4096字节(但可以通过setvbuf函数调整)

为什么要有这些不同的方案??

一般来说写满再刷新效率高,因为这样可以减少调用系统接口的次数,而显示器之所以是行刷新,因为显示器是要给人给的,按行看符合我们的习惯,而文件采用的就是全缓存策略,因为用户不需要马上看到这些信息,所以这样可以提高效率。 而对于一些特殊情况我们就可以用fllush之前强制刷新出来。 ------>所以方案是根据不同的需求来的!

解释现象1

当我们从向显示器写入转变为向普通文件打印时,此时刷新策略从行刷新转变为全刷新,所以前三个C接口并没有直接写入,而是暂时保存在了缓冲区里面,而write是系统调用接口优先被打印了出来,之后当进程退出的时候,缓冲区的内容才被刷新出来。

解释现象2

跟现象1一样,前三个C接口的数据暂时被存在了缓冲区,而write的调用先被打了出来。当fork的时候,子进程会和父进程指向相同的代码和数据,当其中一方打算刷新缓冲区时,其实就相当于要修改数据,操作系统检测到之后就会发生写时拷贝,于是缓冲区的数据被多拷贝了一份,而后该进程退出时就会再刷新一次,因此C接口写入的数据被调了2次!!

缓冲区在哪?

通过现象3,我们可以观察到,一旦调用 **close()**后,缓冲区的内容就丢失了。因为现代操作系统不做浪费空间和时间的问题所以操作系统在关闭文件之前会自动刷新缓冲区,避免浪费资源。 因此,**close()**作为系统调用,必定会在关闭文件之前确保缓冲区的内容被写入目标设备或文件。而在 close() 之后,缓冲区中的数据已经被清空,无法再访问。

所以我们的库函数接口是先把内容放到一个C提供的缓冲区,当需要刷新的时候,才会去调用write函数进行写入

那么现在我们也清楚了。close后刷新不出来的原因就是:进程退出后想要刷新的时候,文件描述符被关了,所以即使调了write也写不进去,缓冲区的数据被丢弃了

为什么要有缓冲区呢?

举个例子,比方说你和你的好朋友相隔千里,而你想要给他送个键盘,如果没有快递公司和菜鸟驿站(缓冲区)的话,那么你可能得坐车好几天才能到他那里,但如果你的楼下有菜鸟驿站和快递公司,那么你只需要下楼付点钱填个单子就行了,接着你可以去忙你自己的事情,当旁边的人问你键盘去哪里的时候,你会说已经寄给朋友了,其实这个时候你的键盘可能还在快递公司放着。

从总体来看东西是你送还是快递公司送其实都差不多,区别就是你不需要操太多心。因此缓冲区方便了用户!

快递公司可以有不同的策略来提高整体的效率,比方说你这个快递不急,那么我就等快递车装满了再送(全刷新) ,如果比较急,我就装满一个袋子就送(行刷新),如果你特别急,可以通过加钱(fllus强制刷新)来加急。 所以缓冲区解决了效率问题

**配合格式化!**比方说C语言经常需要%d这样的格式化,我们的数字123 最后被打印的时候也是要转化成字符串的123 才能调用write写入,因此我们可以将这个解格式化的工作放在缓冲区去完成!!

理性理解:

CPU 计算速度非常快!而磁盘的读取速度相对于 CPU 来说是非常非常慢的,因此需要先将数据写入缓冲区中,依据不同的刷新策略,将数据刷新至内核缓冲区中,供 CPU 进行使用,这样做的是目的是尽可能的提高效率,节省调用者的时间

本来 IO 就慢,如果没有缓冲区的存在,那么速度会更慢,下面通过一个代码来看看是否进行 IO 时,CPU 的算力差距

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>

using namespace std;

int count = 0;

int main()
{
    //定一个 1 秒的闹钟,查看算力
    alarm(1);   //一秒后闹钟响起
    while(true)
    {
        cout << count++ << endl;
    }
    return 0;
}

最终在 1s 内,count 累加了 10w+ 次(有 IO 的情况下)

下面改变程序,取消IO

cpp 复制代码
int count = 0;

void handler(int signo)
{
    cout << "count: " << count << endl;
    exit(1);
}

int main()
{
    //定一个 1 秒的闹钟,查看算力
    signal(14, handler);
    alarm(1);   //一秒后闹钟响起
    while(true) count++;
    
    return 0;
}

最终在没有 IO 的情况下,count 累加了 5亿+ 次,由此可以看出频繁 IO 对 CPU 计算的影响有多大,假若没有缓冲区,那么整个累加值将会更多(因为需要花费更多的时间在 IO 上)

因此在进行 读取 / 写入 操作时,常常会借助 缓冲区 buffer

cpp 复制代码
#include <iostream>
#include <cassert>
#include <cstdio>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

using namespace std;

int main()
{
    int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    assert(fd != -1);

    char buffer[256] = { 0 };   //缓冲区
    int n = read(0, buffer, sizeof(buffer));    //读取信息至缓冲区中
    buffer[n] = '\0';

    //写入成功后,在写入文件中
    write(fd, buffer, strlen(buffer));

    close(fd);
    return 0;
}

用户缓冲区在哪?

我们回忆一下exit和_exit 区别就是exit会先调用一次fllush把缓冲区的数据刷新出来。我们会注意到fllush传递的参数是FILE* 类型

FILE* 不仅封装了fd的信息,还维护了对应文件的缓冲区字段和文件信息!

FILE*是用户级别的缓冲区(任何语言都属于用户层),当我们打开一个文件的时候语言层给我们malloc(FILE),同时也会维护一个专属于该文件的缓冲区!! 所以如果有10个文件就会有10个缓冲区!

内核缓冲区在哪?

内核缓冲区也是由操作系统的file结构体维护的一段空间,和语言的缓冲区模式是类似的,作为用户我们不需要太关心操作系统什么时候会刷新**,我们只需要认为数据只要刷新到了内核,就必然可以到达硬件,因为现代操作系统不做任何浪费空间和时间的事情。!!**

相关推荐
sin25804 分钟前
蓝桥杯C++基础算法-0-1背包(优化为一维)
c++·算法·蓝桥杯
Wireless_wifi618 分钟前
QCN9274/QCN6274 WiFi 7 Modules: Transforming Mining & Oil Industries
linux·5g·service_mesh
随行就市19 分钟前
树的深度优先(DFS)和广度优先(BFS)算法
算法·深度优先·宽度优先
多多*20 分钟前
Java 双端队列实战 实现滑动窗口 用LinkedList的基类双端队列Deque实现 洛谷[P1886]
java·开发语言·数据结构·算法·cocoa
程序员老冯头22 分钟前
第八节 MATLAB运算符
开发语言·算法·matlab
CYRUS_STUDIO31 分钟前
Android 自定义变形 HMAC 算法
android·算法·安全
uhakadotcom37 分钟前
零基础玩转千卡训练!Modalities框架中文指南:从安装到实战的全解析
算法·面试·github
JZC_xiaozhong1 小时前
单一主数据系统 vs. 统一主数据中心,哪种更优?
大数据·运维·企业数据管理·主数据管理·mdm管理·数据孤岛解决方案·数据集成与应用集成
筑梦之月1 小时前
常用密码学算法分类
算法·密码学