0


【Linux】分析缓冲区,刷新机制,FILE

文章目录


一、Linux的缓冲区

在学习中我们会经常遇到两个缓冲区概念,一个是用户层的缓冲区,另一个是内核层的缓冲区。本文主要讨论用户层缓冲区的知识点以及不同的坑

(一) 用户层缓冲区

标准IO库自带缓冲区,像

stdin

stdout

stderr

这些都是FILE文件流,FILE指向一个FILE结构体,结构体包含了缓冲区基地址和末尾地址,还封装了fd

FILE结构体关键代码如下:

struct_IO_FILE{int _flags;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. */struct_IO_marker*_markers;struct_IO_FILE*_chain;int _fileno;/* fd */};

设置用户层缓冲区的目的和好处:为了减少read,write等系统调用的次数,从而减少用户态和内核态的切换次数,降低系统的开销

(二) 内核层缓冲区(Kernel Buffer Cache)

内核层缓冲区为

buffer

cache

,它们位于内核空间,被所有进程可见。buffer和cache是内存的不同的体现,它们搭建了CPU和磁盘快速交互的桥梁

  • buffer存储暂未写入到磁盘的数据,积攒到一定量后写入磁盘,可以降低和磁盘IO的频率
  • cache实现数据预读的功能:可以暂时存储来自磁盘的数据,提高这部分数据重用性,使得OS无需频繁访问磁盘

设置内核缓冲区的好处:内核缓冲区数据不写回磁盘也能被其它进程读取,在这点的作用上和磁盘存储文件无异,直接读取内核缓冲区的数据,带来了读写的高效性

验证buffer增加和减少

增加buffer
对上述命令的解释如下:

 读取/dev/zero文件时,它会提供无限的空字符nul,一个常见的用法是产生一个特定大小的空白文件
 创建一个1000M的txt文件,其内容为空:
     dd if=/dev/zero of=test.txt count=10M bs=100if:输入文件,默认为标准输入
     of:输出文件名,默认为标准输出
      bs:块大小,同时设置读入/输出的块大小为bytes个字节
      count:块个数

释放缓存

在书房缓存前先指向sync讲缓存的数据写到磁盘避免数据丢失,随后输入

echo 3 >/proc/sys/vm/drop_caches

释放slab和页缓存
在这里插入图片描述

二、缓冲区的刷新策略

(一) 用户层缓冲区刷新策略

缓冲区有三种类型对应三种刷新缓冲区的方式:

  1. 全缓冲 当填满标准I/O缓存后才进行实际I/O操作,如将数据从用户层缓冲区拷贝到内核缓冲区。全缓冲的典型代表是对磁盘文件的读写
  2. 行缓冲 当输入和输出中遇到换行符时才执行实际I/O操作,典型代表是标准输入stdin和标准输出stdout
  3. 无缓冲 不对数据进行缓冲,直接进行I/O,如标准错误stderr就是无缓冲刷新

缓冲区何时会被刷呢&刷新方法:

  1. 调用exit()进程结束时会刷新缓冲区,return会自动调用exit(),注意_exit()不会刷新缓冲区
  2. 缓冲区满了也会被刷新出来
  3. 可通过fflush强制将缓冲流中的数据复制到内核缓冲区中
  4. 流被关闭时也会被刷出来,如调用fclose函数
  5. 行缓冲遇见'\n'会被刷新出来

(二) 内核层缓冲区刷新策略

Linux以页作为高速缓存的单位,因此刷新内核缓冲区即对页的管理,操作系统会基于

LRU

算法回收文件页和匿名页,当缓冲区内容被修改则变为脏页,其数据在合适的时间将会被写到磁盘中去,以保证高速缓存中的数据和磁盘中的数据是一致的。此外:可以通过

sync

命令可以将内存中的数据写入到硬盘中

三、探究缓冲区常见问题的产生

(一) 由于缺失换行符导致内容没有按预期呈现

1、实验设计

编写一段代码(见下),预期是先输出“hello world”,再sleep3秒

#include<stdio.h>#include<unistd.h>intmain(){printf("hello world");sleep(3);return0;}

运行结果如下:

程序运行先

sleep


随后才打印语句
随后才打印“hello world”

2、原理分析

当我们调用

printf

函数往显示器打印字符串时,采用的是行刷新模式,

printf

底层调用

stdout

这个流文件,当遇到

\n

stdout

能立马刷新FILE结构体维护的缓冲区。而上面代码并没有携带

\n

,故

hello world

这个语句一直停留再FILE维护的缓冲区中,直到最后

return 0;

