输入输出缓冲区(写透模式,写回模式,刷新策略),父子进程之前的缓冲区问题(用户级缓冲区和内核级缓冲区的介绍和关系,问题原因),文件缓冲区位置,模拟实现文件缓冲区(写入文件,写入显示器)

目录

输入输出缓冲区

介绍

作用 

如果没有缓冲区 -- 写透模式

当我们有缓冲区后 -- 写回模式

用户的响应速度

刷新策略  

介绍

一般

特殊

显示器的行刷新

父子进程之间的缓冲区问题

引入

输出重定向后

分析 

原理

用户级缓冲区 和 内核级缓冲区

用户级缓冲区

内核级缓冲区

关系

原理过程

写入的缓冲区不同

文件刷新策略 -- 全刷新

刷新时发生写时拷贝

用户级缓冲区的位置

引入

FILE结构体内部的定义

模拟实现用户级缓冲区

前提代码

模拟将内容写入显示器

模拟将内容写入文件


输入输出缓冲区

介绍

实际上是一块内存空间

用于临时存储数据,以便在数据传输或处理期间提高性能

作用 

如果没有缓冲区 -- 写透模式

  • 当我们写入数据后,就需要内存自己和磁盘直接交互
  • 但磁盘属于外存,和内存的速度差别很大,这样会使整个过程效率变得很低
  • 而这样的传输模式,被叫做写透模式(WT)

当我们有缓冲区后 -- 写回模式

  • 可以将要写入的内容交给它,让缓冲区去送
  • 而缓冲区是内存中的一个特定区域,相当于是内存和内存之间交互,速度就快很多(这里的速度主要是用户的响应速度)
  • 这样的模式叫做写回模式(WB)
用户的响应速度
  • 因为数据通常是在后台写回主存的
  • 所以读取操作不必等待主存写入完成,从而降低了读取操作的延迟
  • 而数据的快速读取通常可以加速用户响应

刷新策略  

介绍

是涉及将缓冲区中的数据写入磁盘或其他持久性存储介质的决策

在许多编程语言和操作系统中,程序员可以选择何时刷新或清空缓冲区,以控制数据的传输和一致性

一般

立即刷新

行刷新(\n)

满刷新

特殊

用户强制刷新(fflush)

进程退出时

文件关闭时

显示器的行刷新

实际上,所有的设备都是倾向于全刷新的(可以减少对外设的访问)

  • 因为和外设预备IO是最耗费时间的

但是,显示器是行刷新的,这是为什么?

  • 其实行刷新是综合考量后决定的策略
  • 因为设备最终目的还是为了让用户有更好的使用体验
  • 显示器一行一行的打印,符合我们的阅读习惯

父子进程之间的缓冲区问题

引入

我们使用三种函数向标准输出写入数据,并且在进程结束前,创建一个子进程

输入输出缓冲区(写透模式,写回模式,刷新策略),父子进程之前的缓冲区问题(用户级缓冲区和内核级缓冲区的介绍和关系,问题原因),文件缓冲区位置,模拟实现文件缓冲区(写入文件,写入显示器)_第1张图片

运行结果:

看起来没有什么问题

输出重定向后

当我们把输出结果重定向到log.txt中:

输入输出缓冲区(写透模式,写回模式,刷新策略),父子进程之前的缓冲区问题(用户级缓冲区和内核级缓冲区的介绍和关系,问题原因),文件缓冲区位置,模拟实现文件缓冲区(写入文件,写入显示器)_第2张图片

输出结果怎么变了呢?

分析 

  • 第一次是向显示器写入,但重定向到文件中后,就是向文件写入了
  • 所以我们可以猜测,是不是写入位置的差别呢?
  • 除此之外,printf和fprintf都是c库函数,只有write是系统调用,也只有它的数据只打印了一次
  • 前面说了,我们写入的数据都是先被存放在缓冲区的
  • 所以也许是缓冲区不同导致的,不同的缓冲区有不同的刷新策略
  • 也就是说 -- [系统调用用到的缓冲区]和[语言级别函数用的缓冲区]应该不是同一个

原理

用户级缓冲区 和 内核级缓冲区

