前言

  在最近看了APUE的标准IO部分之后感觉对标准IO的缓存太模糊,没有搞明白,APUE中关于缓存的部分一笔带过,没有深究缓存的实现原理,这样一本被吹上天的书为什么不讲透彻呢?今天早上爬起来赶紧找了几篇文章看看,直到发现了这篇博客:http://blog.sina.com.cn/s/blog_6592a07a0101gar7.html。讲的很不错。

一、IO缓存

  系统调用:只操作系统提供给用户程序调用的一组接口-------获得内核提供的服务。

  在实际中程序员使用的通常不是系统调用,而是用户编程接口API,也称为系统调用编程接口。它是遵循Posix标准(Portable operation system interface),API函数可能要一个或者几个系统调用才能完成函数功能,此函数通过c库(libc)实现,如read,open。
  fsync是把内核缓冲刷到磁盘上。
  fflush:是把C库中的缓冲调用write函数写到磁盘[其实是写到内核的缓冲区]。
 
  linux对IO文件的操作分为:
  • 不带缓存:open  read。posix标准,在用户空间没有缓冲,在内核空间还是进行了缓存的。数据-----内核缓存区----磁盘。假设内核缓存区长度为100字节,你调用ssize_t write (int fd,const void * buf,size_t count);写操作时,设每次写入count=10字节,那么你要调用10次这个函数才能把这个缓存区写满,没写满时数据还是在内核缓冲区中,并没有写入到磁盘中,内核缓存区满了之后或者执行了fsync(强制写入硬盘)之后,才进行实际的IO操作,吧数据写入磁盘上。
  • 带缓存区:fopen fwrite fget 等,是c标准库中定义的。数据-----流缓存区-----内核缓存区----磁盘。假设流缓存区长度为50字节,内核缓存区100字节,我们用标准c库函数fwrite()将数据写入到这个流缓存中,每次写10字节,需要写5次流缓存区满后调用write()(或调用fflush()),将数据写到内核缓存区,直到内核缓存区满了之后或者执行了fsync(强制写入硬盘)之后,才进行实际的IO操作,吧数据写入磁盘上。标准IO操作fwrite()最后还是要掉用无缓存IO操作write。

  以fgetc / fputc 为例,当用户程序第一次调用fgetc 读一个字节时,fgetc 函数可能通过系统调用 进入内核读1K字节到I/O缓冲区中,然后返回I/O缓冲区中的第一个字节给用户,把读写位置指 向I/O缓冲区中的第二个字符,以后用户再调fgetc ,就直接从I/O缓冲区中读取,而不需要进内核 了,当用户把这1K字节都读完之后,再次调用fgetc 时,fgetc 函数会再次进入内核读1K字节 到I/O缓冲区中。在这个场景中用户程序、C标准库和内核之间的关系就像在“Memory Hierarchy”中 CPU、Cache和内存之间的关系一样,C标准库之所以会从内核预读一些数据放 在I/O缓冲区中,是希望用户程序随后要用到这些数据,C标准库的I/O缓冲区也在用户空间,直接 从用户空间读取数据比进内核读数据要快得多。另一方面,用户程序调用fputc 通常只是写到I/O缓 冲区中,这样fputc 函数可以很快地返回,如果I/O缓冲区写满了,fputc 就通过系统调用把I/O缓冲 区中的数据传给内核,内核最终把数据写回磁盘或设备。有时候用户程序希望把I/O缓冲区中的数据立刻 传给内核,让内核写回设备或磁盘,这称为Flush操作,对应的库函数是fflush,fclose函数在关闭文件 之前也会做Flush操作。

  虽然write 系统调用位于C标准库I/O缓冲区的底 层,被称为Unbuffered I/O函数,但在write 的底层也可以分配一个内核I/O缓冲区,所以write 也不一定是直接写到文件的,也 可能写到内核I/O缓冲区中,可以使用fsync函数同步至磁盘文件,至于究竟写到了文件中还是内核缓冲区中对于进程来说是没有差别 的,如果进程A和进程B打开同一文件,进程A写到内核I/O缓冲区中的数据从进程B也能读到,因为内核空间是进程共享的, 而c标准库的I/O缓冲区则不具有这一特性,因为进程的用户空间是完全独立的.

  下面是一个利用buffered I/O读取数据的例子:

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h> int main(void)
{
char buf[];
FILE *myfile = stdin;
fgets(buf, , myfile);
fputs(buf, myfile); return ;
}

  buffered I/O中的"buffer"到底是指什么呢?这个buffer在什么地方呢?FILE是什么呢?它的空间是怎么分配的呢  要弄清楚这些问题,就要看看FILE是如何定义和运作的了.(特别说明,在平时写程序时,不用也不要关心FILE是如何定义和运作的,最好不要直接操作它,这里使用它,只是为了说明buffered IO)下面的这个是glibc给出的FILE的定义,它是实现相关的,别的平台定义方式不同.

