Linux基础IO --- 内核级和用户级缓冲区磁盘结构磁盘的分治管理block group块组剖析…

Posted rygttm

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux基础IO --- 内核级和用户级缓冲区磁盘结构磁盘的分治管理block group块组剖析…相关的知识,希望对你有一定的参考价值。

出身寒微,不是耻辱。能屈能伸,方为丈夫。

文章目录



一、缓冲区(语言级:IO流缓冲,内核级:块缓冲)

1.观察一个现象

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <string.h>
  4 int main()
  5 
  6     //C接口
  7     printf("hello printf\\n");
  8     fprintf(stdout,"hello fprintf\\n");
  9     fputs("hello fputs\\n",stdout);
 10 
 11     //系统接口
 12     const char* msg = "hello write\\n";
 13     write(1,msg,strlen(msg));//不要把\\0带上
 14 
 15     //fork();                                                                                                                                                              
 16 
 17 
 18     return 0;
 19 

1.
如果没有fork创建子进程的步骤,无论是运行进程还是将运行结果重定向到log.txt文件,两者输出结果都是相同的,均为4条打印信息


2.
若具有了创建子进程的步骤,运行进程后显示到显示器上的结果是4条信息,但如果重定向到log.txt文件中,就变为7条信息,并且可以看到C函数的打印信息被重复打印了两次,而系统调用write接口打印的信息只在log.txt中打印了一次。

3.
可以猜测到的是,log.txt文件中的C函数打印两次,一定和C语言函数有关,并且和创建子进程也有一定的关系,和进程有关,那是不是和写时拷贝有一些关系呢?这些都是我们的猜测,下面来系统的学习一下缓冲区的相关知识。

2.理解缓冲区存在的意义(节省进程IO数据的时间)

1.缓冲区就是一段专门用来作缓存的一块内存空间。

2.
在平常生活中,如果我们想要给远方的朋友送一些东西的话,我们为了节省时间去做其他的事情,一般都会选择快递的方式来邮递东西,如果时间比较紧的话,也会选择顺丰快递来帮我们邮递快递。
快递行业存在的意义,实际上就是为了节省发送者的时间。

3.
在上面例子当中,发送者代表内存,接收者代表磁盘,发送的东西就是数据,顺丰就是缓冲区,我们依靠内存中的进程来将数据写入到磁盘的文件中。

4.
但是我们知道,如果直接将内存中的数据写到磁盘文件中,非常的消耗时间,因为磁盘是外设,外设和内存的速度相比差距非常大,一旦开始访问外设,读取数据的效率就会非常低(讲冯诺依曼那里我们说过),这个时候在内存中就会开辟一段空间,这段空间就是缓冲区,进程会将内存中的数据拷贝到缓冲区里,最后再从缓冲区中将数据输入到磁盘外设里。

5.缓冲区的意义实际上就是为了节省进程进行数据IO的时间。

6.
进程将内存中的数据拷贝到缓冲区,这句话可能有些晦涩难懂,但实际上这个工作就是fwrite做的,与其说fwrite函数是写入到文件的函数,倒不如理解成是拷贝函数,将数据从进程拷贝到“缓冲区”或者“外设”中!!!

3.语言级缓冲区的刷新策略(三种策略,两种特殊情况)

1.
当发送者将快递给到顺丰之后,快递什么时候开始发货,快递公司有自己的规定和策略,可能等到快递数量达到什么样的程度之后,统一开始发货。

2.
所以进程在将数据拷贝到缓冲区之后,缓冲区将数据再刷新到磁盘中,这个过程中缓冲区也有自己的规定和策略,下面我们来谈谈缓冲区的具体刷新策略是什么。

3.
如果有一块数据想要写入到外设中,是一次性将这么多的数据写到外设中效率高,还是将这么多的数据多次少批量的写入到外设中效率高呢?答案显而易见,当然是前者,因为外设的访问速度非常的慢,假设数据output到显示器外设的时间是1s,那么可能990ms的时间都在等待显示器就绪,10ms的时间就已经完成数据的准备工作了,所以访问一个外设是非常辛苦的

4.
缓冲区一定会结合具体的设备,定制自己的刷新策略:
a.立即刷新 — 无缓冲
b.行刷新 — 行缓冲 — 显示器
c.缓冲区满刷新 — 全缓冲 — 磁盘文件

