目录
输入输出缓冲区
介绍
实际上是一块内存空间
用于临时存储数据,以便在数据传输或处理期间提高性能
作用
如果没有缓冲区 -- 写透模式
- 当我们写入数据后,就需要内存自己和磁盘直接交互
- 但磁盘属于外存,和内存的速度差别很大,这样会使整个过程效率变得很低
- 而这样的传输模式,被叫做写透模式(WT)
当我们有缓冲区后 -- 写回模式
- 可以将要写入的内容交给它,让缓冲区去送
- 而缓冲区是内存中的一个特定区域,相当于是内存和内存之间交互,速度就快很多(这里的速度主要是用户的响应速度)
- 这样的模式叫做写回模式(WB)
用户的响应速度
- 因为数据通常是在后台写回主存的
- 所以读取操作不必等待主存写入完成,从而降低了读取操作的延迟
- 而数据的快速读取通常可以加速用户响应
刷新策略
介绍
是涉及将缓冲区中的数据写入磁盘或其他持久性存储介质的决策
在许多编程语言和操作系统中,程序员可以选择何时刷新或清空缓冲区,以控制数据的传输和一致性
一般
立即刷新
行刷新(\n)
满刷新
特殊
用户强制刷新(fflush)
进程退出时
文件关闭时
显示器的行刷新
实际上,所有的设备都是倾向于全刷新的(可以减少对外设的访问)
- 因为和外设预备IO是最耗费时间的
但是,显示器是行刷新的,这是为什么?
- 其实行刷新是综合考量后决定的策略
- 因为设备最终目的还是为了让用户有更好的使用体验
- 显示器一行一行的打印,符合我们的阅读习惯
父子进程之间的缓冲区问题
引入
我们使用三种函数向标准输出写入数据,并且在进程结束前,创建一个子进程
运行结果:
看起来没有什么问题
输出重定向后
当我们把输出结果重定向到log.txt中:
输出结果怎么变了呢?
分析
- 第一次是向显示器写入,但重定向到文件中后,就是向文件写入了
- 所以我们可以猜测,是不是写入位置的差别呢?
- 除此之外,printf和fprintf都是c库函数,只有write是系统调用,也只有它的数据只打印了一次
- 前面说了,我们写入的数据都是先被存放在缓冲区的
- 所以也许是缓冲区不同导致的,不同的缓冲区有不同的刷新策略
- 也就是说 -- [系统调用用到的缓冲区]和[语言级别函数用的缓冲区]应该不是同一个
原理
用户级缓冲区 和 内核级缓冲区
用户级缓冲区
由程序员在应用程序中明确定义和管理的缓冲区
这种缓冲区用于在程序内部进行数据的处理和临时存储
内核级缓冲区
在操作系统内核中管理的缓冲区,用于在用户空间程序和硬件设备(如磁盘、网络接口)之间进行数据传输
内核级缓冲区通常由操作系统自动管理,程序员无法直接控制或访问它们
关系
当用户程序执行文件读取或写入操作时,数据首先从用户级缓冲区写入内核级缓冲区,然后由内核负责将数据传输到硬件设备或文件系统
同样,当数据从硬件设备或文件系统传输到内核级缓冲区时,用户程序可以从内核级缓冲区中读取数据并存储到用户级缓冲区
也就是 -- 程序 - 用户级缓冲区 - 内核级缓冲区 - 硬件
原理过程
写入的缓冲区不同
其中,printf和fprintf都是语言级别的函数,使用标准I/O库进行缓冲管理
- 数据首先被写入标准输出的用户级缓冲区
- 然后由标准I/O库负责将数据从用户级缓冲区传输到内核级缓冲区
write是系统调用函数,直接向文件描述符写入,而不使用标准I/O库
- 数据首先被写入文件描述符的内核级缓冲区
- 然后由操作系统内核负责将数据传输到标准输出设备
文件刷新策略 -- 全刷新
- 当我们向显示器写入时,缓冲区刷新策略是行刷新
- 但一旦重定向后,它会隐形的变成全缓冲(从显示器重定向为了普通文件,普通文件的刷新策略就是全缓冲)
- 因此当执行到fork时,父进程中的缓冲区的内容还存在
刷新时发生写时拷贝
- 创建子进程时,父子进程共享缓冲区
- 创建之后就该执行return了,进程要结束了,缓冲区要被强制刷新
- 但刷新缓冲区实际上就是要改变缓冲区,所以要发生写时拷贝
- 但是!!!这个拷贝只包括用户级缓冲区,毕竟拷贝一份内核级缓冲区其实毫无意义
- 那么父子进程各自都拥有了自己的用户级缓冲区
- 所以进程结束后,会将自己缓冲区的内容刷新出来,写入用户级缓冲区的数据也就会显示2次噜
用户级缓冲区的位置
引入
- 还记得我们每次调用c语言中的文件操作函数时,都要使用FILE*参数吗?
- 说明FILE结构体中包含了我们操作需要的大部分信息
- 其中当然也包括缓冲区结构(每个文件打开后,都有自己的缓冲区结构,也叫做文件缓冲区)
FILE结构体内部的定义
模拟实现用户级缓冲区
前提代码
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<malloc.h> #include<fcntl.h> #include<string.h> #include<sys/stat.h> #include<sys/types.h> #include<assert.h> #define num 1024 typedef struct myfile{ //模拟的FILE结构体 int fd; char buffer[num]; int end; //可以表示字符数 }myfile; myfile* _fopen(const char* pathname,const char* option){ assert(pathname); assert(option); myfile* fp=NULL; //先定义为null,如果中间有失败,就可以返回null int fd=0; //通过不同的option,以不同方式打开文件 if(strcmp(option,"r")==0){ fd=open(pathname,O_RDONLY,0666); if(fd>=0){ //成功打开后,就可以初始化我们的结构体 fp=(myfile*)malloc(sizeof(myfile)); //为结构体开辟空间 memset(fp->buffer,0,sizeof(fp->buffer)); //初始化缓冲区 fp->end=0; fp->fd=fd; } } if(strcmp(option,"w")==0){ fd=open(pathname,O_WRONLY|O_TRUNC|O_CREAT,0666); if(fd>=0){ fp=(myfile*)malloc(sizeof(myfile)); memset(fp->buffer,0,sizeof(fp->buffer)); fp->end=0; fp->fd=fd; } } if(strcmp(option,"a")==0||strcmp(option,"a+")==0){ fd=open(pathname,O_WRONLY|O_APPEND|O_CREAT,0666); if(fd>=0){ fp=(myfile*)malloc(sizeof(myfile)); memset(fp->buffer,0,sizeof(fp->buffer)); fp->end=0; fp->fd=fd; } } return fp; } void _fflush(myfile* fp){ //模拟fflush 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); }
模拟将内容写入显示器
void _fputs(const char* str,myfile* fp){ //行写入 assert(fp); assert(str); strcpy(fp->buffer+fp->end,str); //拷贝内容到缓冲区中 fp->end+=strlen(str); //字符数增加 fprintf(stderr,"prev:%s ",fp->buffer); //因为已经关闭了stdout //只能将内容打印到stderr中,stderr绑定的也是显示器 if(fp->fd==1){ //打开显示器时 if(fp->buffer[fp->end-1]=='\n'){ //行刷新 write(fp->fd,fp->buffer,fp->end); //遇到\n就将缓冲区写入文件 memset(fp->buffer,0,sizeof(fp->buffer)); fp->end=0; } fprintf(stderr,"after:%s\n",fp->buffer); } } int main(){ close(1); //先关闭标准输出,才能让我们打开的文件占据标准输出的位置 const char* arr1="hello "; const char* arr2="world\n"; myfile* fp=_fopen("log.txt","w"); //向文件写入,相当于向标准输出写入 if(fp==NULL){ printf("_fopen error\n"); return 1; } printf("fd:%d\n",fp->fd); _fputs(arr1,fp); _fputs(arr2,fp); _fputs(arr1,fp); _fputs(arr2,fp); _fclose(fp); return 0; }
(code打印结果为缓冲区内容,log.txt模拟为显示器),结果如图所示:
- 为了让log.txt为显示器,所以要先关闭stdout(fd=1),这样log.txt的fd就是1了
- 由于关闭了stdout,当我们想要打印内容到显示器上时,可以借助stderr来打印(它也绑定的是显示器)
模拟将内容写入文件
写入文件是满刷新,从前面的结果来看,可以猜测code的前后无差别void _fputs(const char* str,myfile* fp){ assert(fp); assert(str); strcpy(fp->buffer+fp->end,str); fp->end+=strlen(str); printf("prev:%s ",fp->buffer); if(fp->fd==1){ if(fp->buffer[fp->end-1]=='\n'){ write(fp->fd,fp->buffer,fp->end); memset(fp->buffer,0,sizeof(fp->buffer)); fp->end=0; } } printf("after:%s\n",fp->buffer); } int main(){ const char* arr1="hello "; const char* arr2="world\n"; myfile* fp=_fopen("log.txt","w"); //写入该文件 if(fp==NULL){ printf("_fopen error\n"); return 1; } printf("fd:%d\n",fp->fd); _fputs(arr1,fp); sleep(1); _fputs(arr2,fp); _fclose(fp); return 0; }