linux文件io缓冲

2018-05-22 12:19:33

出于速度和效率考虑,系统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文件io缓冲做了简单的介绍,如果有疑问可以给我留言。


备注

1.编译器版本gcc4.8,运行环境centos7 64位
2.原文地址http://www.freecls.com/a/2712/31

©著作权归作者所有
收藏
推荐阅读
  • err
    linux系统和进程信息(/proc)

    为了更方便的访问内核信息,许多现代的unix实现提供了/proc虚拟文件系统。其包含的文件和子目录并未存储在磁盘上,而是由内核进程访问此类信息动态的创建而成,都存储在内存...

  • err
    linux c时间

    日历时间#include &lt;sys/time.h&gt; struct timeval{ time_t tv_sec; /*自1970-01...

  • linux进程凭证(权限)

    每个进程都有一套用数字表示的用户id和组id,这些id决定了进程执行时具体的权限。实际用户id和实际组id这2个id决定了进程所属的用户和组。假设我们用root用户登录,那么我们在shell中创建的新...

  • linux用户和组

    linux上每个用户都拥有一个唯一的用户名和一个相对应的用户id(uid),用户可以隶属于一个或多个组。每个组也拥有一个唯一的组名和组id(gid)。用户和组主要是用来控制资源访问权限的。记录用户相关...

  • err
    linux动态内存分配

    进程可以通过增加堆的大小来分配内存,堆就是一段长度可变的连续的虚拟内存,开始于未初始化数据段末尾,随着内存的分配和释放增减。通常堆的当前内存边界称为program bre...

  • nginx模块 ngx_http_headers_module

    ngx_http_headers_module 模块是用来增加 Expires 和 Cache-control,或者是任意的响应头。Syntax: add_header name value [alw...

  • nginx模块 ngx_http_gunzip_module、ngx_http_gzip_module、ngx_http_gzip_static_module

    ngx_http_gunzip_module 模块将文件解压缩后并在响应头加上 "Content-Encoding: gzip" 返回给客户端。为了解决客户端不支持gzip压缩。编译的时候带上 --w...

  • nginx模块 ngx_http_flv_module、ngx_http_mp4_module

    ngx_http_flv_module模块提供了对 flv 视频的伪流支持。编译的时候带上 --with-http_flv_module。它会根据指定的 start 参数来指定跳过多少字节,并在返回数...

  • nginx模块 ngx_http_fastcgi_module

    ngx_http_fastcgi_module 模块使得nginx可以与 fastcgi 服务器通信。比如目前要使得 nginx 支持 php 就得使用 fastcgi技术,在服务器上装上 nginx...

  • nginx模块 ngx_http_autoindex_module

    ngx_http_autoindex_module 模块可以将uri以 / 结尾时,列出里面的文件和目录。Syntax: autoindex on | off; Default: autoindex ...

简介
天降大任于斯人也,必先苦其心志。