无缓冲:一般情况下,立即刷新这样的场景非常少,比如显示错误信息的时候,例如发生标准错误的时候,编译器会立即将错误信息输出到显示器文件上,也就是外设当中,而不是将信息先存放到缓冲区当中,应当是立即刷新到显示器文件中。

行缓冲:之前写的进度条小程序,带\\n时数据就会立马显示到显示器上,而不带\\n时,就只能通过fflush的方法来刷新数据。上面我们所说的缓冲区数据积累满之后在刷新,本身就是效率很高的刷新策略,那为什么显示器的刷新策略是行缓冲而不是全缓冲呢?是因为显示器设备太特殊了,显示器不是给其他设备或机器看的,而是给人看的,而人的阅读习惯就是从左向右按照行来读取,所以为了保证显示器的刷新效率和提升用户体验,那么显示器最好就是按照行缓冲策略来刷新数据

全缓冲:全缓冲的效率毫无疑问是最高的,因为只需要等待一次设备就绪即可,其他刷新策略等待的次数可就不止一次了,在磁盘文件读写的时候,采用的策略就是全缓冲

5.
两种违反刷新策略的特殊情况:
a.用户强制刷新(fflush)
b.进程退出时,一般都要进行缓冲区刷新

下面图片转载自csdn博主小C博客博主的文章:全缓冲、行缓冲、无缓冲()

4.语言级缓冲区在哪里?(C语言FILE结构体里包含fd和语言级缓冲区)


1.
上面这种现象一定和缓冲区有关,但从现象可以知道缓冲区一定不在操作系统内核中,因为如果在内核中,hello write也应该打印两次。
所以我们之前所谈到的缓冲区,都指的是用户级语言层面给我们提供的缓冲区!!!

2.
这个缓冲区在stdout、stdin、stderr,而这三个流都是FILE*类型的,不管是printf隐式调用stdout,还是fprintf显示调用stdout,都要传文件指针stdout给操作函数,在FILE结构体中不仅有封装的文件描述符fd,例如标准输入,输出,错误对应的FILE结构体中封装的fd是012,FILE中除fd外,实际上还包括了一个缓冲区!!!

3.
所以在我们强制刷新缓冲区时,调用fflush( )或fclose( )时候,必须传文件指针,因为FILE里面有缓冲区


Linux中的标准文件IO流(转载自博客园博主独孤剑—宇枫的文章)

4.
下面是路径下/usr/include/libio.h文件中的struct _IO_FILE结构体,这个结构体实际上就是struct FILE结构体,只不过在/usr/include/stdio.h文件中做了类型的重定义,重定义为FILE类型。


5.
int _flags代表缓冲区的刷新策略,int _fileno代表文件描述符fd,中间一大堆的char*指针维护的内存空间就是进程IO数据时相关的缓冲区。

6.
所以,我们以前进行的所有的C语言操作,fgets、fputs、fprintf函数实际都是把数据先写到文件指针所指结构体内部的缓冲区里

5.用已学知识来解释刚开始的现象(系统调用没有语言级缓冲区,缓冲区刷新就是对数据修改,什么数据被修改就拷贝什么数据,所以写时拷贝后就会出现两份语言级缓冲区的数据。)

1.
如果没有进行重定向,也就是没有将数据写到文件里,而是写到显示器上,那么缓冲区策略就是行刷新,在fork之前,三条C函数已经将数据打印输出到显示器上了,因为输出的字符串末尾有换行符,执行了行刷新策略,所以在FILE内部,也就是进程内部,实际上就不存在对应的数据了


2.
下面代码的运行结果便可以看出,没有语言级缓冲区的write系统调用,在调用时根本不用依靠什么缓冲区刷新策略,这些策略压根不用看,直接打印到显示器即可
而具有缓冲的C函数在调用时,字符串末尾不加换行符\\n,无法满足显示器文件的行刷新策略,所以不会立即将数据刷新到显示器,只有在进程退出的时候才会将数据刷新到显示器上。

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <string.h>
  4 int main()
  5 
  6     //C接口
  7     printf("hello printf");
  8     fprintf(stdout,"hello fprintf");
  9     fputs("hello fputs",stdout);
 10 
 11     //系统接口
 12     const char* msg = "hello write\\n";                                                                                                                                   
 13     write(1,msg,strlen(msg));//不要把\\0带上
 14 
 15     sleep(5);
 16     // fork();
 19     return 0;
 20 


