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


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


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

    请添加图片描述

    请添加图片描述

    一. 缓冲区

    在这里插入图片描述

    🌈缓冲区是什么

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

    🌈为什么要引入缓冲器

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

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

    在这里插入图片描述

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

    总的来说:

    🌈缓冲区的初步认识

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

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

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

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

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

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

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

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

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

    🌈解疑答惑

    在这里插入图片描述

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

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

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

    🥑缓冲区是谁提供的

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

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

    在这里插入图片描述

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

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

    在这里插入图片描述

    总结:

    • 重定向到文件导致:刷新策略改变(变成全缓冲)
    • 写时拷贝:父子进程各自刷新一次
    🥑用户级缓冲区在哪里?

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

    #include
    #include
    #include
    
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    结果如下:

    在这里插入图片描述

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

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

    FILE *fopen(const char *path, const char *mode);
    
    • 1
    • 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
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

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

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

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

    💢struct file的设计

    在这里插入图片描述

    struct MyFILE_{                  
     	 int fd;            //文件描述符
    	 char buffer[1024]; //缓冲区
    	 int end;           //当前缓冲区的结尾
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    💢主函数

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    我们发现: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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    💢接口实现

    💦fputs

    在这里插入图片描述

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

    运行结果:
    在这里插入图片描述

    上面覆盖了\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
        {
            //其他文件
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    测试用例:

    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);
    
    • 1
    • 2
    • 3
    • 4

    结果:当遇到\n,才刷新
    在这里插入图片描述

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

    #include 
    //将缓冲区缓存提交到磁盘
    int syncfs(int fd);
    
    • 1
    • 2
    • 3

    具体实现:

        void fflush(MyFILE *fp)                  
        {                                        
          assert(fp);                            
          if(fp->end != 0)                       
          {                                      
            //暂且认为刷新了 ——其实是把数据写到  内核
            write(fp->fd, fp->buffer, fp->end);  
            syncfs(fp->fd); //将数据写入到磁盘                                                     
            fp->end = 0;
          }                                                                          
        }  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    💦fclose
    关闭之前要先刷新

      void fclose(MyFILE *fp)
      {
        assert(fp);
        fflush(fp);                                                                                
        close(fp->fd);
        free(fp);
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    💢附源码
    #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;//什么也没做,最后返回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);
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138

    📢写在最后

    但行好事,莫问前程

    请添加图片描述

  • 相关阅读:
    HTML世界核心
    linux同步机制-completion
    【C++基础】2. 标准库
    Mac PS2023/2024储存窗口黑屏不显示 解决方法
    【AI理论学习】语言模型:从Word Embedding到ELMo
    springboot小商户茶叶信息管理毕业设计-附源码
    断点测试怎么做?一文教你用Charles 工具做好接口测试!
    几款好用到爆炸的在线画图工具
    SpriteAtlas
    html简单案例
  • 原文地址:https://blog.csdn.net/qq_42996461/article/details/127859569