struct _IO_FILE {
int _flags;
#define _IO_file_flags _flags char* _IO_read_ptr;
char* _IO_read_end;
char* _IO_read_base;
char* _IO_write_base;
char* _IO_write_ptr;
char* _IO_write_end;
char* _IO_buf_base;
char* _IO_buf_end; char *_IO_save_base;
char *_IO_backup_base;
char *_IO_save_end; struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno;
};

  上面的定义中有三组重要的字段:

.
char* _IO_read_ptr;
char* _IO_read_end;
char* _IO_read_base;
.
char* _IO_write_base;
char* _IO_write_ptr;
char* _IO_write_end;
.
char* _IO_buf_base;
char* _IO_buf_end;
  其中,
  _IO_read_base 指向"读缓冲区"
  _IO_read_end  指向"读缓冲区"的末尾
  _IO_read_end - _IO_read_base "读缓冲区"的长度

  _IO_write_base 指向"写缓冲区"
  _IO_write_end 指向"写缓冲区"的末尾
  _IO_write_end - _IO_write_base "写缓冲区"的长度

  _IO_buf_base  指向"缓冲区"
  _IO_buf_end   指向"缓冲区"的末尾
  _IO_buf_end - _IO_buf_base "缓冲区"的长度

  上面的定义貌似给出了3个缓冲区,实际上上面的_IO_read_base,_IO_write_base, _IO_buf_base都指向了同一个缓冲区.这个缓冲区跟上面程序中的char buf[5];没有任何关系.他们在第一次buffered I/O操作时由库函数自动申请空间,最后由相应库函数负责释放.(再次声明,这里只是glibc的实现,别的实现可能会不同,后面就不再强调了)

  请看下面的程序(这里给的是stdin,行缓冲的例子):

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

  可以看到,在读操作之前,myfile的缓冲区是没有被分配的,在一次读之后,myfile的缓冲区才被分配.这个缓冲区既不是内核中的缓冲区,也不是用户分配的缓冲区,而是有用户进程空间中的由buffered I/O系统负责维护的缓冲区.(当然,用户可以可以维护该缓冲区,这里不做讨论了)

  上面的例子只是说明了buffered I/O缓冲区的存在,下面从全缓冲,行缓冲和无缓冲3个方面看一下buffered I/O是如何工作的.

二、 全缓冲

  下面是APUE上的原话:全缓冲"在填满标准I/O缓冲区后才进行实际的I/O操作.对于驻留在磁盘上的文件通常是由标准I/O库实施全缓冲的"书中这里"实际的I/O操作"实际上容易引起误导,这里并不是读写磁盘,而应该是进行read或write的系统调用,下面两个例子会说明这个问题:

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h> int main(void)
{
char buf[];
char *cur;
FILE *myfile;
myfile = fopen("bbb.txt", "r");
printf("before reading, myfile->_IO_read_ptr: %d/n", myfile->_IO_read_ptr - myfile->_IO_read_base);
fgets(buf, , myfile); //仅仅读4个字符
cur = myfile->_IO_read_base;
while (cur <</span> myfile->_IO_read_end) //实际上读满了这个缓冲区
{
printf("%c",*cur);
cur++;
}
printf("/nafter reading, myfile->_IO_read_ptr: %d/n", myfile->_IO_read_ptr - myfile->_IO_read_base);
return ;
}

  上面提到的bbb.txt文件的内容是由很多行的"123456789"组成上例中,fgets(buf, 5, myfile); 仅仅读4个字符,但是,缓冲区已被写满,但是_IO_read_ptr却向前移动了5位,下次再次调用读操作时,只要要读的位数不超过myfile->_IO_read_end - myfile->_IO_read_ptr那么就不需要再次调用系统调用read,只要将数据从myfile的缓冲区拷贝到buf即可(从myfile->_IO_read_ptr开始拷贝)

  全缓冲读的时候,_IO_read_base始终指向缓冲区的开始,_IO_read_end始终指向已从内核读入缓冲区的字符的下一个(对全缓冲来说,buffered I/O读每次都试图都将缓冲区读满),IO_read_ptr始终指向缓冲区中已被用户读走的字符的下一个(_IO_read_end < (_IO_buf_base-_IO_buf_end)) && (_IO_read_ptr == _IO_read_end)时则已经到达文件末尾其中_IO_buf_base-_IO_buf_end是缓冲区的长度
  一般大体的工作情景为:第一次fgets(或其他的)时,标准I/O会调用read将缓冲区充满,下一次fgets不调用read而是直接从该缓冲区中拷贝数据,直到缓冲区的中剩余的数据不够时,再次调用read.在这个过程中,_IO_read_ptr就是用来记录缓冲区中哪些数据是已读的,