3.
如果进行了重定向,数据写入的对象由显示器文件改为普通文件,那么缓冲区的刷新策略也就由行缓冲变为全缓冲,所以即使C函数打印的三条字符串带了\\n,也不会被立即刷新到普通文件中,因为这点儿字符串不足以将stdout中的缓冲区写满,数据也就不会被立即刷新,而是存放在stdout输出流中的缓冲区里面。
然后继续向下执行fork时,创建了子进程,紧接着就是进程退出(具体哪个进程先退出我们不关心,这是操作系统的事情),只要是进程退出,那就需要进行缓冲区刷新,也就是将数据从缓冲区里拿到外设中,这不就是对数据进行了修改吗?这个时候就会发生写时拷贝,什么数据被修改就会拷贝什么数据,所以物理地址空间中就会有两份语言级缓冲区的数据,等到父子进程都退出的时候,这两份数据就都会被刷新到外设的磁盘文件中了,所以在文件中就会有两份C函数打印的数据,因为C函数具有语言级的IO流缓冲区

4.
至于write系统调用没有被打印两次,是因为write并没有语言级别的缓冲区,只有内核缓冲区,所以write直接在内核中将数据传输到磁盘文件就OK

6.自己写一份代码来模拟封装C语言缓冲区(加深对于C语言缓冲区和内核缓冲区的理解)

1.
下面的代码最精华部分在于mystdio.c源文件里面,通过自己封装的FILE_结构体,fopen_,fwrite_,fclose_,fflush_可以更加清楚的了解到,C语言的IO函数在被调用时,对数据操作的细节和流程,以及当满足刷新策略时,fwite函数是怎么做的,fclose实际内部隐式的包含了fflush,清空缓冲区时利用了惰性释放的方式,这些代码让我们真正从原理上理解了C语言的缓冲区在数据IO时,具体是怎么做的,以及FILE结构体是如何封装的。同时也让我们看到了系统调用write可以直接将数据写到内核缓冲区。