输入输出缓冲区(写透模式,写回模式,刷新策略),父子进程之前的缓冲区问题(用户级缓冲区和内核级缓冲区的介绍和关系,问题原因),文件缓冲区位置,模拟实现文件缓冲区(写入文件,写入显示器)_第3张图片

用户级缓冲区

由程序员在应用程序中明确定义和管理的缓冲区

这种缓冲区用于在程序内部进行数据的处理和临时存储

内核级缓冲区

在操作系统内核中管理的缓冲区,用于在用户空间程序和硬件设备(如磁盘、网络接口)之间进行数据传输

内核级缓冲区通常由操作系统自动管理,程序员无法直接控制或访问它们

关系
  • 当用户程序执行文件读取或写入操作时,数据首先从用户级缓冲区写入内核级缓冲区,然后由内核负责将数据传输到硬件设备或文件系统

  • 同样,当数据从硬件设备或文件系统传输到内核级缓冲区时,用户程序可以从内核级缓冲区中读取数据并存储到用户级缓冲区

也就是 -- 程序 - 用户级缓冲区 - 内核级缓冲区 - 硬件

原理过程
写入的缓冲区不同

其中,printf和fprintf都是语言级别的函数,使用标准I/O库进行缓冲管理

  • 数据首先被写入标准输出的用户级缓冲区
  • 然后由标准I/O库负责将数据从用户级缓冲区传输到内核级缓冲区

write是系统调用函数,直接向文件描述符写入,而不使用标准I/O库

  • 数据首先被写入文件描述符的内核级缓冲区
  • 然后由操作系统内核负责将数据传输到标准输出设备
文件刷新策略 -- 全刷新
  • 当我们向显示器写入时,缓冲区刷新策略是行刷新
  • 但一旦重定向后,它会隐形的变成全缓冲(从显示器重定向为了普通文件,普通文件的刷新策略就是全缓冲)
  • 因此当执行到fork时,父进程中的缓冲区的内容还存在
刷新时发生写时拷贝
  • 创建子进程时,父子进程共享缓冲区
  • 创建之后就该执行return了,进程要结束了,缓冲区要被强制刷新
  • 刷新缓冲区实际上就是要改变缓冲区,所以要发生写时拷贝
  • 但是!!!这个拷贝只包括用户级缓冲区,毕竟拷贝一份内核级缓冲区其实毫无意义
  • 那么父子进程各自都拥有了自己的用户级缓冲区
  • 所以进程结束后,会将自己缓冲区的内容刷新出来,写入用户级缓冲区的数据也就会显示2次噜

用户级缓冲区的位置

引入

  • 还记得我们每次调用c语言中的文件操作函数时,都要使用FILE*参数吗?
  • 说明FILE结构体中包含了我们操作需要的大部分信息
  • 其中当然也包括缓冲区结构(每个文件打开后,都有自己的缓冲区结构,也叫做文件缓冲区)

FILE结构体内部的定义

  • 输入输出缓冲区(写透模式,写回模式,刷新策略),父子进程之前的缓冲区问题(用户级缓冲区和内核级缓冲区的介绍和关系,问题原因),文件缓冲区位置,模拟实现文件缓冲区(写入文件,写入显示器)_第4张图片
  • 输入输出缓冲区(写透模式,写回模式,刷新策略),父子进程之前的缓冲区问题(用户级缓冲区和内核级缓冲区的介绍和关系,问题原因),文件缓冲区位置,模拟实现文件缓冲区(写入文件,写入显示器)_第5张图片

模拟实现用户级缓冲区

前提代码

#include
#include
#include
#include
#include
#include
#include
#include
#include

#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模拟为显示器),结果如图所示:

输入输出缓冲区(写透模式,写回模式,刷新策略),父子进程之前的缓冲区问题(用户级缓冲区和内核级缓冲区的介绍和关系,问题原因),文件缓冲区位置,模拟实现文件缓冲区(写入文件,写入显示器)_第6张图片

  • 为了让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;
}
输入输出缓冲区(写透模式,写回模式,刷新策略),父子进程之前的缓冲区问题(用户级缓冲区和内核级缓冲区的介绍和关系,问题原因),文件缓冲区位置,模拟实现文件缓冲区(写入文件,写入显示器)_第7张图片

你可能感兴趣的:(linux,c,linux,c)