【Linux】基础IO —— 缓冲区深度剖析

🌈欢迎来到Linux专栏~~基础IO

  • (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是 Scort
  • 目前状态:大三非科班啃C++中
  • 🌍博客主页:张小姐的猫~江湖背景
  • 快上车🚘,握好方向盘跟我有一起打天下嘞!
  • 送给自己的一句鸡汤🤔:
  • 🔥真正的大师永远怀着一颗学徒的心
  • 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏
  • 🎉🎉欢迎持续关注!
    【Linux】基础IO —— 缓冲区深度剖析

【Linux】基础IO —— 缓冲区深度剖析

基础IO

【Linux】基础IO —— 缓冲区深度剖析

; 一. 缓冲区

【Linux】基础IO —— 缓冲区深度剖析

🌈缓冲区是什么

💦缓冲区 (buffer),它是 内存空间的一部分。 也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区,显然缓冲区是具有一定大小的

🌈为什么要引入缓冲器

高速设备与低速设备的 不匹配(cpu运算是纳秒,内存是微秒,磁盘是毫秒甚至是秒 相差1000倍),势必会 让高速设备花时间等待低速设备,我们可以在这两者之间设立一个缓冲区

💥举个例子:(顺丰就是缓冲区)

【Linux】基础IO —— 缓冲区深度剖析
  • 可以解除两者的制约关系,数据可以直接送往缓冲区,高速设备不用再等待低速设备, 提高了计算机的 效率
  • 可以减少数据的读写次数,如果每次数据只传输一点数据,就需要传送很多次,这样会浪费很多时间,因为开始读写与终止读写所需要的时间很长,如果将数据送往缓冲区,待缓冲区满后再进行传送会大大减少读写次数,这样就可以节省很多时间。例如:我们想将数据写入到磁盘中,不是立马将数据写到磁盘中,而是先输入缓冲区中,当缓冲区满了以后,再将数据写入到磁盘中,这样就可以减少磁盘的读写次数,不然磁盘很容易坏掉

总的来说:

; 🌈缓冲区的初步认识

⚡缓冲区刷新策略!(一般+特殊)

  • 立即刷新
  • 行刷新(行缓冲) \n
  • 满刷新(全缓冲)
  • 特殊情况:用户强制刷新( fflush)、进程退出(必须刷新)

一般而言 ,行缓冲的设备文件 —— 显示器
全缓冲的设备文件 —— 磁盘文件

💦所以的设备,永远都 倾向于全缓冲!(倾向于,但不绝对) —— 缓冲区满了,才刷新 —— 需要 更少次的IO操作 —— 也就是 更少次的外设访问(1次IO vs 10次IO)—— 也就可以 提高效率

🌈其他刷新策略是结合具体情况做的妥协!

  • 显示器:直接给用户看的,一方面要 照顾效率,一方面要 照顾用户的体验( 极端情况,可以自定义规则的)
  • 磁盘文件:用户不需要立马看见文件的内容,可以把缓冲区写满再输出,更加 注重效率的考量

我们可能有疑问:1000个字节,刷一次是1000个字节,刷十次整体也是1000个字节,哪里效率高呢❓

  • 👍和外设进行沟通IO的时候,数据量的大小不是主要矛盾,和 外设预备IO的过程才是 最耗费时间

好比:别人找你借钱,每一次都来找你唠嗑大半天,分开十次, 沟通的时间花的很久,而转账的时间就几秒钟, 一次沟通直接把钱全转过去了,才是 效率最高的

🌈解疑答惑

【Linux】基础IO —— 缓冲区深度剖析

同样的一个程序,向显示器打印输出4行文本,向普通文件(磁盘上)打印的时候,变成了7行,说明上面测试,并不影响系统接口

  1. C的IO接口是打印了2次的
  2. 系统接口,只打印了一次

我们最后调用fork,上面的函数已经被执行完了,但不代表数据已经被刷新了

; 🥑缓冲区是谁提供的

🔥曾经”我们所谈的 缓冲区“,绝对不是由OS提供的,如果是OS同一提供,那么我们上面的代码,表现应该是一样的,而不是C的IO接口打印两次,所以 是C标准库提供并且维护的用户级缓冲区

fputs把不是直接把数据直接放进操作系统,而是加载进C标准库的缓冲区中,加载完后自己可以直接返回;如果直接调用的是write接口,则是直接写给OS,不经过缓冲区

【Linux】基础IO —— 缓冲区深度剖析
  1. C语言提供的接口都是向 显示器打印的,刷新策略都是 行刷新,那么最后执行fork的时候 —— 一定是函数执行完了 && 数据已经被刷新了(因为都带 \n),所以fork执行无意义
  2. 如你对应的程序进行了重定向 ——> 要向磁盘文件打印 ——> 隐形的 刷新策略变成了 全缓冲!—— > \n便没有意义了 ——> 函数一定执行完了,数据还没有刷新!! 在当前进程对应的C标准库中的缓冲区中!!

这缓冲区的部分数据是父进程的数据吗? 是的
fork之后,父子分流,父进程的数据发生 写时拷贝给子进程,所以C标准库会打印两次

【Linux】基础IO —— 缓冲区深度剖析

总结:

  • 重定向到文件导致: 刷新策略改变(变成全缓冲)
  • 写时拷贝:父子进程 各自刷新一次

🥑用户级缓冲区在哪里?

当我们用 fflush强制刷新的时候

#include
#include
#include

int main()
{

    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    const char *s = "hello fputs\n";
    fputs(s, stdout);

    const char *ss = "hello write\n";
    write(1, ss, strlen(ss));

    fflush(stdout);

    fork();
    return 0;
}

结果如下:

【Linux】基础IO —— 缓冲区深度剖析

数据在fork之前,已经被fflush刷新了,缓冲区里没有数据了,也就不存在写时拷贝。

这里更夸张的是, fflush(stdout)只告诉了stdout就能知道 缓冲区在哪里?

FILE *fopen(const char *path, const char *mode);
  • C语言中,open打开文件,返回的是 FILE * ,struct FILE结构体 — 内部封装了fd,还包含了该文件 fd对应的语言层的缓冲区结构!(远在天边,近在眼前)

我们可以看看FILE结构体:


struct _IO_FILE {
     int _flags;
    #define _IO_file_flags _flags

     char* _IO_read_ptr;
     char* _IO_read_end;
     char* _IO_read_base;
     char* _IO_write_base;
     char* _IO_write_ptr;
    char* _IO_write_end;
     char* _IO_buf_base;
     char* _IO_buf_end;

     char *_IO_save_base;
     char *_IO_backup_base;
     char *_IO_save_end;
     struct _IO_marker *_markers;
     struct _IO_FILE *_chain;
    int _fileno;
    #if 0
     int _blksize;
    #else
     int _flags2;
    #endif
    _IO_off_t _old_offset;
    #define __HAVE_COLUMN

     unsigned short _cur_column;
     signed char _vtable_offset;
     char _shortbuf[1];

     _IO_lock_t *_lock;
    #ifdef _IO_USE_OLD_IO_FILE
};

所以在C语言上,进行写入的时候放进缓冲区,定期刷新

C语言打开的FILE是文件流。C++中的cout 是类;里面必定包含了 fd、buffer(缓冲区)

🌏设计用户层缓冲区的代码 ~ 实战

💢 struct file 的设计

【Linux】基础IO —— 缓冲区深度剖析
struct MyFILE_{
     int fd;
     char buffer[1024];
     int end;
};

💢主函数

open文件 —— fputs输入 —— fclose关闭,接口函数都要我们逐一实现

int main()
{
    MyFILE *fp = fopen_("./log.txt", "r");
    if(fp = NULL)
    {
         printf("open file error");
        return 0;
    }

     fputs_("hello world error", fp);
     fclose_(fp);
}

我们发现:C语言的接口一旦打开成功,全部都要带上 FILE*结构,原因很简单,因为什么数据都在这个FILE结构体中

FILE *fopen(const char *path, const char *mode);

int fputc(int c, FILE *stream);
int fclose(FILE *fp);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

💢接口实现

💦fputs

【Linux】基础IO —— 缓冲区深度剖析

    void fputs_(const char *message, MyFILE *fp)
    {
      assert(message);
      assert(fp);

      strcpy(fp->buffer + fp->end, message);
      fp->end += strlen(message);
    }

运行结果:

【Linux】基础IO —— 缓冲区深度剖析

上面覆盖了 \0,strcpy会在结尾时候自动添加\0

若要往显示器上打印:变成 行刷新

    if(fp->fd == 0)
    {

    }
    else if(fp->fd == 1)
    {

        if(fp->buffer[fp->end-1] =='\n' )
        {

            write(fp->fd, fp->buffer, fp->end);
            fp->end = 0;
        }
    }
    else if(fp->fd == 2)
    {

    }
    else
    {

    }
}