语句调用

exit

函数,

exit

执行清理缓冲区的操作,

hello world

才刷新到屏幕,此时已经到程序末尾,故会出现先sleep才打印字符串的现象

(二) 由于提前close(fd)导致内容无法呈现

1、实验设计

编写一段代码(见下),先以写权限打开txt文档,然后利用

dup2

将1号fd标准输出重定向到txt文档,最后向txt文档写入

Hello 1

,结束后关闭打开的文件

#include<stdio.h>#include<stdlib.h>#include<unistd.h>intmain(){    
    FILE *pfd =fopen("text.txt","w");int fd =fileno(pfd);if(fd<0){perror("open error\n");exit(1);}dup2(fd,1);printf("Hello 1\n");fclose(pfd);close(1);return0;}

运行结果如下:
写入txt失败
实现结果显示

Hello 1

并没有成功写入到txt文档中去,txt文档大小为0

2、原理分析

 分析代码可知,重定向后

printf

语句是往txt文档写入,那么此时采用的是全缓冲刷新

printf

调用后内容一直存在

FILE

的缓冲区当中,当遇到

exit

或者

fflush

或者缓冲区满的时候才刷新,而在

return

语句前

close(1)

,那么

return

时调用

exit

刷新

FILE

的缓冲区时,拿到

FILE

封装的fd后,发现fd对应的文件被关闭,无法刷新缓冲区,导致txt内容为空
解决方法:可以在

close(fd)

前用

fclose(stdout)

fflush(stdout)

提前刷新出来

Q:有读者可能会疑问,在

close(1)

之前执行了

fclose(pfd)

,即关闭了txt的文件流,那么缓冲区的内容应该被刷新了啊,txt应该有内容啊?

A:要注意分清文件流,向txt文档写入内容是

printf

函数,故而字符串语句保存在

stdout

这个文件流的

FILE

结构体中,所以在

close(1)

之前关闭了txt的文件流

pfd

并不能将

stdout

FILE

结构体中缓冲区内容刷新出去,

pfd

stdout

文件流是互相独立的


Q:有读者可能继续追问,在

fclose(pfd)

之前执行了

dup2(fd,1)

,即1号fd指向txt的fd,那么

fclose(pfd)

应该也能刷新1号fd,那么

stdout

FILE

的内容应该会被刷新到文档中啊?

A:首先对于“ 那么fclose(pfd)应该也能刷新1号fd ”这句话是错误的,因为

pfd

FILE

结构体封装的文件描述符一直都是txt的fd,并不会因为重定向了而改变。其次对于“ xxx刷新1号fd,那么stdout的FILE的内容应该会被刷新到文档中 ”也是错的,fd是内核层概念,在本文探讨内容之内对fd的操作是不会影响

FILE

结构体的,即对内核层fd操作不能刷新用户层的

FILE

结构体的缓冲区,但是你刷新用户层的

FILE

结构体的缓冲区能影响到fd对应的文件,因为

FILE

结构体封装了fd

(三) dup重定向不改变缓冲区刷新方式

1、实验设计

在上面代码基础上在dup2前加入一行

printf("Hello 0\n")

代码见下),根据上面的分析txt文档的内容应该为空,终端输出

Hello 0
#include<stdio.h>#include<stdlib.h>#include<unistd.h>intmain(){    
    FILE *pfd =fopen("text.txt","w");int fd =fileno(pfd);if(fd<0){perror("open error\n");exit(1);}printf("Hello 0\n");//增加一行代码                                                             dup2(fd,1);printf("Hello 1\n");fclose(pfd);close(1);return0;}

运行结果如下:
在这里插入图片描述
实验结果显示,终端确实输出了

Hello 0

,但是txt文档居然出现了

Hello 1

,根据上面第(二)点分析应该是空才对,究竟是为什么呢,难道是上面的分析错了?

2、原理分析

首先上面第(二)点分析没错,此处出现这个诡异现象是由其它知识点造成的,直接给出结论:

  • FILE结构体获得时,里面的fd被填充,但是缓冲区还没有被分配,且缓冲刷新方式还没指定
  • 只有当FILE真正发生读写,如printf到屏幕,fwrite到文件,此时FILE才真正分配得缓冲区,且缓冲刷新方式被永久指定
  • dup重定向无法改变FILE的缓冲刷新方式

基于这些结论对上述诡异现象进行解释:
当程序执行到

printf("Hello 0\n");

时,发生向屏幕写的行为,

FILE

结构体的缓冲区还被分配,缓冲刷新方式被指定为行缓冲刷新,之后即使

