一.缓冲区
int main() { printf("hello linux"); sleep(2); return 0; }
对于这样的代码,首先可以肯定的是printf
语句先于sleep
执行,既然如此那么就应该是先打印语句然后进行休眠,下面看看结果:
但这里却是先休眠以后再打印语句,这是因为存在一个叫缓冲区的东西,当我们要向外设写入数据(让显示器显示就是向显示器写入数据)时会将数据暂存至缓冲区,然后在根据缓冲区的刷新策略刷新。
先休眠再显示数据是因为我们并不是直接向外设写入数据,而休眠以后还能刷出数据是因为有缓冲区暂存数据。下面就来谈谈缓冲区。
1.什么是缓冲区
缓冲区的本质就是一块内存(物理内存)
2.缓冲区的意义
我是一个奇思妙想的手艺人,我有一个好朋友叫泰裤辣。每当我打造出一个东西的时候我都会骑着自行车跨越一百多公里去送给他。后来有一天,快递行业兴起了,我有新发明就不用再自己骑着自行车跨越山和大海去给他送了,我只要将我的东西交给快递点,就可以继续回家搞发明,东西有快递公司去给我送,这样就节省了我大量的时间。
那么我就是进程,我的好朋友泰裤辣就是文件,而我的新发明就是数据,缓冲区就是快递点。所以说缓冲区最大意义就在于节省发送者的时间,也就是节省进程的时间。因为外设是一个很慢的东西,当我们访问外设的时候大部分时间都是在等外设准备好,真正写入的时间占比很少。如果有缓冲区的存在,那么进程只要将数据交给缓冲区以后就可以返回去执行后续的代码,缓冲区帮进程承担了等外设准备好的时间代价。
3.缓冲区的刷新策略
但我去寄快递,往往都是我将东西交给快递点一段时间后我的东西才被快递点发出,因为如果一有人寄东西快递点就派车去送这样效率太低百分百亏钱。但是如果是在淡季,等了很长也没有多少人寄快递,快递点也不会说将你的东西留在他那里好几个月。而且如果你是寄一辆轿车大小或者等级的东西,快递点也是会根据你这个情况单次的将你的快递发出。所以虽然快递公司正常情况下是等货物累计要一定数量才发送,但是也会有特殊情况。
同理,缓冲区刷新也是一样,虽然效率最高的是缓冲区满了以后再一次将整个缓冲区中的数据刷新出去(又称全缓冲),但是这个刷新方式只在将数据刷新到磁盘文件上的时候才使用。
向显示器写入数据时,缓冲区采用的方式是行刷新(行缓冲)。这是因为显示器是给用户看的,而我们人的阅读习惯是按行从左到右读取,计算机本质就是给人使用的工具,所以在给显示器刷新的时候采用行刷新。
除了全缓冲和行缓冲以外,还有一种很少见的刷新方式叫无缓冲,也就是说一有数据写入就立马刷新出去。比如printf立马fflush
此外还有两种特殊的刷新方式:
1.用户强制刷新
2.进程退出;进程在退出之前为了防止缓冲区还有数据没被刷新出去导致数据丢失会再刷新一次缓冲区
4.我们目前谈论的缓冲区在哪里
#include<stdio.h> #include<unistd.h> #include<string.h> int main() { //先写一批C语言函数接口 printf("hello printf\n"); fprintf(stdout,"hello fprintf\n"); fputs("hello fputs\n",stdout); //再写一个系统调用 const char*s="hello write\n"; write(1,s,strlen(s)); fork(); return 0; }
上面的代码在直接将结果显示到屏幕中和将结果重定向到文件中是两种不同的结果:
根据上图可以看到,当我们直接将结果输出到屏幕上,一共打印了四条语句这很符合我们的推测。但是一旦将这个输出结果重定向到文件中,就变成了打印七条语句,其中C语言的函数接口被打印了两次。首先这个现象的原因和缓冲区有关,其次和fork有关。
上述现象可以说明我们目前为止在谈论的缓冲区不在内核中,否则系统调用write
也要被打印两次,那么它就只能在用户层。要访问一个文件首先要有这个文件的fd
,所以C语言所用的FILE结构体中一定要包含fd
,那么今天可以知道FILE结构体中肯定也是有缓冲区的,否则为什么我们调用fflush函数都是传FILE*
呢?上面谈论的各种刷新策略也针对的是FILE结构体中的缓冲区。
上述情况的解释:
1.因为显示器是给用户看的外设,所以必须要符合用户按行从左到右的阅读习惯,也就是说向显示器文件中写入时采用的是行刷新,一旦遇到\n
就果断刷新,而向文件中刷新数据为了效率采用的是全缓冲,虽然四条输出语句都带了\n
,但是仍然不足以将缓冲区写满。
2.fork创建的子进程是对父进程的一种拷贝,它们共享代码和数据(包括FILE中的缓冲区),fork之后马上就退出了,进程一旦退出为了防止进程丢失会刷新一次缓冲区,而刷新缓冲区就是将缓冲区清空,这本质上是一种修改,因为进程具有独立性,为了不然子进程的行为影响父进程就会发生写时拷贝,即子进程复制父进程缓冲区的数据并将其刷新到文件中,随后父进程退出再将数据刷新到文件中。
3.系统调用用的是fd,没有FILE结构体,也就没有FILE所提供的缓冲区。
5.仿写FILE
纸上得来终觉浅,绝知此事要躬行。接下来我们就自己通过使用系统调用接口,来尝试封装一下FILE结构体:
5.1myStdio.h
#pragma once #include<unistd.h> #include<assert.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<assert.h> #include<stdlib.h> #include<string.h> #include<stdio.h> //FILE中有缓冲区,刷新方式,以及fd //定义缓冲区大小 #define SIZE 1024 //定义刷新方式 #define SYNC_NOW 1 #define SYNC_LINE 2 #define SYNC_ALL 3 typedef struct FILE_ { int flag;//刷新方式 int feilno;//fd int cap;//记录缓冲区容量 int size;//记录缓冲区使用 char buff[];//缓冲区 }FILE_; //实现四个函数:fopen,fflush,fwrite,fclose FILE_*fopen_(const char*path,const char*mode); void fflush_(FILE_*fp); void fwrite_(const char*ptr,size_t num,FILE_*fp); void fclose_(FILE_*fp);
5.2myStdio.c
#include"myStdio.h" //实现四个函数 FILE_ *fopen_(const char*path,const char*mode) { int flags=0;//设置文件打开的方式 int defaultmode=0666;//设置文件打开的默认权限 if(strcmp(mode,"r")==0) { flags|=O_RDWR; } else if(strcmp(mode,"w")==0) { flags|=O_WRONLY|O_CREAT|O_TRUNC; } else if(strcmp(mode,"a")==0) { flags|=O_WRONLY|O_CREAT|O_APPEND; } int fd=0; if(flags&O_RDWR) fd=open(path,flags); else fd=open(path,flags,defaultmode); if(fd<0)//文件打开失败 { perror("open"); return NULL; } FILE_*fp=(FILE_*)malloc(sizeof(FILE_));//为FILE_结构体开辟空间 assert(fp); //初始化FILE_ fp->cap=SIZE; fp->feilno=fd; fp->flag=SYNC_LINE;//默认设为行刷新 fp->size=0; memset(fp->buff,0,SIZE); return fp; } void fwrite_(const char*ptr,size_t num,FILE_*fp) { //将字符串拷贝到缓冲区 memcpy(fp->buff+fp->size,ptr,num); //更新缓冲区使用量 fp->size+=num; //按照刷新方式刷新 if(fp->flag&SYNC_NOW) { write(fp->feilno,fp->buff,fp->size); fp->size=0; } else if(fp->flag&SYNC_ALL) { if(fp->size==fp->cap) { write(fp->feilno,fp->buff,fp->size); fp->size=0; } } else if(fp->flag&SYNC_LINE) { if(fp->buff[fp->size-1]=='\n')//如果最后一个字符是\n { write(fp->feilno,fp->buff,fp->size); fp->size=0; } } } void fflush_(FILE_*fp) { //所谓刷新,不过就是将缓冲区中的内容刷新到外设中,有内容才刷新 if(fp->size>0) write(fp->feilno,fp->buff,fp->size); fsync(fp->feilno);//强制刷新到磁盘 //刷新完以后缓冲区就没数据了,要将缓冲区置空 fp->size=0; } void fclose_(FILE_*fp)//在关闭文件之前,还要刷新缓冲区 { fflush_(fp); close(fp->feilno); }
6.操作系统的缓冲区
不止用户层有缓冲区,内核中也有一个内核缓冲区。当我们使用C语言文件操作函数写入数据时,首先将数据拷贝到FILE结构体的缓冲区中,并按照无缓冲/行缓冲/全缓冲的刷新策略将数据刷新到内核缓冲区中,最后由操作系统自主将内核缓冲去中的数据刷新到磁盘中。
与其将fwrite等函数理解成写入函数,不如将其理解成拷贝函数
如果你要强制将内核缓冲区中的数据刷新到外设中,可以使用系统调用fsync
。