2.
虽然我们所写的肯定不如库函数所写的,但是原理是相同的,只要理解了原理,那么我们的目的就达到了。

  1 #include "mystdio.h"
  2 
  3 int main()
  4 
  5     FILE_ *fp = fopen_("log.txt","w");
  6     if(fp == NULL)
  7     
  8         return 1;
  9     
 10     const char *msg = "hello linux\\n";
 11     fwrite_(msg, strlen(msg), fp);
 12                                                                                                                                                                          
 13     fclose_(fp);  
 14     return 0;
 15         
  1 #include "mystdio.h"
  2 
  3 FILE_ *fopen_(const char *path_name, const char *mode)
  4 
  5     int flags = 0;
  6     int default_mode = 0666;
  7     if(strcmp(mode,"r") == 0)
  8     
  9         flags |= O_RDONLY;
 10     
 11     else if(strcmp(mode,"w") == 0)
 12     
 13         flags |= (O_WRONLY | O_CREAT | O_TRUNC);
 14     
 15     else if(strcmp(mode,"a") == 0) 
 16     
 17         flags |= (O_WRONLY | O_CREAT | O_APPEND);
 18     
 19     umask(0000);
 20 
 21     int fd = 0;
 22     if(flags & O_RDONLY) fd = open(path_name ,flags);
 23     else fd = open(path_name, flags,default_mode);
 24     
 25     if(fd < 0)
 26     
 27         const char *error_msg = strerror(errno);
 28         write(2, error_msg, strlen(error_msg));
 29         return NULL; // 打开文件失败,返回空指针
 30     
 31     FILE_ *fp = (FILE_ *)malloc(sizeof(FILE_));
 32     assert(fp);// malloc申请空间必须成功
 33 
 34     fp->flags = SYNC_LINE;// 默认设置为行刷新
 35     fp->fileno = fd;
 36     fp->capacity = SIZE;
 37     fp->size = 0;
 38     memset(fp->buffer, 0, SIZE);//将所有字节初始化为\\0
 39     
 40     return fp;// 返回FILE_*指针                                                                                                                                          
 41      
 42 
 43 void fwrite_(const void *ptr, int num, FILE_ *fp)
 44 
 45     //1.将数据写入到语言级缓冲区里
 46     memcpy(fp->buffer + fp->size, ptr, num);
 47     //加fp->size的原因是因为打开文件的方式有可能是追加。
 48     //这里不考虑缓冲区溢出的问题,如果你想考虑可以通过realloc的方式来解决,
 49     
 50     fp->size += num;//更新FILE_中的buffer当前使用量
 51     
 52     //2.判断是否满足刷新策略,如果满足那就刷新,不满足就不刷新
 53     if(fp->flags & SYNC_NOW)
 54                                                                                                                                                                         
 55         write(fp->fileno, fp->buffer, num);
 56         fp->size = 0;//相当于清空缓冲区,下次写入时直接覆盖原有缓冲区内容
 57         //惰性释放
 58     
 59     else if(fp->flags & SYNC_LINE)
 60     
 61         //暂时不考虑abc\\ndef这种情况,处理这种情况可以利用for循环遍历,记录\\n位置并将\\n之前的数据刷新到磁盘外设文件中。
 62         if(fp->buffer[fp->size-1] == '\\n')
 63         
 64             write(fp->fileno,fp->buffer,fp->size);
 65             fp->size = 0;//清空缓冲区
 66         
 67     
 68     else if(fp->flags & SYNC_FULL)
 69     
 70         if(fp->size == fp->capacity) 
 71         
 72             write(fp->fileno, fp->buffer, num);
 73             fp->size = 0;//清空缓冲区
 74         
 75     
 76 
 77 void fflush_(FILE_ *fp)
 78 
 79    //fflush做两件事情,1.用户缓冲区数据->内核 2.内核数据->外设
 80    
 81    //系统调用write可以直接将数据写到内核缓冲区里。
 82    if( fp->size > 0 ) write(fp->fileno, fp->buffer,fp->size);
 83    //实际上write可以将任何数据直接写到内核缓冲区中。
 84    fsync(fp->fileno); //将内核缓冲区数据强制性刷新到外设里   
 85    fp->size = 0;//清空缓冲区
 86 
 87 void fclose_(FILE_ *fp)
 88 
 89     //fclose关闭文件,需要先进行语言级缓冲区刷新,然后再关闭文件描述符
 90     fflush_(fp);
 91     close(fp->fileno);
 92 
  1 #pragma once 
  2 
  3 #include <string.h>
  4 #include <stdlib.h>
  5 #include <assert.h>
  6 #include <errno.h>
  7 #include <stdio.h>
  8 #include <sys/types.h>
  9 #include <sys/stat.h>
 10 #include <fcntl.h>
 11 #include <unistd.h>
 12 
 13 #define SIZE 1024
 14 #define SYNC_NOW  (1<<0)
 15 #define SYNC_LINE (1<<1)
 16 #define SYNC_FULL (1<<2)
 17 typedef struct FILE_
 18 
 19     int flags;//刷新策略
 20     int fileno;//文件描述符
 21     int capacity;//buffer总容量
 22     int size;//buffer当前使用量
 23     char buffer[SIZE];
 24 FILE_;
 25 
 26 FILE_ *fopen_(const char *path_name, const char *mode);                                                                                                                  
 27 void fwrite_(const void *ptr, int num, FILE_ *fp);     
 28 void fflush_(FILE_ *fp);                               
 29 void fclose_(FILE_ *fp);   

下面是Makefile文件内容

  1 main: main.c mystdio.c
  2     gcc $^ -o $@ -std=c99
  3 .PHONY:clean
  4 clean:
  5     rm -f main         

3.
上面代码默认的语言级刷新策略是行缓冲,下面做一些实验来验证我们对于缓冲区的理解。

> log.txt --- 清空文件内容
while :; do cat log.txt ; sleep 1; echo "#################"; done
--- 每隔1s查看log.txt文件内容的监控脚本并打印一行分隔符
ctrl + r --- 命令的快速提取

