linux文件io缓冲

出于速度和效率考虑,系统io调用(即内核)和标准 C语言库的io函数(即 stdin 函数)在操作磁盘文件时会对数据进行缓冲。

read(),write()系统调用在操作磁盘文件时不会直接发起磁盘访问,而是仅仅在用户空间缓冲区与内核缓冲区高速缓存( kemel buffer cache )之间复制数据。

例如,如下调用将 3 个字节的数据从用户空间内存传递到内核空间的缓冲区中

write ( fd , " abc " , 3 ) ; 

write()随即返回。在后续某个时刻,内核会将其缓冲区中的数据写入(刷新至)磁盘。(因此,可以说系统调用与磁盘操作并不同步。)如果在此期间,另一进程试图读取该文件的这儿个字节,那么内核将自动从缓冲区高速缓存中提供这些数据,而不是从文件中(读取过期的内容)。

与此同理,对输入而言,内核从磁盘中读取数据并存储到内核缓冲区中。read()调用将从该缓冲区中读取数据,直至把缓冲区中的数据取完,这时,内核会将文件的下一段内容读入缓冲区高速缓存。(这里的描述有所简化。对于序列化的文件访问,内核通常会尝试执行预读,以确保在需要之前就将文件的下一数据块读入缓冲区高速缓存中。采用这一设计,意在使read()和write()操作更为快速,因为它们不需要等待(缓慢的)磁盘操作。同时,这一设计也极为高效,因为这减少了内核必须执行的磁盘传输次数。 Linux 内核对缓冲区高速缓存的大小没有固定上限。内核会分配尽可能多的缓冲区高速缓存页,而仅受限于两个因素:可用的物理内存总量,以及出于其他目的对物理内存的需求(例如,需要将正在运行进程的文本和数据页保留在物理内存中)。若可用内存不足,则内核会将一些修改过的缓冲区高速缓存页内容刷新到磁盘,并释放共系统重用。


stdio库的缓冲

当操作磁盘文件时,缓冲大块数据以减少系统调用,c语言函数库的io函数(fprintf()、fscanf()、fgets()、fputs()、fputc、fgetc())都是这么做的。因此,使用stdio库可以使编程者免于自行处理数据的缓冲。

int setvbuf(FILE *stream, char *buf, int mode, size_t size);
//成功返回0

stream为要指定缓冲类型的文件流,mode如下表

 mode描述 
 _IONBF 不缓冲,每个stdio库函数立即调用write()或read(),并且忽略buf,size参数。
 _IOLBF 行缓冲,遇到换行符就刷新缓冲数据,除非缓冲区提前满了。
 _IOFBF 全缓冲,缓冲区满了才刷新。

void setbuf(FILE *stream, char *buf);

//类似于setvbuf(fq, buf, (buf != NULL)?_IOFBF:_IONBF, BUFSIZ);

要么不缓冲,要么设置全缓冲,全缓冲的大小固定为BUFSIZ(8192)


void setbuffer(FILE *stream, char *buf, size_t size);

相较于setbuf这个setbuffer可以指定缓冲区的大小。

例子

//不缓冲
setvbuf(stderr, NULL, _IONBF, 0);
setbuf(stderr, NULL);

setvbuf(stderr, NULL, _IOLBF, 1024);  //系统分配缓冲内存1024

char *buf = malloc(1024);
setvbuf(stderr, buf, _IOLBF, 1024);   //行缓冲到buf

//全缓冲,缓冲区大小固定为8192
char *buf = malloc(BUFSIZ);
setbuf(stderr, buf, BUFSIZ);

//全缓冲,缓冲区大小自定义为1024
char *buf = malloc(1024);
setbuffer(stderr, buf, 1024);


手动刷新stdio缓冲区

无论当前使用何种缓冲区模式,在任何时候,都可以使用fflush()库函数强制将stdio输出流中的数据(即write())刷新到内核缓冲区中。注意:不一定写入到磁盘,只是从用户空间复制到内核空间。

#include <stdio.h>

int fflush(FILE *stream);

若stream为NULL,则将刷新所有的stdio缓冲区。

当关闭流时,自动刷新缓冲区。

当fflush()函数应用于输入流时,这将丢弃已缓冲的输入数据。当程序下一次尝试从流中读取数据时,将重新装满缓冲区。

在包括glibc库在内的许多C函数库实现中,若stdin和stdout指向终端,那么无论何时从stdin中读取输入时,都将隐式调用一次fflush(stdout)函数。这将刷新写入stdout的任何提示,但不包括换行符。比如(printf("name:"))。然而c99并未规定,所以为了可移植性,最好手动调用fflush(stdout)。

上面只是控制用户空间的缓冲区,下面来看看如何控制内核缓冲区。


控制文件io的内核缓冲

强制刷新内核缓冲到输出文件是可能的,有时候也很有必要。

int fsync(int fd);
//成功返回0

刷新文件数据,元数据(文件大小、修改时间、文件权限...)到磁盘。


int fdatasync(int fd);
//成功返回0

只刷新文件数据到磁盘,不包括元数据。


void sync(void);

刷新所有内核缓冲区(数据块、指针块、元数据等)到磁盘


使所有写入同步

fd = open(pathname, O_WRONLY|O_SYNC);

每次wirte操作都会自动将文件数据和元数据刷新到磁盘。谨慎使用,会影响性能。


直接IO(direct io)

linux允许应用程序在执行磁盘io时绕过高速缓冲区,从用户空间直接将数据传递到文件或磁盘设备。

在open的时候指定O_DIRECT标志。因为直接io涉及到磁盘的直接访问,必须遵守一些限制

1.用于传递数据的缓冲区,器内存边界必须对齐为块大小的整数倍(可以利用posix_memalign)。
2.数据传输的开始点,亦即文件和设备的偏移量,必须为块大小的整数倍。
3.待传递数据的长度必须为块大小的整数倍。

注意:

对大多数程序而言,使用直接io会大大降低性能,因为内核针对缓冲区高速缓存做了不少优化,其中包括按顺序预读取,在成簇磁盘块上执行io,允许访问同一文件的多个进程共享高速缓存的缓冲区。如果使用了直接io则都无法受益于这些优化举措。直接io只适用于特定的应用如数据库系统,因为它的高速缓存,io优化都自己做了,那么就无需内核消耗cpu时间和内存去完成相同的任务。


混合使用库函数和系统调用进行文件io

int fileno(FILE *stream);
//成功返回文件描述符,错误返回-1

传入文件流,返回文件描述符,随即可以使用read()、write()、dup之类的系统调用来处理。


FILE *fdopen(int fd, const char *mode);
//失败返回NULL

这个方法与fileno的功能刚好相反,给定一个文件描述符,返回一个文件流指针。mode跟fopen函数中的mode参数含义相同。r为读,w为写,a为追加。

一般的用途是比如一般socket返回的都是文件描述符,可以通过fdopen()转化成支持stdio库的操作的文件流

也可以混合操作write,fprintf单数要注意缓冲的问题,write直接缓冲到内核,而fprintf会先缓冲在用户空间再到内核空间。

例子

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int
main(int argc, char *argv[])
{
    char *url = "http://www.freecls.ocm ";
    printf("stdio库");
    
    write(STDOUT_FILENO, url, strlen(url));
    
    return 0;
}

/*
http://www.freecls.ocm stdio库
*/

为啥会出现上述结果呢,因为stdio库的printf()会先缓冲到用户空间内存,待缓冲满时才会复制到内核缓冲区,而write会直接缓冲到内核空间。

为了规避这一问题,我们可以使用fflush()或使用setvbuf()或setbuf()来禁用缓冲区,单这样会影响到应用的性能,因为每个输出操作都将调用一次系统调用write()。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int
main(int argc, char *argv[])
{
    char *url = "http://www.freecls.ocm ";
    printf("stdio库 ");
    fflush(stdout);
    
    write(STDOUT_FILENO, url, strlen(url));
    
    return 0;
}

/*
stdio库 http://www.freecls.ocm
*/


上一篇: linux系统和进程信息(/proc)
下一篇: linux c挂载(mount)和卸载(unmount)
作者邮箱: 203328517@qq.com