哪些数据是未读的.
  

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h> int main(void)
{
char buf[]={};
int i;
FILE *myfile;
myfile = fopen("aaa.txt", "r+");
i= ;
while (i<</span>)
{
fwrite(buf+i, , , myfile);
i +=;
//注释掉这句则可以写入aaa.txt
myfile->_IO_write_ptr = myfile->_IO_write_base;
printf("%p write buffer base/n", myfile->_IO_write_base);
printf("%p buf buffer base /n", myfile->_IO_buf_base);
printf("%p read buffer base /n", myfile->_IO_read_base);
printf("%p write buffer ptr /n", myfile->_IO_write_ptr);
printf("/n");
}
return ;
}

  上面这个是关于全缓冲写的例子.全缓冲时,只有当标准I/O自动flush(比如当缓冲区已满时)或者手工调用fflush时,标准I/O才会调用一次write系统调用.例子中,fwrite(buf+i, 1, 512, myfile);这一句只是将buf+i接下来的512个字节写入缓冲区,由于缓冲区未满,标准I/O并未调用write.此时,myfile->_IO_write_ptr = myfile->_IO_write_base;会导致标准I/O认为没有数据写入缓冲区,所以永远不会调用write,这样aaa.txt文件得不到写入.注释掉myfile->_IO_write_ptr = myfile->_IO_write_base;前后,看看效果

  全缓冲写的时候:_IO_write_base始终指向缓冲区的开始,_IO_write_end全缓冲的时候,始终指向缓冲区的最后一个字符的下一个(对全缓冲来说,buffered I/O写总是试图在缓冲区写满之后,再系统调用write),_IO_write_ptr始终指向缓冲区中已被用户写入的字符的下一个,flush的时候,将_IO_write_base和_IO_write_ptr之间的字符通过系统调用write写入内核

三、 行缓冲

  下面是APUE上的原话:行缓冲"当输入输出中遇到换行符时,标准I/O库执行I/O操作. "书中这里"执行O操作"也容易引起误导,这里不是读写磁盘,而应该是进行read或write的系统调用
  下面两个例子会说明这个问题
  第一个例子可以用来说明下面这篇帖子的问题
  http://bbs.chinaunix.net/viewthread.php?tid=954547
  

#include <stdlib.h>
#include <stdio.h> int main(void)
{
char buf[];
char buf2[]; fgets(buf, , stdin); //第一次输入时,超过5个字符 puts(stdin->_IO_read_ptr);//本句说明整行会被一次全部读入缓冲区, //而非仅仅上面需要的个字符
stdin->_IO_read_ptr = stdin->_IO_read_end; //标准I/O会认为缓冲区已空,再次调用read
//注释掉,再看看效果
printf("/n");
puts(buf); fgets(buf2, , stdin);
puts(buf2); return ;
}

  上例中, fgets(buf, 5, stdin); 仅仅需要4个字符,但是,输入行中的其他数据也被写入缓冲区,但是_IO_read_ptr向前移动了5位,下次再次调用fgets操作时,就不需要再次调用系统调用read,只要将数据从stdin的缓冲区拷贝到buf2即可(从stdin->_IO_read_ptr开始拷贝)stdin->_IO_read_ptr = stdin->_IO_read_end;会导致标准I/O会认为缓冲区已空,再次fgets则需要再次调用read.比较一下将该句注释掉前后的效果


  