dup2

重定向,

stdout

这个

FILE

结构体一直是行缓冲刷新,刷新方式不会被更改,故而在执行

printf("Hello 1\n");

时内容直接以行缓冲刷新的方式刷新到txt里,在

close(fd)

前就已经刷新了内容,所以最终txt里有

"Hello 1"

以下对上述结论深入理解和验证

(1) 用例子阐述原理

如何理解缓冲刷新方式被永久指定,举个例子:
当调用

printf

往屏幕输出信息,此时

stdout

这个

FILE

流封装了1号fd,缓冲区被分配,缓冲区刷新方式被指定为行缓冲刷新。后续我们打开了一个txt文件,设其fd=3,我们调用

dup2(3,1)

后再调用

printf

printf

仍是行刷新到txt文件内,并不会因为txt是文件而更换为全缓冲刷新,因为

FILE

的缓冲区刷新方式只能被被指定一次

(2) FILE发生读写时才分配得缓冲区

验证:当FILE结构体获得时其缓冲区还没有被分配,当FILE发生读写时才分配得缓冲区
代码如下,参考深究标准IO的缓存:

#include<stdlib.h>#include<stdio.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>intmain(){char buf[24];     
    FILE *myfile =stdin;printf("before reading\n");printf("myfile base %p\n", myfile);printf("read buffer base %p\n", myfile->_IO_read_base);printf("read buffer length %ld\n", myfile->_IO_read_end - myfile->_IO_read_base);printf("write buffer base %p\n", myfile->_IO_write_base);printf("write buffer length %ld\n", myfile->_IO_write_end - myfile->_IO_write_base);printf("buf buffer base %p\n", myfile->_IO_buf_base);printf("buf buffer length %ld\n", myfile->_IO_buf_end - myfile->_IO_buf_base);printf("\n");fgets(buf,24, myfile);//read                                 printf("after reading\n");printf("read buffer base %p\n", myfile->_IO_read_base);printf("read buffer length %ld\n", myfile->_IO_read_end - myfile->_IO_read_base);printf("write buffer base %p\n", myfile->_IO_write_base);printf("write buffer length %ld\n", myfile->_IO_write_end - myfile->_IO_write_base);printf("buf buffer base %p\n", myfile->_IO_buf_base);printf("buf buffer length %ld\n", myfile->_IO_buf_end - myfile->_IO_buf_base);return0;}

结果如下:
在这里插入图片描述
从实验结果可以看到,还没发生读取时缓冲区地址还没分配,在读入

hello

后,缓冲区被分配,大小为

1024Bytes

(3) 探究FILE刷新策略何时被指定

验证:当FILE结构体获得时其缓冲刷新策略还没有被指定,当FILE发生读写时首次且永久指定刷新策略
首先指出在FILE结构体里的_flags变量的作用相当于位图,它的某些位表示了缓冲区刷新方式

对【 (二) 1、实验设计】中的代码,即还没增加

printf("Hello 0\n");

的代码进行调试
在这里插入图片描述
可以看到,当代码执行完

printf("Hello 1\n");

_flags

值改变,具体而言是低8位到低15位从

0x20

变为

0x28

。接下来我们深入

printf

函数看看到底执行了什么导致标志位改变
在这里插入图片描述

_flags

按位与

_IO_CURRENTLY_PUTTING

后,

_flags

这个位图某些位发生以下变化

0x20:0010 0000
0x28:0010 1000

综上:【 (二) 1、实验设计】中的代码,重定向后执行

printf

stdout

采取的是全缓冲刷新,深入调试查看源代码发现,

_flags

_IO_CURRENTLY_PUTTING

标志位被设置,表示缓冲区内容被设置,但是没有出现对

_flags

行缓冲标志位的设置


那么要对

_flags

设置行缓冲应该设置什么标志位呢?接下来对【 (三) 1、实验设计】中的代码,即增加

printf("Hello 0\n");

的代码进行调试
在这里插入图片描述
可以看到,当代码执行完

printf("Hello 0\n");

,即

stdout

首次发生读写后

_flags

值改变,具体而言是低8位到低15位从

0x20

变为

0x22

。继续深入

printf

函数看看到底执行了什么导致标志位改变
在这里插入图片描述

_flags

按位与

_IO_LINE_BUF

后,

_flags

这个位图某些位发生以下变化

0x20:0010 0000
0x22:0010 0010

故而当FILE结构体是执行行缓冲刷新策略时,

_flags

位图的

_IO_LINE_BUF

标志位被设置为1
此外,在设置

_IO_LINE_BUF

