深度剖析缓存区
- 1.💭缓冲区介绍
- 1.1💫什么是缓冲区
- 1.2💫缓冲区有什么用
- 1.3 💫缓冲区的初步认识
- 2.💭缓存区测试
- 3.🥑缓冲区是谁提供的
- 3.1测试 一下 在fork()之前刷新缓冲区会怎么样?
- 3.3总结
- 4.C语言源码剖析
- 5.手撕一个缓冲区
1.💭缓冲区介绍
1.1💫什么是缓冲区
缓冲区 (buffer),它是内存空间的一部分。 也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区,显然缓冲区是具有一定大小的
1.2💫缓冲区有什么用
高速设备与低速设备的不匹配(cpu运算是纳秒,内存是微秒,磁盘是毫秒甚至是秒相差1000倍),势必会让高速设备花时间等待低速设备,我们可以在这两者之间设立一个缓冲区
💥举个例子:(顺丰就是缓冲区)
- 可以解除两者的制约关系,数据可以直接送往缓冲区,高速设备不用再等待低速设备,提高了计算机的效率
- 可以减少数据的读写次数,如果每次数据只传输一点数据,就需要传送很多次,这样会浪费很多时间,因为开始读写与终止读写所需要的时间很长,如果将数据送往缓冲区,待缓冲区满后再进行传送会大大减少读写次数,这样就可以节省很多时间。例如:我们想将数据写入到磁盘中,不是立马将数据写到磁盘中,而是先输入缓冲区中,当缓冲区满了以后,再将数据写入到磁盘中,这样就可以减少磁盘的读写次数,不然磁盘很容易坏掉
1.3 💫缓冲区的初步认识
⚡缓冲区刷新策略!(一般+特殊)
- 立即刷新
- 行刷新(行缓冲) \n
- 满刷新(全缓冲)
- 特殊情况:用户强制刷新(fflush)、进程退出(必须刷新)
💦所以的设备,永远都倾向于全缓冲!(倾向于,但不绝对) —— 缓冲区满了,才刷新 —— 需要更少次的IO操作 —— 也就是更少次的外设访问(1次IO vs 10次IO)—— 也就可以提高效率
🌈其他刷新策略是结合具体情况做的妥协!
显示器:直接给用户看的,一方面要照顾效率,一方面要照顾用户的体验( 极端情况,可以自定义规则的)
磁盘文件:用户不需要立马看见文件的内容,可以把缓冲区写满再输出,更加注重效率的考量
我们可能有疑问:1000个字节,刷一次是1000个字节,刷十次整体也是1000个字节,哪里效率高呢❓
👍和外设进行沟通IO的时候,数据量的大小不是主要矛盾,和外设预备IO的过程才是最耗费时间的
好比:别人找你借钱,每一次都来找你唠嗑大半天,分开十次,沟通的时间花的很久,而转账的时间就几秒钟,一次沟通直接把钱全转过去了,才是效率最高的
2.💭缓存区测试
- 🌀有fork()
- 🌀没有fork()
由上面对比可见:
同样的一个程序,向显示器打印输出4行文本,向普通文件(磁盘上)打印的时候,变成了7行,说明上面测试,并不影响系统接口
C的IO接口是打印了2次的
系统接口,只打印了一次
我们最后调用fork,上面的函数已经被执行完了,但不代表数据已经被刷新了
调用了fork之后把缓冲区的数据刷新了 所以会把数据写入到log.txt里面两次
3.🥑缓冲区是谁提供的
🔥曾经“我们所谈的缓冲区”,绝对不是由OS提供的,如果是OS同一提供,那么我们上面的代码,表现应该是一样的,而不是C的IO接口打印两次,所以是C标准库提供并且维护的用户级缓冲区
fputs把不是直接把数据直接放进操作系统,而是加载进C标准库的缓冲区中,加载完后自己可以直接返回;如果直接调用的是write接口,则是直接写给OS,不经过缓冲区
C语言提供的接口都是向显示器打印的,刷新策略都是行刷新,那么最后执行fork的时候 —— 一定是函数执行完了 && 数据已经被刷新了(因为都带\n),所以fork执行无意义
如你对应的程序进行了重定向 ——> 要向磁盘文件打印 ——> 隐形的刷新策略变成了全缓冲!—— > \n便没有意义了 ——> 函数一定执行完了,数据还没有刷新!! 在当前进程对应的C标准库中的缓冲区中!!
这缓冲区的部分数据是父进程的数据吗? 是的
fork之后,父子分流,父进程的数据发生写时拷贝给子进程,所以C标准库会打印两次
3.1测试 一下 在fork()之前刷新缓冲区会怎么样?
可见 刷新了缓冲区之后 就只打印了一份~
3.3总结
- 重定向到文件导致:刷新策略改变(变成全缓冲)
- 写时拷贝:父子进程各自刷新一次
4.C语言源码剖析
我们知道缓冲区不是OS提供的而是C语言提供的 下面我们看一下文件源代码:
//在/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语言打开的FILE是文件流。C++中的cout 是类;里面必定包含了 fd、buffer(缓冲区)
5.手撕一个缓冲区
- 代码
#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);
}