为什么需要标准IO缓冲?

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

缓冲区可由标准I/O例程自动冲洗,或者可以调用函数fflush(File *fp)冲洗一个流。如若fp是NULL,此函数将导致所有输出流被冲洗。

值得引起注意的是在UNIX环境 中,flush有两种意思:在标准I/O库方面,flush意味着将缓冲区中的内容写到磁盘上;在终端驱动程序方面flush表示丢弃已存储在缓冲区中的数据。

首先介绍一下UNIX里面关于标准IO的几种缓冲机制:

1、全缓冲 。全缓冲指的是系统在填满标准IO缓冲区之后才进行实际的IO操作;注意,对于驻留在磁盘上的文件来说通常是由标准IO库实施全缓冲。

标准IO库对流的缓冲不是在一开始就分配的,只有对流进行了输入或者输出才会实际的分配。一个流上执行第一次I/O操作时,相关标准I/O函数通常调用malloc获得需使用的缓冲区。

2、行缓冲 。在这种情况下,标准IO在输入和输出中遇到换行符时执行IO操作;注意,当流涉及终端的时候,通常使用的是行缓冲。

对于行缓冲有两个限制:

第一,因为标准io库用来收集每一行的缓冲区的长度是固定的,所以只要填满了缓冲区,那么即使还没有写一个换行符,也进行IO操作。

第二,任何时候只要通过标准的IO库要从(a)一个不带缓冲的流,或者(b)一个行缓冲的流(它从内核请求需要数据)得到输入数据,那么就会造成冲洗所有行缓冲输出流。其理由是,对于(b)所需的数据可能已经在缓冲区中,它并不要求一定从内核中读数据,对于(a),其需要从内核中获得数据。

3、无缓冲 。无缓冲指的是标准IO库不对字符进行缓冲存储;注意,标准出错流stderr通常是无缓冲的。 

ISO C要求下列缓冲特征:

当且仅当标准输入和标准输出并不涉及交互式设备使,他们才是全缓冲的。标准出错绝不会使全缓冲的。

但是,这并没有告诉我们如果标准输入和标准输出涉及交互式设备时,他们是不带缓冲的还是行缓冲的;以及标准出错时不带缓冲的还是行缓冲的。

很多系统默认使用下列类型的缓冲:标准出错是不带缓冲的。如若是涉及终端设备的其他流,则他们是行缓冲的;否则是全缓冲的。


#include <stdio.h>

void setbuf(FILE* restrict fp, char* restrict buf);

int setvbuf(FILE* restrict fp, char* restrict buf, int mode, size_t size); //如果成功返回0,出错则返回非0.  


可以使用setbuf函数打开或关闭缓冲机制,为了带缓冲进行IO,参数buf必须制定一个长度为BUFSIZ的缓冲区(这就是为什么没有在setbuf函数的参数中指定buf的长度),通常在此之后该流就是全缓冲的,但是如果该流与一个终端相关,那么某些系统也可以将其设置为行缓冲。为了关闭缓冲,将buf设置为NULL。

使用setvbuf,可以精确地指定所需的缓冲类型。mode的取值及其代表的含义如下:

  _IOFBF 全部缓冲
  _IOLBF 行缓冲
  _IONBF 不缓冲

注意:

如果指定一个不带缓冲的流,则忽略buf和size参数。

如果指定全缓冲和行缓冲,则buf和size可选择地指定一个缓冲区及其长度。

如果该流是带缓冲的,而buff是NULL,则标准IO库将自动地为该流分配适当长度的缓冲区(长度为BUFSIZ指定的值)

某日一朋友写了一个HELLO WORLD代码,出不来结果,代码如下:


#include <stdio.h>

#include<iostream>

int main(int argc, char **argv)

{

    printf("hello world!");

 system(“pause”);

    return 0;

}


注意到,在代码中printf语句打印的字符串最后没有带换行符,而且最后调用了_Exit函数,这导致了在终端屏幕上显示不出来字符串"hello world!"。

其次介绍一下几个退出函数:

1、exit ()。调用exit函数之后,它首先会执行一系列的清理处理,包括调用执行各终止处理程序,关闭所有标准IO流等,然后进入内核。

2、_exit ()。与exit不同的是,它不进行清理工作而直接进入内核。此函数由POSIX.1说明,放在unistd.h里面。