测试用例:

fputs_("one:hello world error", fp);
fputs_("two:hello world error\n", fp);
fputs_("three:hello world error", fp);
fputs_("four:hello world error\n", fp);

结果:当遇到\n,才刷新

【Linux】基础IO —— 缓冲区深度剖析

💦fflush刷新
当end!=0 ,就刷新进内核
内核刷新进外设,这就要用一个函数 syncfs

#include

int syncfs(int fd);

具体实现:

    void fflush(MyFILE *fp)
    {
      assert(fp);
      if(fp->end != 0)
      {

        write(fp->fd, fp->buffer, fp->end);
        syncfs(fp->fd);
        fp->end = 0;
      }
    }

💦fclose
关闭之前要先刷新

  void fclose(MyFILE *fp)
  {
    assert(fp);
    fflush(fp);
    close(fp->fd);
    free(fp);
  }

💢附源码

#include
#include
#include
#include
#include
#include
#include
#include

#define NUM 1024

struct MyFILE_{
    int fd;
    char buffer[1024];
    int end;
};

typedef struct MyFILE_ MyFILE;

MyFILE *fopen_(const char *pathname, const char *mode)
{
    assert(pathname);
    assert(mode);

    MyFILE *fp = NULL;

    if(strcmp(mode, "r") == 0)
    {
    }
    else if(strcmp(mode, "r+") == 0)
    {

    }
    else if(strcmp(mode, "w") == 0)
    {

        int fd = open(pathname, O_WRONLY | O_TRUNC | O_CREAT, 0666);
        if(fd >= 0)
        {
            fp = (MyFILE*)malloc(sizeof(MyFILE));
            memset(fp, 0, sizeof(MyFILE));
            fp->fd = fd;
        }
    }
    else if(strcmp(mode, "w+") == 0)
    {

    }
    else if(strcmp(mode, "a") == 0)
    {

    }
    else if(strcmp(mode, "a+") == 0)
    {

    }
    else{

    }

    return fp;
}

