前置博客 基础IO
为什么有缓冲 因为磁盘的读写
与内存的读写
操作速度相比,磁盘的读写是相差数量级的慢,所以为了提高内存多次 ,频繁 读写磁盘文件的效率,缓冲区
被投入使用。尤其是内存内容写入 磁盘时,常常先写入内存级缓冲区
,再在特定规则下一次性将缓冲区
的内容写入磁盘
**本文以C语言
提供的用户级缓冲区为例介绍缓冲区
缓冲区的刷新规则 首先当一个进程正常退出 时,会先刷新缓冲区再关闭文件,此时必定有一次刷新
而当进程运行时 缓冲区的刷新策略主要有以下三种
无缓冲
内容直接写入文件
行缓冲
输入一般内容不刷新,遇到\n
时刷新一次缓冲区
全缓冲
缓冲区有容量限制,满了 之后就刷新
认识一下C语言的缓冲区 这里的系统环境是Linux
刷新规则 运行如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdio.h> #include <unistd.h> int main () { FILE* pfile = fopen("file.txt" ,"w" ); fprintf (stdout ,"stdout" ); fprintf (stderr ,"strerr" ); fprintf (pfile,"file" ); _exit(0 ); return ; }
终端和文件的内容为:
可以看到只有标准错误输出
有实际的输出,而标准输出
和文件输出
都没有输出
目前可以得出:
因此我们再运行如下代码,再输出内容后面加上\n
换行
1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> #include <unistd.h> int main () { FILE* pfile = fopen("file.txt" ,"w" ); fprintf (stdout ,"stdout\n" ); fprintf (pfile,"file\n" ); _exit(0 ); return ; }
终端输出内容为
而文件依然为空
由此可得:
标准输出
遵循行缓冲
的刷新规则
文件输出
遵循全缓冲
的刷新规则
缓冲区在fork中的行为 1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> #include <unistd.h> #include <sys/types.h> int main () { printf ("hello1 " ); fprintf (stdout ,"hello2 " ); fork(); return 0 ; }
上段代码的输出内容为
1 hello1 hello2 hello1 hello2
可见fork
前的缓冲区内容被打印了两次(父子进程各一次),所以fork
也会复制缓冲区
的内容
实际上缓冲区
属于进程的一部分,且fork
时遵循写时拷贝
模拟封装Linux下C语言的文件接口(包括缓冲区) 主要目标 采用Mystdio.h
声明,Mystdio.c
实现的方式,封装read
,write
,close
系统调用接口。并提供用户级缓冲区和缓冲区的刷新等功能
声明结构体和接口 我们先把主要的接口和主要的内容做出来看看封装效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #ifndef __MYSTDIO_H__ #define __MYSTDIO_H__ #include <string.h> typedef struct IO_FILE { int fileno; }_FILE; _FILE * _fopen(const char *filename,const char *flag); int _fwrite(_FILE* fp,const char *s, int len);void _fclose(_FILE* fp);#endif
实现无缓冲区的接口 实现的部分由Mystdio.c
完成
头文件 这里的头文件要能够提供使用系统调用接口,以及调用堆区的接口,所以 头文件如下:
1 2 3 4 5 6 #include "Mystdio.h" #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <unistd.h>
_fopen函数 我们先模拟实现fopen
函数的主要功能,主要实现"w"``"a"``"r"
的打开模式
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 #define FILE_MODE 0666 _FILE * _fopen(const char *filename,const char *flag) { int f = 0 ; int fd = -1 ; if (strcmp (flag,"w" ) == 0 ) { f = (O_CREAT|O_WRONLY|O_TRUNC); fd = open(filename,f,FILE_MODE); } else if (strcmp (flag,"a" ) == 0 ) { f = (O_CREAT|O_WRONLY|O_APPEND); fd = open(filename,f,FILE_MODE); } else if (strcmp (flag,"w" ) == 0 ) { f = O_RDONLY; fd = open(filename,f); } else return NULL ; if (fd == -1 ) return NULL ; _FILE *fp = (_FILE*)malloc (sizeof (_FILE)); fp->fileno = fp; return fp; }
_fwrite函数 ————无缓冲区 然后是先简单地写一个没有缓冲区的_fwrite
带缓冲区版本的稍后添加
1 2 3 4 int _fwrite(_FILE *fp,const char * s,int len){ return wrtie(fp->fileno,s,len); }
_fclose函数 ————不刷新缓冲区 这里也是先写个没缓冲区的
1 2 3 4 5 6 void _fclose(_FILE* fp){ if (fp == NULL ) return ; close(fp->fileno); free (fp); }
为接口适配缓冲区 为_FILE
结构体添加输入输出缓冲区 缓冲区
和刷新规则标志
声明在结构体中,使每个打开的文件都有独立的用户级缓冲区和刷新规则
1 2 3 4 5 6 7 8 9 10 11 12 13 #define SIZE 1024 #define FLUSH_NONE 0 #define FLUSH_LINE 1 #define FLUSH_ALL 2 typedef struct IO_FILE { int fileno; int flag; char inbuffer[SIZE]; int in_pos; char outbuffer[SIZE]; int out_pos; }_FILE;
fopen添加语句 fopen
仅需添加几句用于初始化的代码
1 2 3 4 5 6 fp->fileno = fd; fp->flag = FLUSH_LINE; fp->in_pos = 0 ; fp->out_pos = 0 ;
fwrite重写 fwrite
要根据不同的刷新方式进行写入
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 int _fwrite(_FILE *fp,const char * s,int len){ memcpy (&(fp->outbuffer[fp->out_pos]),s,len); fp->out_pos +=len; if (fp->fileno == FLUSH_NONE) { write(fp->fileno,s,fp->out_pos); } else if (fp->flag == FLUSH_LINE) { if (fp->outbuffer[fp->out_pos-1 ] == '\n' ) { write(fp->fileno,s,fp->out_pos); fp->out_pos = 0 ; } else { return len; } } else if (fp->flag == FLUSH_ALL) { if (fp->out_pos == SIZE) { write(fp->fileno,s,fp->out_pos); fp->out_pos = 0 ; } else { return len; } } }
添加_fflush
和修改_fclose
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void _fflush(_FILE* fp){ if (fp->out_pos > 0 ) { write(fp->fileno,fp->outbuffer,fp->out_pos); fp->out_pos = 0 ; } } void _fclose(_FILE* fp){ if (fp == NULL ) return ; _fflush(fp); close(fp->fileno); free (fp); }
小结 至此,我们已经封装了基本的写入功能,更多的细节可自行完善
戳我去github仓库🔗 查看源文件