🌈欢迎来到Linux专栏~~基础IO
- (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort
- 目前状态:大三非科班啃C++中
- 🌍博客主页:张小姐的猫~江湖背景
- 快上车🚘,握好方向盘跟我有一起打天下嘞!
- 送给自己的一句鸡汤🤔:
- 🔥真正的大师永远怀着一颗学徒的心
- 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏
- 🎉🎉欢迎持续关注!
基础IO
一. 缓冲区
🌈缓冲区是什么
💦缓冲区 (buffer),它是内存空间的一部分。 也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区,显然缓冲区是具有一定大小的
🌈为什么要引入缓冲器
高速设备与低速设备的不匹配(cpu运算是纳秒,内存是微秒,磁盘是毫秒甚至是秒相差1000倍),势必会让高速设备花时间等待低速设备,我们可以在这两者之间设立一个缓冲区
💥举个例子:(顺丰就是缓冲区)
- 可以解除两者的制约关系,数据可以直接送往缓冲区,高速设备不用再等待低速设备,提高了计算机的效率
- 可以减少数据的读写次数,如果每次数据只传输一点数据,就需要传送很多次,这样会浪费很多时间,因为开始读写与终止读写所需要的时间很长,如果将数据送往缓冲区,待缓冲区满后再进行传送会大大减少读写次数,这样就可以节省很多时间。例如:我们想将数据写入到磁盘中,不是立马将数据写到磁盘中,而是先输入缓冲区中,当缓冲区满了以后,再将数据写入到磁盘中,这样就可以减少磁盘的读写次数,不然磁盘很容易坏掉
总的来说:
🌈缓冲区的初步认识
⚡缓冲区刷新策略!(一般+特殊)
- 立即刷新
- 行刷新(行缓冲)
\n
- 满刷新(全缓冲)
- 特殊情况:用户强制刷新(
fflush
)、进程退出(必须刷新)
一般而言 ,行缓冲的设备文件 —— 显示器
全缓冲的设备文件 —— 磁盘文件
💦所以的设备,永远都倾向于全缓冲!(倾向于,但不绝对) —— 缓冲区满了,才刷新 —— 需要更少次的IO操作 —— 也就是更少次的外设访问(1次IO vs 10次IO)—— 也就可以提高效率
🌈其他刷新策略是结合具体情况做的妥协!
- 显示器:直接给用户看的,一方面要照顾效率,一方面要照顾用户的体验( 极端情况,可以自定义规则的)
- 磁盘文件:用户不需要立马看见文件的内容,可以把缓冲区写满再输出,更加注重效率的考量
我们可能有疑问:1000个字节,刷一次是1000个字节,刷十次整体也是1000个字节,哪里效率高呢❓
- 👍和外设进行沟通IO的时候,数据量的大小不是主要矛盾,和外设预备IO的过程才是最耗费时间的
好比:别人找你借钱,每一次都来找你唠嗑大半天,分开十次,沟通的时间花的很久,而转账的时间就几秒钟,一次沟通直接把钱全转过去了,才是效率最高的
🌈解疑答惑
同样的一个程序,向显示器打印输出4行文本,向普通文件(磁盘上)打印的时候,变成了7行,说明上面测试,并不影响系统接口
- C的IO接口是打印了2次的
- 系统接口,只打印了一次
我们最后调用fork,上面的函数已经被执行完了,但不代表数据已经被刷新了
🥑缓冲区是谁提供的
🔥曾经“我们所谈的缓冲区”,绝对不是由OS提供的,如果是OS同一提供,那么我们上面的代码,表现应该是一样的,而不是C的IO接口打印两次,所以是C标准库提供并且维护的用户级缓冲区
fputs
把不是直接把数据直接放进操作系统,而是加载进C标准库的缓冲区中,加载完后自己可以直接返回;如果直接调用的是write接口,则是直接写给OS,不经过缓冲区
- C语言提供的接口都是向显示器打印的,刷新策略都是行刷新,那么最后执行fork的时候 —— 一定是函数执行完了 && 数据已经被刷新了(因为都带
\n
),所以fork执行无意义 - 如你对应的程序进行了重定向 ——> 要向磁盘文件打印 ——> 隐形的刷新策略变成了全缓冲!—— >
\n
便没有意义了 ——> 函数一定执行完了,数据还没有刷新!! 在当前进程对应的C标准库中的缓冲区中!!
这缓冲区的部分数据是父进程的数据吗? 是的
fork
之后,父子分流,父进程的数据发生写时拷贝给子进程,所以C标准库会打印两次
总结:
- 重定向到文件导致:刷新策略改变(变成全缓冲)
- 写时拷贝:父子进程各自刷新一次
🥑用户级缓冲区在哪里?
当我们用
fflush
强制刷新的时候
#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>intmain(){//C语言提供的printf("hello printf\n");fprintf(stdout,"hello fprintf\n");constchar*s ="hello fputs\n";fputs(s,stdout);//OS提供的constchar*ss ="hello write\n";write(1, ss,strlen(ss));//fork之前,强制刷新fflush(stdout);//最后调用fork的时候,上面的函数已经被执行完了fork();//创建子进程 return0;}
结果如下:
数据在fork之前,已经被fflush刷新了,缓冲区里没有数据了,也就不存在写时拷贝。
这里更夸张的是,
fflush(stdout)
只告诉了stdout就能知道缓冲区在哪里?
FILE *fopen(constchar*path,constchar*mode);
- C语言中,open打开文件,返回的是
FILE *
,struct FILE结构体 — 内部封装了fd,还包含了该文件fd对应的语言层的缓冲区结构!(远在天边,近在眼前)
我们可以看看FILE结构体:
//在/usr/include/libio.hstruct_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;//封装的文件描述符#if0int _blksize;#elseint _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. */unsignedshort _cur_column;signedchar _vtable_offset;char _shortbuf[1];/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;#ifdef_IO_USE_OLD_IO_FILE};
所以在C语言上,进行写入的时候放进缓冲区,定期刷新
C语言打开的FILE是文件流。C++中的cout 是类;里面必定包含了 fd、buffer(缓冲区)
🌏设计用户层缓冲区的代码 ~ 实战
💢
struct file
的设计
structMyFILE_{int fd;//文件描述符char buffer[1024];//缓冲区int end;//当前缓冲区的结尾};
💢主函数
open文件 —— fputs输入 —— fclose关闭,接口函数都要我们逐一实现
intmain(){
MyFILE *fp =fopen_("./log.txt","r");if(fp =NULL){printf("open file error");return0;}fputs_("hello world error", fp);fclose_(fp);}
我们发现:C语言的接口一旦打开成功,全部都要带上
FILE*
结构,原因很简单,因为什么数据都在这个FILE结构体中
FILE *fopen(constchar*path,constchar*mode);//以下全是要带FILE*intfputc(int c, FILE *stream);intfclose(FILE *fp);
size_t fread(void*ptr, size_t size, size_t nmemb, FILE *stream);
💢接口实现
💦fputs
//此处刷新策略还没定 全部放进缓冲区voidfputs_(constchar*message, MyFILE *fp){assert(message);assert(fp);strcpy(fp->buffer + fp->end, message);//abcde\0
fp->end +=strlen(message);}
运行结果:
上面覆盖了
\0
,strcpy会在结尾时候自动添加\0
若要往显示器上打印:变成行刷新
if(fp->fd ==0){//标准输入}elseif(fp->fd ==1){//标准输出if(fp->buffer[fp->end-1]=='\n'){//fprintf(stderr, "fflush: %s", fp->buffer); //2write(fp->fd, fp->buffer, fp->end);
fp->end =0;}}elseif(fp->fd ==2){//标准错误}else{//其他文件}}
测试用例:
fputs_("one:hello world error", fp);fputs_("two:hello world error\n", fp);fputs_("three:hello world error", fp);fputs_("four:hello world error\n", fp);
结果:当遇到\n,才刷新
💦fflush刷新
当end!=0 ,就刷新进内核
内核刷新进外设,这就要用一个函数
syncfs
#include<unistd.h>//将缓冲区缓存提交到磁盘intsyncfs(int fd);
具体实现:
voidfflush(MyFILE *fp){assert(fp);if(fp->end !=0){//暂且认为刷新了 ——其实是把数据写到 内核write(fp->fd, fp->buffer, fp->end);syncfs(fp->fd);//将数据写入到磁盘
fp->end =0;}}
💦fclose
关闭之前要先刷新
voidfclose(MyFILE *fp){assert(fp);fflush(fp);close(fp->fd);free(fp);}
💢附源码
#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>#defineNUM1024structMyFILE_{int fd;//文件描述符char buffer[1024];// 缓冲区int end;//当前缓冲区的结尾};typedefstructMyFILE_ MyFILE;//类型重命名
MyFILE *fopen_(constchar*pathname,constchar*mode){assert(pathname);assert(mode);
MyFILE *fp =NULL;//什么也没做,最后返回NULLif(strcmp(mode,"r")==0){}elseif(strcmp(mode,"r+")==0){}elseif(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;}}elseif(strcmp(mode,"w+")==0){}elseif(strcmp(mode,"a")==0){}elseif(strcmp(mode,"a+")==0){}else{//什么都不做}return fp;}//是不是应该是C标准库中的实现!voidfputs_(constchar*message, MyFILE *fp){assert(message);assert(fp);strcpy(fp->buffer+fp->end, message);//abcde\0
fp->end +=strlen(message);//for debugprintf("%s\n", fp->buffer);//暂时没有刷新, 刷新策略是谁来执行的呢?用户通过执行C标准库中的代码逻辑,来完成刷新动作//这里效率提高,体现在哪里呢??因为C提供了缓冲区,那么我们就通过策略,减少了IO的执行次数(不是数据量)if(fp->fd ==0){//标准输入}elseif(fp->fd ==1){//标准输出if(fp->buffer[fp->end-1]=='\n'){//fprintf(stderr, "fflush: %s", fp->buffer); //2write(fp->fd, fp->buffer, fp->end);
fp->end =0;}}elseif(fp->fd ==2){//标准错误}else{//其他文件}}voidfflush_(MyFILE *fp){assert(fp);if(fp->end !=0){//暂且认为刷新了--其实是把数据写到了内核write(fp->fd, fp->buffer, fp->end);syncfs(fp->fd);//将数据写入到磁盘
fp->end =0;}}voidfclose_(MyFILE *fp){assert(fp);fflush_(fp);close(fp->fd);free(fp);}intmain(){close(1);
MyFILE *fp =fopen_("./log.txt","w");if(fp ==NULL){printf("open file error");return1;}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);}
📢写在最后
但行好事,莫问前程
版权归原作者 张小姐的猫 所有, 如有侵权,请联系我们删除。