void fputs_(const char *message, MyFILE *fp)
{
    assert(message);
    assert(fp);

    strcpy(fp->buffer+fp->end, message);
    fp->end += strlen(message);

    printf("%s\n", fp->buffer);

    if(fp->fd == 0)
    {

    }
    else if(fp->fd == 1)
    {

        if(fp->buffer[fp->end-1] =='\n' )
        {

            write(fp->fd, fp->buffer, fp->end);
            fp->end = 0;
        }
    }
    else if(fp->fd == 2)
    {

    }
    else
    {

    }
}

void fflush_(MyFILE *fp)
{
    assert(fp);

    if(fp->end != 0)
    {

        write(fp->fd, fp->buffer, fp->end);
        syncfs(fp->fd);
        fp->end = 0;
    }
}

void fclose_(MyFILE *fp)
{
    assert(fp);
    fflush_(fp);
    close(fp->fd);
    free(fp);
}

int main()
 {
     close(1);
     MyFILE *fp = fopen_("./log.txt", "w");
     if(fp == NULL)
     {
       printf("open file error");
       return 1;
     }

     fputs_("one:hello world error", fp);
     fputs_("two:hello world error", fp);
     fputs_("three:hello world error", fp);
     fputs_("four:hello world error", fp);
     fclose(fp);
   }

📢写在最后

但行好事,莫问前程

【Linux】基础IO —— 缓冲区深度剖析

Original: https://blog.csdn.net/qq_42996461/article/details/127859569
Author: 张小姐的猫
Title: 【Linux】基础IO —— 缓冲区深度剖析

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/723750/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球