目录
一. 什么是缓冲区
1.1 缓冲区的概念
1.2 缓冲区存在的价值
二. 缓冲区的刷新策略
三. 缓冲区的提供者和所在的位置
3.1 代码测试缓冲区的提供者
3.2 缓冲区的位置和工作原理
四. 缓冲区的简单模拟实现
五. 总结
缓冲区,就是一段存储空间。当进程要向外部设备中写数据,并不是直接将数据写入到外部设备,而是会先将待读写的数据写到缓冲区,当缓冲区的数据积累到一定量时,再集中将缓冲区中的数据写到外部设备,这样就可以提高IO效率。
进程从外部设备中读数据时,同样也存在一段输入缓冲区。
当进程执行将数据写到外部设备的操作时,有两种IO模式:
不同硬件设备的访问速度遵循的这样的规律:CPU > 内存 > 外部设备(磁盘、显示器、网卡等),这样就造成了如果采用写透模式,进程会频繁多次的访问外部设备,降低效率。
这里通过收发快递的例子来类比缓冲区,我们要寄快递并不是直接将快递送到收件人手上,而是将快递先寄存到菜鸟驿站,当菜鸟驿站中的待发送快递达到一定量时,再集中发送。我们寄出去的快递就好比要进行读写的数据,菜鸟驿站就好比缓冲区。相对于直接将快递送到收件人手上,菜鸟驿站将一批快递集中发送,效率会大幅提高。缓冲区的存在,会很大程度上提高整机效率,最重要的是提高用户响应速度。
或许有人会问,无论是采用写透模式,还是写回模式,进程向外部设备中写数据的数据量是一样的,为什么有缓冲区效率就变高了呢?注意:当进程与外部设备进行IO操作时,数据量并不是主要问题所在,准备IO的过程,是最消耗时间的,缓冲区的存在在数据量不变的条件下,减少了访问外部设备的次数,这样就提高了整机效率。
缓冲区的刷新策略,分为以下三种:
当然,缓冲区的刷新也存在两种特殊情况,需要立即刷新:
一般而言,显示器会采用行缓冲策略,其他的设备基本都采用全缓冲策略。
为了减少进程与外部设备的IO次数,提高效率,所有设备都倾向于采用全缓冲。但是,显示器相对特殊,它不仅要提高效率,还要兼顾用户体验,因此采用行缓冲。
我们猜测,缓冲区的提供者有两种可能:C/C++标准库、操作系统
通过图3.1的代码进行测试获取结论,在代码中先后调用C语言的IO函数printf、fprintf、fputs以及系统接口write向标准输出中打印信息,所有输出的信息都已'\n'结尾,在代码末尾通过fork创建子进程。代码的运行结果见图3.1的右侧,如果直接./test.exe执行代码,那么所有要输出的信息都在屏幕上输出了一次,而如果将进程运行结果输出重定向到log.txt文件中,C语言IO接口printf、fprintf、fputs打印的内容被输出了两次,而系统接口write打印的内容依旧只被输出了一次。
如果缓冲区由OS提供,那么就不应当出现输出重定向到log.txt时,C接口内容输出两次而系统接口内容输出一次的现象。
因此,我们可以断定:缓冲区由C/C++标准库提供。
至于为什么不进行输出重定向时无论C语言IO函数还是系统接口内容都输出一次,则是由于向显示器输出数据时采用行刷新策略,执行fork操作时,缓冲区已经没有了内容。
针对运行图3.1所示代码的允许结果,做出下面的总结:
注意:上面提到的只是C/C++标准库提供的用户级缓冲区,实际上,OS也会提供内核级缓冲区。
通过对语言的学习,我们知道C语言提供了FILE类,用于描述文件的属性信息,进程所打开的每个文件,都有一个FILE*指针与之对应,指向描述这个文件属性信息的结构体。
在FILE结构体中,会存储文件描述符fd,也会有指向该文件对应的缓冲区的指针,FILE的定义见代码3.2,可见C会为每个打开的文件都申请缓冲区,用于该文件的读写。
结论:缓冲区就是一段内存空间,每一个被进程打开的文件都有与之对应的缓冲区。
代码3.2:FILE结构体的定义
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++的IO函数向外部设备(磁盘文件)输出数据时,数据会被先写入目标磁盘文件的缓冲区,当缓冲区的数据积累到了一定的量时,再调用系统接口write,将缓冲区的数据写操作系统的内核缓冲区,在合适的时间,内核缓冲区的数据就会被刷新到外部设备,具体流程参考图3.2。这样相比于每次调用C/C++的IO函数都直接使用系统接口write,效率会有所提高。
从外部设备读取数据时,C/C++标准库提供的缓冲区的工作原理与向外部设备写数据时类似。
只需要在自定义的struct myFILE中定义一段缓冲区,在写文件时先将内容写入到缓冲区中,在缓冲区满、文件关闭或用户强制刷新缓冲区时,调用系统write函数刷新缓冲区,即可模拟实现简单的缓冲区。
#include
#include
#include
#include
#include
#include
#include
#include
#define NUM 10
struct myFile
{
int fd; //文件描述符
char buffer[NUM]; //缓冲区
int end;
};
typedef struct myFile MyFILE;
MyFILE* fopen_(const char* path, const char* mode)
{
assert(path);
assert(mode);
MyFILE* ret = NULL;
int fd = -1;
if(strcmp(mode, "w") == 0)
{
//只写、清空、没有就创建文件
fd = open(path, O_WRONLY|O_CREAT|O_TRUNC, 0666);
}
else if(strcmp(mode, "r") == 0)
{
fd = open(path, O_RDONLY); //只写打开
}
else if(strcmp(mode, "a") == 0)
{
fd = open(path, O_WRONLY|O_CREAT|O_APPEND, 0666);
}
else if(strcmp(mode, "w+") == 0)
{
fd = open(path, O_RDWR|O_CREAT|O_TRUNC, 0666);
}
else if(strcmp(mode, "r+") == 0)
{
fd = open(path, O_RDWR);
}
else if(strcmp(mode, "a+") == 0)
{
fd = open(path, O_RDWR|O_CREAT|O_APPEND, 0666);
}
else
{
printf("mode error!\n");
}
if(fd >= 0)
{
ret = (MyFILE*)malloc(sizeof(MyFILE));
memset(ret, 0, sizeof(MyFILE));
ret->fd = fd;
}
return ret;
}
//缓冲区刷新函数
void fflush_(MyFILE* pf)
{
size_t n = strlen(pf->buffer);
//fprintf(stdout, "%s\n", pf->buffer);
write(pf->fd, pf->buffer, n);
memset(pf->buffer, 0, NUM);
pf->end = 0;
}
//文件关闭函数
void close_(MyFILE* pf)
{
fflush_(pf);
close(pf->fd);
free(pf);
}
void fputs_(const char* s, MyFILE* pf)
{
int len = (int)strlen(s); //要写入的字符数
while(pf->end + len >= NUM)
{
int n = NUM - pf->end - 1; //实际可以读取的字符数
strncpy(pf->buffer + pf->end, s, n); //字符数拷贝到缓冲区
//fprintf(stdout, "%s\n", pf->buffer);
fflush_(pf); //缓冲区满,强制清理
sleep(1);
len -= n; //还没有拷贝的字符数
s += n;
}
strcpy(pf->buffer, s);
pf->end = len;
if(pf->end != 0 && (pf->fd == 1 || pf->fd == 2))
{
//标准输出和标准错误(显示屏)采用行刷新策略
if(s[strlen(s) - 1] == '\n')
{
fflush_(pf);
}
}
}
int main()
{
MyFILE* pf = fopen_("log.txt", "w");
dup2(pf->fd, 2);
//int fd = open("log2.txt", O_RDONLY);
//printf("%d\n", fd);
const char* s1 = "hello world, hello linux, hello everyone\n";
fputs_(s1, pf);
const char* s2 = "zhangHHHHHHHHHH\n";
fputs_(s2, pf);
close_(pf);
return 0;
}