3、_Exit ()。同样,它也不进行清理工作而直接进入内核。此函数跟exit一样由ISO C说明,放在stdlib.h里面。

现在回过头来看上面的那段代码,很容易发现,由于printf函数是行缓冲的(因为它要往终端输出数据),而且要打印的字符串不带换行符,因此在它没有遇到换行符或者没有填满缓冲区之前不会进行实际的IO操作,而紧接下来的_Exit函数又立即进入内核没有处理IO缓冲区,所以我们在终端上看不到hello world语句。

我们可以有很多方法修正这段代码。最简单的莫过于增加一个换行符:


#include <stdio.h>

int main(int argc, char **argv)

{

    printf("hello world!/n");

    _Exit(0);

}


此时行缓冲遇到换行符/n,执行实际IO操作。

其次,我们可以调用exit函数,让它帮我们进行相应的IO处理:


#include <stdio.h>

int main(int argc, char **argv)

{

    printf("hello world!");

    exit(0);

}


exit函数在进入内核之前,对存储在缓冲区内的数据进行冲洗,然后关闭IO流。

 或者,我们可以改变标准输出流的默认缓冲模式:


#include <stdio.h>

int main(int argc, char **argv)

{

    setvbuf(stdout, NULL, _IONBF, 0);

    printf("hello world!");

    _Exit(0);

}


此时,由于调用了setvbuf函数,把标准输出流默认的行缓冲变成了无缓冲(具体请查阅setvbuf函数实现机制),因此调用printf时立即输出。

 当然,我们还可以调用fclose函数来达到此目的: 


#include <stdio.h>

int main(int argc, char **argv)

{

    printf("hello world!");

    fclose(stdout) ;

    _Exit(0);

}


实际上, fclose函数隐含包含了一次fflush操作,把缓冲区内的数据冲洗到终端。

 当然,我们还可以直接调用fflush函数来达到此目的:


#include <stdio.h>

int main(int argc, char **argv)

{

    printf("hello world!");

    fflush(stdout);

    _Exit(0);

}


fflush不指定fp时,会冲洗所有输出流。

看个小例子
源程序:


#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int globa = 4;
int main (void )
{
  pid_t pid;
  int vari = 5;
  printf ("before fork " );
  if ((pid = fork()) < 0)

  {
    printf ("fork error ");
    exit (0);
  }

  else if (pid == 0)

  {
    globa++ ;
    vari--;
    printf("Child changed ");

    //printf("globa = %d vari = %d ",globa,vari);  ,没有这一句?
  }

  else
    printf("Parent did not changde ");
  printf("globa = %d vari = %d ",globa,vari);
  exit(0);
}


执行结果:
输出到标准输出
[root@happy bin]# ./simplefork
before fork
Child changed
globa = 5 vari = 4
Parent did not changde
globa = 4 vari = 5

重定向到文件时before fork输出两边
[root@happy bin]# ./simplefork>temp
[root@happy bin]# cat temp
before fork
Child changed
globa = 5 vari = 4
before fork
Parent did not changde
globa = 4 vari = 5

分析直接运行程序时标准输出是行缓冲的,很快被新的一行冲掉。

而重定向后,标准输出是全缓冲的。当调用fork时before fork这行仍保存在缓冲中,并随着数据段复制到子进程缓冲中。

这样,这一行就分别进入父子进程的输出缓冲中,余下的输出就接在了这一行的后面。

 1.缓冲文件系统

缓冲文件系统的特点是:在内存开辟一个“缓冲区”,为程序中的每一个文件使用,当执行读文件的操作时,从磁盘文件将数据先读入内存“缓冲区”,装满后再从内存“缓冲区”依此读入接收的变量。执行写文件的操作时,先将数据写入内存“缓冲区”,待内存“缓冲区”装满后再写入文件。由此可以看出,内存 “缓冲区”的大小,影响着实际操作外存的次数,内存“缓冲区”越大,则操作外存的次数就少,执行速度就快、效率高。一般来说,文件“缓冲区”的大小随机器而定。
fopen, fclose, fread, fwrite, fgetc, fgets, fputc, fputs, freopen, fseek, ftell, rewind等是带缓冲的。

