> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。
> 目标:理解缓冲区
> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!
> 专栏选自:Linux初阶
> 望小伙伴们点赞👍收藏✨加关注哟💕💕
🌟前言
缓冲区大家其实不陌生,像我们使用的 VS2019 编译器这里就有缓冲区,那它到底在哪呢,比如我们打印时的窗口需要我们输入,这里就有缓冲区。其实在输入我们也好奇为什么编译器会等待我们输入,这里就不得不谈我们缓冲区的相关知识,那具体是什么呢?今天我们来解开这层面纱。
⭐主体
学习【Linux】基础IO----理解缓冲区咱们按照下面的图解:
🌙 认识缓冲区
💫 为什么有缓冲区
概念:
缓冲区 (buffer),它是内存空间的一部分。 也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区,显然缓冲区是具有一定大小的。
理解:
数据如果直接从内存到磁盘,在内存中速度快,但是访问外设效率比较低,那太消耗时间了,属于外设IO,所以缓冲区的意义就是节省进程进行数据IO的时间!进程需要把数据拷贝到缓冲区里:我们并不需要拷贝,而是调用fwrite,与其理解fwrite是写入到文件的函数,倒不如理解fwrite是拷贝函数,将数据从进程拷贝到缓冲区或者外设当中。
图解:
数据可以直接拷贝到缓冲区,高速设备不用在等待低速设备,提高计算机的效率。
💫 缓冲区如何刷新
概念:
缓冲区的刷新策略:如果有一块数据,一次写入到外设(效率最高)vs如果有一块数据,多次少量写入到外设,需要多次IO。缓冲区一定结合具体的设备定制自己的刷新策略
方法:
- 立即刷新——无缓冲 ,场景较少,比如调用printf直接fflush
- 行刷新——行缓冲——显示器 ,数据的printf带上\n就会立马显示到显示器上。显示器为什么是行缓冲:显示器是外设,进程运行时在内存里的,把数据定期要刷新到外设,显示器设备比较特殊,是给用户来看的,从左到右,所以显示器为了保证刷新效率,并且用户体验良好,所以显示器采用行缓冲,满足用户的阅读体验并且在一定程度上效率不至于太低
- 缓冲区满——全缓冲——磁盘文件,效率最高,只需要一次IO,比如文件读写的时候,直接写到磁盘文件
总结:
但是存在特殊情况:a.用户强制刷新 b,进程退出——一般到要进行缓冲区刷新,所以对于全缓冲,缓冲区满了采取刷新,减少IO次数,提高效率。
💫 缓冲区在哪里呢
缓冲区的位置究竟在哪里???
从上面的例子我们直接往显示器上打印结果为4条,往文件打印为7条,这跟缓冲区有关,同时这也说明了缓冲区一定不在内核中,为什么?如果在内核中write也应该打印两次,write是系统接口。我们之前谈论的所有缓冲区都指的是用户级语言层面提供的缓冲区。这个缓冲区,在stdout,stdin,stderr对应的类型---->FILE*,FILE是一个结构体,里面封装了fd,同时还包括了一个缓冲区!
理解FILE结构体缓冲区:
FILE结构体缓冲区,所以我们直接要强制刷新的时候fflush(文件指针),关闭文件fclose(文件指针),这是因为传进去的文件指针对应的缓冲区。
查看源码来解释FILE结构体:
分析:
总结:
- 所以我们一般所说的缓冲区是语言级别的缓冲区,C语言提供的在FILE结构体里对应的缓冲区。
- 重定向导致刷新策略发生了改变(由行缓冲变成了全缓冲)。同时发生了写时拷贝,父子进程各自刷新
🌙 引入缓冲器
概念分析:
高速设备与低速设备的不匹配(cpu运算是纳秒,内存是微秒,磁盘是毫秒甚至是秒相差1000倍),势必会让高速设备花时间等待低速设备,我们可以在这两者之间设立一个缓冲区。
缓冲区优点:
- 可以解除两者的制约关系,数据可以直接送往缓冲区,高速设备不用再等待低速设备,提高了计算机的效率
- 可以减少数据的读写次数,如果每次数据只传输一点数据,就需要传送很多次,这样会浪费很多时间,因为开始读写与终止读写所需要的时间很长,如果将数据送往缓冲区,待缓冲区满后再进行传送会大大减少读写次数,这样就可以节省很多时间。例如:我们想将数据写入到磁盘中,不是立马将数据写到磁盘中,而是先输入缓冲区中,当缓冲区满了以后,再将数据写入到磁盘中,这样就可以减少磁盘的读写次数,不然磁盘很容易坏掉
🌙 缓冲区答疑
💫 问题一:代码分析
问题抛出:
分析结果:
同样的一个程序,向显示器打印输出4行文本,向普通文件(磁盘上)打印的时候,变成了7行,说明上面测试,并不影响系统接口
- C的IO接口是打印了2次的
- 系统接口,只打印了一次
我们最后调用fork,上面的函数已经被执行完了,但不代表数据已经被刷新了。
💫 问题二:缓冲区是谁提供
曾经“我们所谈的缓冲区”,绝对不是由OS提供的,如果是OS同一提供,那么我们上面的代码,表现应该是一样的,而不是C的IO接口打印两次,所以是C标准库提供并且维护的用户级缓冲区
fputs把不是直接把数据直接放进操作系统,而是加载进C标准库的缓冲区中,加载完后自己可以直接返回;如果直接调用的是write接口,则是直接写给OS,不经过缓冲区
- C语言提供的接口都是向显示器打印的,刷新策略都是行刷新,那么最后执行fork的时候 —— 一定是函数执行完了 && 数据已经被刷新了(因为都带\n),所以fork执行无意义
- 如你对应的程序进行了重定向 ——> 要向磁盘文件打印 ——> 隐形的刷新策略变成了全缓冲!—— > \n便没有意义了 ——> 函数一定执行完了,数据还没有刷新!! 在当前进程对应的C标准库中的缓冲区中!!
🌙 设计用户缓冲区
代码如下:
#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); }
🌟结束语
今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。