行缓冲读的时候,
  _IO_read_base始终指向缓冲区的开始
  _IO_read_end始终指向已从内核读入缓冲区的字符的下一个
  _IO_read_ptr始终指向缓冲区中已被用户读走的字符的下一个
  (_IO_read_end < (_IO_buf_base-_IO_buf_end)) && (_IO_read_ptr == _IO_read_end)时则已经到达文件末尾
  其中_IO_buf_base-_IO_buf_end是缓冲区的长度
  

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h> char buf[]={'','', '', '', ''}; //最后一个不要是/n,是/n的话,标准I/O会自动flush的
//这是行缓冲跟全缓冲的重要区别 void writeLog(FILE *ftmp)
{
fprintf(ftmp, "%p write buffer base/n", stdout->_IO_write_base);
fprintf(ftmp, "%p buf buffer base /n", stdout->_IO_buf_base);
fprintf(ftmp, "%p read buffer base /n", stdout->_IO_read_base);
fprintf(ftmp, "%p write buffer ptr /n", stdout->_IO_write_ptr);
fprintf(ftmp, "/n");
} int main(void)
{
int i;
FILE *ftmp;
ftmp = fopen("ccc.txt", "w");
i= ;
while (i<</span>)
{
fwrite(buf, , , stdout);
i++;
*stdout->_IO_write_ptr++ = '/n';//可以单独把这句打开,看看效果
//getchar();//getchar()会标准I/O将缓冲区输出
//打开下面的注释,你就会发现屏幕上什么输出也没有
//stdout->_IO_write_ptr = stdout->_IO_write_base;
writeLog(ftmp); //这个只是为了查看缓冲区指针的变化
}
return ;
}

  这个例子将将FILE结构中指针的变化写入的文件ccc.txt,

  运行后可以有兴趣的话,可以看看.

  上面这个是关于行缓冲写的例子.stdout->_IO_write_ptr = stdout->_IO_write_base;会使得标准I/O认为缓冲区是空的,从而没有任何输出.可以将上面程序中的注释分别去掉,看看运行结果

  行缓冲时,下面3个条件之一会导致缓冲区立即被flush
  1. 缓冲区已满
  2. 遇到一个换行符;比如将上面例子中buf[4]改为'/n'时
  3. 再次要求从内核中得到数据时;比如上面的程序加上getchar()会导致马上输出

  行缓冲写的时候:
  _IO_write_base始终指向缓冲区的开始
  _IO_write_end始终指向缓冲区的开始
  _IO_write_ptr始终指向缓冲区中已被用户写入的字符的下一个

  flush的时候,将_IO_write_base和_IO_write_ptr之间的字符通过系统调用write写入内核

四、无缓冲

  无缓冲时,标准I/O不对字符进行缓冲存储.典型代表是stderr。这里的无缓冲,并不是指缓冲区大小为0,其实,还是有缓冲的,大小为1

#include <</span>stdlib.h>
#include <</span>stdio.h>
#include <</span>sys/types.h>
#include <</span>sys/stat.h>
#include <</span>fcntl.h> int main(void)
{
fputs("stderr", stderr);
printf("%d/n", stderr->_IO_buf_end - stderr->_IO_buf_base); return ;
}

  对无缓冲的流的每次读写操作都会引起系统调用


五、 feof的问题

  这里从缓冲区的角度去考察一下.对于一个空文件,为什么要先读一下,才能用feof判断出该文件到了结尾了呢?

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h> int main(void)
{
char buf[];
char buf2[]; fgets(buf, sizeof(buf), stdin);//输入要于4个,少于13个字符才能看出效果
puts(buf); //交替注释下面两行
//stdin->_IO_read_end = stdin->_IO_read_ptr+1; stdin->_IO_read_end = stdin->_IO_read_ptr + sizeof(buf2)-; fgets(buf2, sizeof(buf2), stdin);
puts(buf2);
if (feof(stdin))
printf("input end/n");
return ;
}
  运行上面的程序,输入多于4个,少于13个字符,并且以连按两次ctrl+d为结束(不要按回车),从上面的例子,可以看出,每当满足(_IO_read_end < (_IO_buf_base-_IO_buf_end)) && (_IO_read_ptr == _IO_read_end)时,标准I/O则认为已经到达文件末尾,feof(stdin)才会被设置其中_IO_buf_base-_IO_buf_end是缓冲区的长度。

  也就是说,标准I/O是通过它的缓冲区来判断流是否要结束了的.这就解释了为什么即使是一个空文件,标准I/O也需要读一次,才能使用feof判断释放为空。