2.非缓冲文件系统
缓冲文件系统是借助文件结构体指针来对文件进行管理,通过文件指针来对文件进行访问,既可以读写字符、字符串、格式化数据,也可以读写二进制数据。非缓冲文件系统依赖于操作系统,通过操作系统的功能对文件进行读写,是系统级的输入输出,它不设文件结构体指针,只能读写二进制文件,但效率高、速度快,由于ANSI标准不再包括非缓冲文件系统,因此建议大家最好不要选择它。本书只作简单介绍。
open, close, read, write, getc, getchar, putc, putchar 等是不带缓冲的。

前者带f的属于高级IO,后者是低级IO。


前者有缓冲,后者无缓冲。
高级IO是在低级IO的基础上扩充而来的,在大多数情况下,使用高级IO。

不带缓存的read和write是相对于fread/fwrite等流函数来说明的,因为fread和fwrite是用户函数,所以他们会在用户层 进行一次数据的缓存,而read/write是系统调用(2)所以他们在用户层是没有缓存的,所以称read和write是无缓存的IO,其实对于内核来 说还是进行了缓存,不过用户层看不到罢了。

现在假设内核所设的缓存是100个字节,如果你使用write,且buff的size为10,当你要把9个同样的buff写到文件时,你需要调用9次write,也就是9次系统调用,此时也并没有写到硬盘,如果想立即写到硬盘,调用fsync,可以进行实际的I/O操作。


open 是系统调用 返回的是文件句柄,文件的句柄是文件在文件描述副表里的索引,
fopen是C的库函数,返回的是一个指向文件结构的指针。

文件描述符是linux下的一个概念,linux下的一切设备都是以文件的形式操作.如网络套接字、硬件设备等。当然包括操作文件。
fopen是标准c函数。返回文件流而不是linux下文件句柄。

设备文件不可以当成流式文件来用,只能用open。
fopen是用来操纵正规文件的,并且设有缓冲的,跟open还是有一些区别。

一般用fopen打开普通文件,用open打开设备文件。

fopen是标准c里的,而open是linux的系统调用,他们的层次不同。
fopen可移植,open不能。

  1,fread是带缓冲的,read不带缓冲. 
  2,fopen是标准c里定义的,open是POSIX中定义的. 
  3,fread可以读一个结构.read在linux/unix中读二进制与普通文件没有区别. 
  4,fopen不能指定要创建文件的权限.open可以指定权限. 
  5,fopen返回指针,open返回文件描述符(整数). 
  6,linux/unix中任何设备都是文件,都可以用open   ,read.

这里使用两个对应的函数进行比较:
ssize_t write(int filedes, const void *buff, size_t nbytes)
size_t fwrite(const void *ptr, size_t size, size_t nobj, FILE *fp)

上面的buff和ptr都是指应用程序自己使用的buffer,实际上当需要对文件进行写操作时,都会先写到内核所设的缓冲存储器。如果该缓存未满,则并不将其排入输出队列,直到缓存写满或者内核再次需要重新使用此缓存时才将其排入磁盘I/O输入队列,再进行实际的I/O操作,也就是此时才把数据真正写到磁盘,这种技术叫延迟写。

如果我们直接用非缓存I/O对内核的缓冲区进行读写,会产生许多管理不善而造成的麻烦(如一次性写入过多,或多次系统调用导致的效率低下)。

标准(带缓存的)I/O为我们解决了这些问题,它处理很多细节,如缓冲区分配,以优化长度执行I/O等,更便于我们使用。

由于标准(带缓存的)I/O在系统调用的上一层多加了一个缓冲区,也因此引入了流的概念,在UNIX/Linux下表示为FILE*(并不限于UNIX/Linux,ANSI C都有FILE的概念),FILE实际上包含了为管理流所需要的所有信息:实际I/O的文件描述符,指向流缓存的指针(标准I/O缓存,由malloc分配,又称为用户态进程空间的缓存,区别于内核所设的缓存),缓存长度,当前在缓存中的字节数,出错标志等。

因此可知,不带缓存的I/O对文件描述符操作,带缓存的标准I/O是针对流的

标准I/O对每个I/O流自动进行缓存管理(标准I/O函数通常调用malloc来分配缓存)。它提供了三种类型的缓存:
      1) 全缓存。当填满标准I/O缓存后才执行I/O操作。磁盘上的文件通常是全缓存的。
      2) 行缓存。当输入输出遇到新行符或缓存满时,才由标准I/O库执行实际I/O操作。stdin、stdout通常是行缓存的。
      3) 无缓存。相当于read、write了。stderr通常是无缓存的,因为它必须尽快输出。

