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

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

一. 缓冲区


0a2653c851af460fa595bd959398a8f1.png


🌈缓冲区是什么


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


🌈为什么要引入缓冲器


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


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


2d65d23f6d4748949b924e4057485923.png


可以解除两者的制约关系,数据可以直接送往缓冲区,高速设备不用再等待低速设备,提高了计算机的效率

可以减少数据的读写次数,如果每次数据只传输一点数据,就需要传送很多次,这样会浪费很多时间,因为开始读写与终止读写所需要的时间很长,如果将数据送往缓冲区,待缓冲区满后再进行传送会大大减少读写次数,这样就可以节省很多时间。例如:我们想将数据写入到磁盘中,不是立马将数据写到磁盘中,而是先输入缓冲区中,当缓冲区满了以后,再将数据写入到磁盘中,这样就可以减少磁盘的读写次数,不然磁盘很容易坏掉

总的来说:


🌈缓冲区的初步认识


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


立即刷新

行刷新(行缓冲) \n

满刷新(全缓冲)

特殊情况:用户强制刷新(fflush)、进程退出(必须刷新)

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

全缓冲的设备文件 —— 磁盘文件


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


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


显示器:直接给用户看的,一方面要照顾效率,一方面要照顾用户的体验( 极端情况,可以自定义规则的)

磁盘文件:用户不需要立马看见文件的内容,可以把缓冲区写满再输出,更加注重效率的考量

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


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

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


🌈解疑答惑


0a2653c851af460fa595bd959398a8f1.png


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


C的IO接口是打印了2次的

系统接口,只打印了一次

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


🥑缓冲区是谁提供的

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


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


2d65d23f6d4748949b924e4057485923.png


C语言提供的接口都是向显示器打印的,刷新策略都是行刷新,那么最后执行fork的时候 —— 一定是函数执行完了 && 数据已经被刷新了(因为都带\n),所以fork执行无意义

如你对应的程序进行了重定向 ——> 要向磁盘文件打印 ——> 隐形的刷新策略变成了全缓冲!—— > \n便没有意义了 ——> 函数一定执行完了,数据还没有刷新!! 在当前进程对应的C标准库中的缓冲区中!!

这缓冲区的部分数据是父进程的数据吗? 是的

fork之后,父子分流,父进程的数据发生写时拷贝给子进程,所以C标准库会打印两次


0a2653c851af460fa595bd959398a8f1.png


总结:


重定向到文件导致:刷新策略改变(变成全缓冲)

写时拷贝:父子进程各自刷新一次

🥑用户级缓冲区在哪里?

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


#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
  //C语言提供的
    printf("hello printf\n");
    fprintf(stdout, "hello fprintf\n");
    const char *s = "hello fputs\n";
    fputs(s, stdout);
    //OS提供的
    const char *ss = "hello write\n";
    write(1, ss, strlen(ss));
    //fork之前,强制刷新
    fflush(stdout);
    //最后调用fork的时候,上面的函数已经被执行完了
    fork();//创建子进程                                                                      
    return 0;
}


结果如下:


2d65d23f6d4748949b924e4057485923.png


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


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


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


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

我们可以看看FILE结构体:


//在/usr/include/libio.h
struct _IO_FILE {
  int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
  #define _IO_file_flags _flags
  //缓冲区相关
  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr; /* Current read pointer */
  char* _IO_read_end; /* End of get area. */
  char* _IO_read_base; /* Start of putback+get area. */
  char* _IO_write_base; /* Start of put area. */
     char* _IO_write_ptr; /* Current put pointer. */
    char* _IO_write_end; /* End of put area. */
  char* _IO_buf_base; /* Start of reserve area. */
  char* _IO_buf_end; /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base; /* Pointer to first valid character of backup  area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */
  struct _IO_marker *_markers;
  struct _IO_FILE *_chain;
  int _fileno; //封装的文件描述符
  #if 0
  int _blksize;
  #else
  int _flags2;
  #endif
  _IO_off_t _old_offset; /* This used to be _offset but it's too small. */
  #define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];
  /* char* _save_gptr; char* _save_egptr; */
  _IO_lock_t *_lock;
  #ifdef _IO_USE_OLD_IO_FILE
};


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


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


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

💢struct file的设计


