JaQLine
- 关注
考虑到数据传输过程中的效率和速度,Linux系统专门设置了标准IO和系统IO的缓冲,我们这次就来详细讲解一下缓冲的相关知识
内核缓冲
之前已经讲到过write和read函数。这两个函数只是外壳函数,其实它们还是需要进行系统调用。举个例子来说
#include <unistd.h>
#include <stdio.h>
#define BUF_SIZE 1000
int main(){
char buf[BUF_SIZE];
int len = read(0,buf,BUF_SIZE);
buf[len] = '\n';
write(1,buf,len);
}
在Linux中,一切皆文件。而对于文件进行读写,难免需要对磁盘进行操作。就拿这个write函数举例,write函数并不会直接对磁盘进行操作。在我们调用wirte(fd,buf,len)函数的时候,首先会进行系统调用,然后内核将buf中的数据存放到内核的缓冲区中。而此时内核还并未对磁盘进行读写,但是write函数就已经返回。在未来的某一时刻,内核才会将缓冲区的数据写入文件。然后如果这时比较碰巧,另外一个进程读取该文件,read也不会直接对磁盘进行访问。而是read中的系统调用直接读取缓存中的字符然后放入用户空间中的缓冲区。
那么同样在进行读文件的时候,read函数会调用系统调用,然后把磁盘中的数据读入内核的缓冲区中,然后read函数在从内核缓冲区中读取数据。
画一下图就是这个样子的。会发现用户空间全都借用内核空间去和磁盘发生操作,而内核中有缓存,当内核和磁盘的数据交换结束以后用户空间再和内核空间进行数据交换。
那么我们不妨想一下为什么要这样设计?在计算机组成原理中其实有讲过,磁盘的传输效率是很低的,之前大家应该见过这个图
相邻两层之间,高层做低层的高速缓存。其实这个道理也是一样的,如果每一次读写都要去访问磁盘,那么数据的传输效率将会特别的低。
然后我们再来说一下内核缓冲的一些相关知识。内核的缓冲区高速缓存没有固定的大小。其中有两点会影响到高速缓存的大小:1.可用的物理内存总量 2.出于其他目的对物理内存的需求,举例来说,当我们的进程很多的时候,每一个进程的.text段和.data段都需要保留在物理内存中,因此物理内存的占用量就会比较大。除此之外内核会分配尽可能多的高速缓存的内存页。而当高速缓存的大小不足以承载剩下的数据的时候,内核就会将缓存中的数据释放到磁盘上,然后就可以重新使用缓存。
Linux从内核2.4开始就不再维护单独的一个缓存,而是会将文件IO缓冲区放到页面高速缓存中。页高速缓存缓存的是页面,当我们访问一个页面的数据以后,由于还可能对该数据进行访问,那么该页面就会被缓存起来。
标准库缓冲
设置缓冲区
标准IO库也就是在linux中,帮助我们封装了write、read等系统IO函数。我们调用的printf、scanf等函数,本质上还是调用write、read等函数。标准库帮助我们封装系统IO函数的同时,还帮助我们定义了一些操作缓冲的函数,我们不需要自己对缓冲区进行操作(缓冲区确实不是必要的,但是众所周知每次系统调用都需要消耗额外的时间。为了加快进程执行任务的效率,添加缓冲区是很重要的)。
首先看一下标准库的缓冲
#include <stdio.h>
int main(){
printf("hello world");
_exit(0);
}
然后我们编译并且输出会发现什么也输出不了。这正是因为标准库帮助我们定义了缓冲区。有些函数可以对标准IO的缓冲区进行一些操作,例如setvbuf函数
一共有四个参数,第一个参数就是要选择的文件流;第二个参数是我们定义的缓冲区,当然我们第二个参数可以填写NULL,如果填NULL的话标准库会默认帮助我们定义缓冲区,并且我们的size也会被忽略掉(如果不为NULL,那么size就表示缓冲区的大小。而且缓冲区应该存放在堆中,如果存放在栈中函数结束就会销毁栈空间而造成一些意想不到的后果);mode参数则代表了缓冲的类型:
1._IONBF表示无缓冲。stdio库将会立刻调用read或者write函数,并且忽略buf和size参数。标准错误流默认使用该模式,就是为了如果程序出现错误能将错误情况立即汇报
2._IOLBF表示行缓冲。终端设备的文件流默认为行缓冲模式。对于输出流,在换行符或缓冲区满之前将会缓存数据。输入流则是一次读取一行(看一下之前代码,无法输出字符就是因为将字符缓存起来,如果字符最后加上\n则会立即输出)
3._IOFBF表示全缓冲,对磁盘的读写默认使用全缓冲模式。单次读写数据的大小与缓冲区大小相同,在全缓冲满之前将会把数据缓存
上面的图片中除了有setvbuf,还有setbuf函数,它们都是同族函数(都是对缓冲区进行操作)。setbuf其实就是对setvbuf的封装,等价于setvbuf(fp,buf,(buf==NULL)?_IOFBF:_IONBF,BUFSIZ)
。其中BUFSIZ是一个stdio中定义的宏。
如果我们不想使用系统给我们的缓冲区大小,那么我们就使用setbuffer函数,该函数也是对setvbuf的封装,等价于setvbuf(fp,buf,(buf==NULL)?_IOFBF:_IONBF,user_size)
刷新缓冲区
刷新缓冲区用到了fflush函数
参数为一个文件流,无论该文件使用的是哪种缓冲模式,都会直接刷新该文件流输出缓冲区(刷新输出缓冲区,其实本质上就是调用wirte()系统调用将数据放入内核缓冲区中)。里面的指针如果为NULL的话,就会刷新所有标准IO的缓冲区。
当我们关闭一个文件流时,将会自动刷新其stdio缓冲。
对文件IO的内核缓冲进行操作
前面我们了解了内核缓冲的概念,我们先来巩固一下。内核缓冲是为了避免多次对磁盘进行直接访问而存在的。当用户直接读取磁盘的文件时,其实就是将内核缓冲中的数据存放到用户缓冲。如果内核缓冲区中还没有数据,那么进程的请求就会被放入内核的请求队列中去,然后将该进程挂起并且去为其它进程服务。
等到数据被读取到内核缓冲,内核就会通知进程并且将进程唤醒。但是有这么一种情况,应用程序需要确保在执行下一条指令之前就确保将数据存放到磁盘中,于是Linux中有fsync函数。
首先有一个概念,叫做文件的元数据。其实就是数据的数据。一个文件,里面存放了一些数据,而这个文件的大小、文件名、上次更改时间等信息就被称为文件的元数据。
我们首先理解一下什么是同步IO和异步IO。这两中IO最大的区别就是当执行IO操作时进程是否会被挂起。当同步IO执行操作的时候,比如我们进行scanf输入操作,终端一直等待我们输入而不执行其它的指令。在此时进程处于挂起状态,只要我们不进行输入,进程就不会继续执行。而异步IO则是当执行IO操作的时候,只要进程触发IO操作就会立即返回,而不会等待我们进行输入。
同步IO是否完成是有一个定义的:"要么已经成功完成到磁盘的数据传递,要么被诊断为不成功"。
根据读写IO,有两个IO成功完成的定义:
1."就读操作而言,被请求的文件数据已经从磁盘传递给进程。若存在任何影响到所请求数据的挂起写操作,那么在执行读操作之前,会将这些数据传递到磁盘"
2."就写操作而言,意味着写请求所指定的数据已经传递至磁盘完毕,且用于获取数据的所有文件元数据也已传递至磁盘完毕"
同时出来了两个状态,synchronized I/O data integrity completion状态和Synchronized I/O file integrity completion状态。第一种状态表示对一次文件的更新传递了足够的信息。第二种状态表示表示发生的所有更新全部传递到磁盘上。可以简单的理解为第一种状态是第二种状态的子集。
然后我们看这个函数
该函数是一个系统调用,将使缓冲数据与文件描述符fd相关的所有元数据都刷新到磁盘上。在对磁盘设备的传递完成以后才会进行返回。该函数调用会使文件处于Synchronized I/O file integrity completion状态
下面的这个函数
将会使文件IO处于synchronized I/O data integrity completion状态。第二个函数请求磁盘的次数将会比第一个函数要少。
我们举一个具体的例子来说明一下:现在有一个文件,我们对它进行了更改,比如原来输入的是hello world,然后我们改成了HELLO WORLD。如果我们调用的是第一个函数,那么该文件的所有元数据将全部被访问一遍,并且查看哪里有进行过更改,然后将文件内容和其元数据进行更新。而第二个函数,我们只是更改了内容的大小写,因此该函数可能不会访问文件内容的大小这个数据,而文件的更新时间也可能不会立即写入到磁盘。
fsync函数本质上调用了系统调用sync函数,该函数将会使包含更新文件信息的所有数据的内核缓冲全部刷新到磁盘上。
在open函数中还有这样的一个标志——O_SYNC标志,如果加上该标志则接下来调用的write函数将会自动将文件的数据和元数据刷新到磁盘上
总结:这次我们学习了内核缓冲、标准IO缓冲的概念,它们的作用,以及对这两种缓冲进行操作的函数。其实对于缓冲来说我们完全可以进行绕过,但是这种方法我不准备进行大篇幅的讲解,就是一个open的标志而已。因为绕过缓冲并不能使进程的效率更高,反而还会出现其它的问题。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