一般而言,由系统选择缓存的长度,并自动分配。标准I/O库在关闭流的时候自动释放缓存。另外,也可以使用函数fflush()将流所有未写的数据送入(刷新)到内核(内核缓冲区),fsync()将所有内核缓冲区的数据写到文件(磁盘)

在标准I/O库中也有引入缓存管理而带来的缺点--效率问题。例如当使用每次一行函数fgets和fputs时,通常需要复制两次数据:一次是在内核和标准I/O缓存之间(当调用read和write时),第二次是在标准I/O缓存(通常系统分配和管理)和用户程序中的行缓存(fgets的参数就需要一个用户行缓存指针)之间。

不管上面讲的到底懂没懂,记住一点:

    使用标准I / O例程的一个优点是无需考虑缓存及最佳I / O长度的选择,并且它并不比直接调用read、write慢多少。

带缓存的文件操作是标准C 库的实现,第一次调用带缓存的文件操作函数时标准库会自动分配内存并且读出一段固定大小的内容存储在缓存中。所以以后每次的读写操作并不是针对硬盘上的文件直接进行的,而是针对内存中的缓存的。何时从硬盘中读取文件或者向硬盘中写入文件有标准库的机制控制。不带缓存的文件操作通常都是系统提供的系统调用,更加低级,直接从硬盘中读取和写入文件,由于IO瓶颈的原因,速度并不如意,而且原子操作需要程序员自己保证,但使用得当的话效率并不差。另外标准库中的带缓存文件IO 是调用系统提供的不带缓存IO实现的。

“术语不带缓冲指的是每个read和write都调用嗯内核中的一个系统调用。所有的磁盘I/O都要经过内核的块缓冲(也称内核的缓冲区高速缓存),唯一例外的是对原始磁盘设备的I/O。既然read或write的数据都要被内核缓冲,那么术语“不带缓冲的I/O“指的是在用户的进程中对这两个函数不会自动缓冲,每次read或write就要进行一次系统调用。“--------摘自<unix环境编程>
-----------------------------------------------------------------------------------------
Linux标准IO库缓存策略介绍
标准IO库操作是围绕着流来进行的,当我们通过fopen标准IO库函数打开一个文件,我们就使一个文件和一个IO流相关联。在这里我们把IO流和文件指针FILE*等同起来,因为所有针对IO流的操作都是通过FILE*指针来实现的。
    我们知道引入标准IO库的目的是为了提高IO的效率,避免频繁的进行read/write系统调用,而系统调用会消耗较多的资源。因此标准IO库引入了IO缓存,通过累积一定量的IO数据后,然后集中写入到实际的文件中来减少系统调用,从而提高IO效率。标准IO库会自动管理内部的缓存,不需要程序员介入。然而,也正是因为我们看不到标准IO库的缓存,有时候会给我们带来一定的迷惑性。这里介绍下标准IO库的缓存策略。
    一。标准I/O的缓存--标准输出为例:(这里都是指缺省情况下)
    1)当STDOUT连接到终端设备时,那么它就是行缓存的,也就是标准IO库没看到一个新行符 时就刷新一次缓存(即执行一次实际的输出操作)。这一特性可以通过如下测试代码来验证
   int main()
  {
      printf("This Line Should be Cached...");
      sleep(3);    //这时候在终端上是看不到任何输出
      printf(" This Line Should be Cached Again");  //这时候可以看到第一个printf的输出,因为被换行符刷新了
      sleep(3);  //这时候也只能看到一行输出,而看不到第二个printf输出的
      printf("This Line Should Not be Cached Again "); //这时候可以看到第二个和第三个printf的输出,因为被结尾的 刷新
      sleep(3);
      getchar();
  }
  2)当STDOUT被重定向到一个具体文件时,那么标准输出是全缓存的,也就是说只有当输出缓存被塞满或者调用fflush或fclose时才会执行实际的写入操作,这里就不给出具体例子,可以通过freopen将STDOUT重定向到一个具体文件来进行测试。
二。标准出错STDERR:为了尽快的看到出错信息,标准出错是不带任何缓存的。
原文地址:https://www.cnblogs.com/-lhy-to-s/p/10656601.html