0a2653c851af460fa595bd959398a8f1.png


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);
//以下全是要带FILE*
int fputc(int c, FILE *stream);
int fclose(FILE *fp);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);


💢接口实现

💦fputs


0a2653c851af460fa595bd959398a8f1.png


//此处刷新策略还没定   全部放进缓冲区
    void fputs_(const char *message, MyFILE *fp)                                 
    {                                                                            
      assert(message);                                                           
      assert(fp);                                                                
      strcpy(fp->buffer + fp->end, message);//abcde\0
      fp->end += strlen(message);                                                              
    }


运行结果:


0a2653c851af460fa595bd959398a8f1.png


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


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


if(fp->fd == 0)
    {
        //标准输入
    }
    else if(fp->fd == 1)
    {
        //标准输出
        if(fp->buffer[fp->end-1] =='\n' )
        {
            //fprintf(stderr, "fflush: %s", fp->buffer); //2
            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,才刷新


💦fflush刷新

当end!=0 ,就刷新进内核

内核刷新进外设,这就要用一个函数syncfs


#include <unistd.h>
//将缓冲区缓存提交到磁盘
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 <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <stdlib.h>
#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;//什么也没做,最后返回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;
}
//是不是应该是C标准库中的实现!
void fputs_(const char *message, MyFILE *fp)
{
    assert(message);
    assert(fp);
    strcpy(fp->buffer+fp->end, message); //abcde\0
    fp->end += strlen(message);
    //for debug
    printf("%s\n", fp->buffer);
    //暂时没有刷新, 刷新策略是谁来执行的呢?用户通过执行C标准库中的代码逻辑,来完成刷新动作
    //这里效率提高,体现在哪里呢??因为C提供了缓冲区,那么我们就通过策略,减少了IO的执行次数(不是数据量)
    if(fp->fd == 0)
    {
        //标准输入
    }
    else if(fp->fd == 1)
    {
        //标准输出
        if(fp->buffer[fp->end-1] =='\n' )
        {
            //fprintf(stderr, "fflush: %s", fp->buffer); //2
            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);
   }
相关文章
|
6天前
|
机器学习/深度学习 缓存 监控
linux查看CPU、内存、网络、磁盘IO命令
`Linux`系统中,使用`top`命令查看CPU状态,要查看CPU详细信息,可利用`cat /proc/cpuinfo`相关命令。`free`命令用于查看内存使用情况。网络相关命令包括`ifconfig`(查看网卡状态)、`ifdown/ifup`(禁用/启用网卡)、`netstat`(列出网络连接,如`-tuln`组合)以及`nslookup`、`ping`、`telnet`、`traceroute`等。磁盘IO方面,`iostat`(如`-k -p ALL`)显示磁盘IO统计,`iotop`(如`-o -d 1`)则用于查看磁盘IO瓶颈。
|
6天前
|
Linux
Linux操作系统调优相关工具(三)查看IO运行状态相关工具 查看哪个磁盘或分区最繁忙?
Linux操作系统调优相关工具(三)查看IO运行状态相关工具 查看哪个磁盘或分区最繁忙?
31 0
|
6天前
|
Unix Linux Shell
【探索Linux】P.12(文件描述符 | 重定向 | 基础IO)
【探索Linux】P.12(文件描述符 | 重定向 | 基础IO)
14 0
|
4天前
|
存储 Linux C语言
|
6天前
|
Unix Linux 开发工具
【探索Linux】P.11(基础IO,文件操作)
【探索Linux】P.11(基础IO,文件操作)
13 0
|
6天前
|
缓存 Linux Shell
[Linux打怪升级之路]-缓冲区
[Linux打怪升级之路]-缓冲区
|
6天前
|
存储 缓存 监控
|
6天前
|
存储 缓存 Linux
【linux基础I/O(二)】文件系统讲解以及文件缓冲区的概念
【linux基础I/O(二)】文件系统讲解以及文件缓冲区的概念
|
6天前
|
监控 Linux API
Linux内核探幽:深入浅出IO模型
在Linux操作系统中,I/O(输入/输出)模型是一套定义如何处理数据读写的机制,它对系统性能有着重要影响。为了适应不同的应用场景和性能需求,Linux抽象出了多种I/O模型。每种模型都有其独特的特点、底层原理、优劣势以及适用场景。🤓
Linux内核探幽:深入浅出IO模型