标志位后,由于缓冲区有内容了,所以

_IO_CURRENTLY_PUTTING

标志位也会被设置,故最终执行完

printf("Hello 0\n");

_flags

8位到低15位从

0x20

变为

0x2a

0x20:0010 0000
0x2a:0010 1010

此外笔者还对

setvbuf

函数进行测试,该函数能指定了文件缓冲的模式,函数原型如下:

intsetvbuf(FILE *stream,char*buffer,int mode,size_t size)

关于函数的细节可见这个网站的说明:函数用法
经过设置不同的参数,分别进行gdb调试,再结合上述调试成果,最终得到对

_flags

位图中有关于文件缓冲模式相关的位探究清楚了,结论见下图:

在这里插入图片描述

(四) fork前没有清空缓冲区

fork

前用户层缓冲区仍有数据,在

fork

后父子进程的缓冲区都保留这些数据,故而当

fork

后执行输出时,会出现有些内容重复输出了两次。对此建议读者对用户层缓冲区的刷新机制有所了解,当不确定缓冲区内容是否刷新出去时可以调用

fflush

函数强制刷新

四、总结

本文做了以下工作或得出以下结论

1、系统区分了用户层和内核层缓冲区,指出两者不同之处和特点
(a) 不同之处:两个缓冲区位置不同;用户层缓冲区目的为了减少read,write等系统调用的次数;系统层缓冲区目的为了减少与磁盘IO次数
(b) 相同之处:都是为了提高IO性能,效率

2、 归纳了用户层缓冲区的三种刷新策略/文件缓冲的模式,分别为:行缓冲,全缓冲和无缓冲

3、分析了用户层缓冲区引起的常见问题
(a) 不清楚缓冲区刷新策略是刷新方式导致内容残留
(b) 提前

close(fd)

导致用户层缓冲区数据无法与内核层缓冲区流动
©

fork

导致内容重复输出,本质是

fork

会”复制“用户层数据
(d)

dup

无法更改FILE刷新机制,

FILE

在首次读写文件时根据文件类型永久确定刷新机制

4、在源码层面分析了

FILE

结构体,结构体里有多个指针指向缓冲区维护缓冲区,

_flags

变量以位图模式解读,总结了刷新策略在

_flags

位图上的体现
在这里插入图片描述

之后的工作

  • 尝试从系统数据结构角度分析一个进程运行到结束,printf从调用到输出的流程,预想到的知识点有以下: - task_struct,内存布局,页表- files_structfd_arraystruct fileinode,文件引用次数,VFS
  • 分析对比联系FILEstruct fileinodefd之间关系,如何逐层调用

知识补充

零拷贝技术
本文主要讨论了IO,在传统IO中,当有两个fd需要数据流动,如磁盘文件fd和网络文件fd通信,需要先将数据从磁盘拷贝到内核缓冲区,用户再调用系统接口

read

到用户层,然后再

write

到内核缓冲区,最后由内核缓冲区将数据刷新到网络文件fd。其过程冗长且拷贝频繁,该数据流动过程见下图橙色线,那么有没有更高效的IO方式呢?
在这里插入图片描述

答案是有的,通过零拷贝技术可以完成上图绿色线的数据流动,即数据只通过内核层就能到达对方fd,零拷贝技术有:

sendfile

mmap

对文章内容的总结图:
在这里插入图片描述

说明:此图部分素材来自网络,侵权删

  • 图解说明:  以进程视角开始,task_struct切入,task_struct里有files_struct结构体,其里面有一个数组fd_array,数组下标即为fd,数组内容是struct file,每一个struct file都对应磁盘一个被打开的文件,OS通过管理内核层的struct file来管理磁盘中被打开的文件。当进程打开一个文件,内核会创建struct file,并在fd_array寻找未被使用的最小下标作为fd,数组值填上struct file*指针,同时用户层/语言层面会创建FILE结构体,封装fd,当文件发生首次读写时,FILE结构体指定刷新方式,开辟缓冲区,通过自身封装的fd找到对应的file struct完成读写
  • 几组对应概念: fd和内核层的struct file一一对应 一个fd可以被多个FILE封装,如stdoutstdin封装同一个fd 一个FILE里只有一个fd``````FILE位于用户空间,内核缓冲区位于内核空间
标签: linux 运维 服务器

本文转载自: https://blog.csdn.net/weixin_51429254/article/details/127453147
版权归原作者 TaroYang 所有, 如有侵权,请联系我们删除。

“【Linux】分析缓冲区,刷新机制,FILE”的评论:

还没有评论