深究标准IO的缓存的更多相关文章

  1. 带标准IO带缓存区和非标准IO 遇到fork是的情况分析

    废话不多说 直接代码 #include<stdio.h> #include<sys/types.h> #include<unistd.h> #include< ...

  2. [APUE]标准IO库(上)

    一.流和FILE对象 系统IO都是针对文件描述符,当打开一个文件时,即返回一个文件描述符,然后用该文件描述符来进行下面的操作,而对于标准IO库,它们的操作则是围绕流(stream)进行的. 当打开一个 ...

  3. 为什么需要标准IO缓冲?

    (转)标准I/O缓冲:全缓冲.行缓冲.无缓冲 标准I/O库提供缓冲的目的是尽可能地减少使用read和write调用的次数.它也对每个I/O流自动地进行缓冲管理,从而避免了应用程序需要考虑这一点所带来的 ...

  4. [APUE]标准IO库(下)

    一.标准IO的效率 对比以下四个程序的用户CPU.系统CPU与时钟时间对比 程序1:系统IO 程序2:标准IO getc版本 程序3:标准IO fgets版本 结果: [注:该表截取自APUE,上表中 ...

  5. 标准io与文件io

    A: 代码重复: 语句块1: while(判断) { 语句块2: 语句块1: } 上面可以改写为: while(1) { 语句块1: if(判断) break: 语句块2: } B: 标准IO和文件I ...

  6. linux标准IO缓冲(apue)

    为什么需要标准IO缓冲? LINUX用缓冲的地方遍地可见,不管是硬件.内核还是应用程序,内核里有页高速缓冲,内存高速缓冲,硬件更不用说的L1,L2 cache,应用程序更是多的数不清,基本写的好的软件 ...

  7. 【linux草鞋应用编程系列】_1_ 开篇_系统调用IO接口与标准IO接口

    最近学习linux系统下的应用编程,参考书籍是那本称为神书的<Unix环境高级编程>,个人感觉神书不是写给草鞋看的,而是 写给大神看的,如果没有一定的基础那么看这本书可能会感到有些头重脚轻 ...

  8. 文件IO函数和标准IO库的区别

    摘自 http://blog.chinaunix.net/uid-26565142-id-3051729.html 1,文件IO函数,在Unix中,有如下5个:open,read,write,lsee ...

  9. linux标准io的copy

    ---恢复内容开始--- 1.linux标准io的copy #include<stdio.h> int main(int argc,char **argv) { if(argc<3) ...

随机推荐

  1. 为C# as 类型转换及Assembly.LoadFrom埋坑!

    背景: 不久前,我发布了一个调试工具:发布:.NET开发人员必备的可视化调试工具(你值的拥有) 效果是这样的: 之后,有小部分用户反映,工具用不了(没反应或有异常)~~~ 然后,建议小部分用户换个电脑 ...

  2. setAttribute()

    ●节点分为不同的类型:元素节点.属性节点和文本节点等.   ●getElementById()方法将返回一个对象,该对象对应着文档里的一个特定的元素节点.   ●getElementsByTagNam ...

  3. C语言 · 整数平均值

    编写函数,求包含n个元素的整数数组中元素的平均值.要求在函数内部使用指针操纵数组元素,其中n个整数从键盘输入,输出为其平均值. 样例输入: (输入格式说明:5为输入数据的个数,3 4 0 0 2 是以 ...

  4. Linux安装LAMP开发环境及配置文件管理

    Linux主要分为两大系发行版,分别是RedHat和Debian,lamp环境的安装和配置也会有所不同,所以分别以CentOS 7.1和Ubuntu 14.04做为主机(L) Linux下安装软件,最 ...

  5. js复杂对象和简单对象的简单转化

    var course = { teacher :{ teacherId:001, teacherName:"王" }, course : { courseId : 120, cou ...

  6. FFmpeg 中AVPacket的使用

    AVPacket保存的是解码前的数据,也就是压缩后的数据.该结构本身不直接包含数据,其有一个指向数据域的指针,FFmpeg中很多的数据结构都使用这种方法来管理数据. AVPacket的使用通常离不开下 ...

  7. PHP设计模式(八)桥接模式(Bridge For PHP)

    一.概述 桥接模式:将两个原本不相关的类结合在一起,然后利用两个类中的方法和属性,输出一份新的结果. 二.案例 1.模拟毛笔(转) 需求:现在需要准备三种粗细(大中小),并且有五种颜色的比 如果使用蜡 ...

  8. 5.2 Array类型的方法汇总

    所有对象都具有toString(),toLocaleString(),valueOf()方法. 1.数组转化为字符串 toString(),toLocaleString() ,数组调用这些方法,则返回 ...

  9. XAMARIN.ANDROID SIGNALR 实时消息接收发送示例

    SignalR 是一个开发实时 Web 应用的 .NET 类库,使用 SignalR 可以很容易的构建基于 ASP.NET 的实时 Web 应用.SignalR 支持多种服务器和客户端,可以 Host ...

  10. React Native 之 Text的使用

    前言 学习本系列内容需要具备一定 HTML 开发基础,没有基础的朋友可以先转至 HTML快速入门(一) 学习 本人接触 React Native 时间并不是特别长,所以对其中的内容和性质了解可能会有所 ...