4.
字符串末尾没有\\n时,不满足刷新策略,只有等到调用fclose时,数据才会被刷新到log.txt文件上,所以现象应该是前9秒log.txt文件中什么都没有,最后一秒log.txt文件中直接出现10行hello linux内容。

  1 #include "mystdio.h"
  2 #include <stdio.h>
  3 int main()
  4 
  5     FILE_ *fp = fopen_("log.txt","w");
  6     if(fp == NULL)
  7     
  8         return 1;
  9     
 10     int cnt = 10;
 11     const char *msg = "hello linux";
 12     while(cnt--)
 13     
 14         fwrite_(msg, strlen(msg), fp);
 15         sleep(1);                                                      
 16         printf("count:%d\\n",cnt);
 17     
 18 
 19     fclose_(fp);
 20 
 21     return 0;
 22 

5.
当打印的字符串有\\n时,满足行缓冲刷新策略,则会出现每隔1s,log.txt文件内容会多一行hello linux,因为循环10s,每次都会向log.txt文件中写一行hello linux,所以log.txt文件中hello linux的行数应该是逐渐增加的。



6.
如果当cnt等于5时,我们强制刷新一下文件指针fp,则缓冲区的数据会立马被刷新,所以我们看到的现象应该是前5秒log.txt中什么都没有,然后第5秒时,log.txt直接出现5行hello linux,接下来的4秒什么都没有,等到第10秒时,log.txt直接出现10行hello linux内容。

7.用户级缓冲区和内核级缓冲区的联系(用户级缓冲区在struct FILE结构体,内核级缓冲区在struct file结构体。)

1.
write写入接口,实际上并不是直接将数据写到磁盘中,而是将数据写到内核缓冲区里面,而且fflush也不是将数据刷新到磁盘里,而是将数据从语言级缓冲区刷新到内核缓冲区里,这个内核缓冲区就在OS中的struct file结构体里面,最后由操作系统自主决定将内核缓冲区的数据刷新到磁盘上

2.
我们上面所谈到的刷新策略都是FILE结构体里面的刷新策略,而内核缓冲区的刷新策略是非常复杂的,不像我们上面所说的那样简单,因为操作系统需要兼顾整个内存的使用情况,来决定是否进行内核缓冲区的刷新,然而这却是非常复杂的

3.
所以C函数打印的一个字符串,首先需要被拷贝到FILE中的用户级缓冲区里,然后通过系统调用write再将数据从FILE缓冲区中刷新到file结构体中的内核级缓冲区,最后再由操作系统自主决定将内核级缓冲区的数据刷新到外设物理媒介上。

4.
内核缓冲区刷新数据到磁盘上,这个过程和用户毫无关系

5.
系统调用接口fsync可以用来同步文件内核状态到存储设备中,说白了就是强制刷新内核缓冲区的数据到磁盘(物理媒介)上


6.
fwrite将数据拷贝到用户级缓冲区,write将数据拷贝到内核级缓冲区,本质上fwrite和write函数都是拷贝函数fsync将数据从内核缓冲区写入到磁盘外设中
真正意义上的fflush不仅要将数据从用户缓冲区依靠write拷贝到内核缓冲区,还要将数据从内核缓冲区依靠fsync刷新到外设中

所以即使hello linux后面没有带\\n,也就是数据会被拷贝到用户级缓冲区里面,但只要我们调用了fflush_,数据就会从用户级缓冲区里被最后输入到外设磁盘文件log.txt中,并且会一条一条的增加到log.txt文件中。

  1 #include "mystdio.h"
  2 #include <stdio.h>
  3 int main()
  4 
  5     FILE_ *fp = fopen_("log.txt","w");
  6     if(fp == NULL)
  7     
  8         return 1;
  9     
 10     int cnt = 10;
 11     const char *msg = "hello linux";
 12     while(cnt--)
 13     
 14         fwrite_(msg, strlen(msg), fp);
 15         sleep(1);
 16         fflush_(fp);                                                   
 17         // if(cnt == 5) fflush_(fp);
 18         printf("count:%d\\n"linux IO模型

Linux中环境变量文件

Linux系统下配置环境变量

Linux中环境变量文件及配置

Linux中环境变量文件及配置

Linux 基础IO——文件(中)