为什么需要标准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\n" );
if ((pid = fork()) < 0)
{
printf ("fork error\n");
exit (0);
}
else if (pid == 0)
{
globa++ ;
vari--;
printf("Child changed\n");
//printf("globa = %d vari = %d\n",globa,vari); ,没有这一句?
}
else
printf("Parent did not changde\n");
printf("globa = %d vari = %d\n",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实现的。
{
printf("This Line Should be Cached...");
sleep(3); //这时候在终端上是看不到任何输出
printf("\nThis Line Should be Cached Again"); //这时候可以看到第一个printf的输出,因为被换行符刷新了
sleep(3); //这时候也只能看到一行输出,而看不到第二个printf输出的
printf("This Line Should Not be Cached Again\n"); //这时候可以看到第二个和第三个printf的输出,因为被结尾的\n刷新
sleep(3);
getchar();
}
为什么需要标准IO缓冲?
LINUX用缓冲的地方遍地可见,不管是硬件、内核还是应用程序,内核里有页高速缓冲,内存高速缓冲,硬件更不用说的L1,L2 cache,应用程序更是多的数不清,基本写的好的软件都有。但归根结底这些缓冲的作用是相同的,都是为了提高机器或者程序的性能。而需要缓冲大部分的情况都是为了协调两个设备或者两个系统间速度的不匹配。
大家都知道IO设备的访问速度与CPU的速度相差好几个数量级,所以为了协调IO设备与CPU的速度的不匹配,对于块设备内核使用了页高速缓存。也就是说,数据会先被拷贝到操作系统内核的页缓存区中,然后才会从操作系统内核的缓存区拷贝到应用程序的地址空间。
当应用程序尝试读取某块数据的时候,如果这块数据已经存放在页缓存中,那么这块数据就可以立即返回给应用程序,而不需要经过实际的物理读盘操作。当然,如果数据在应用程序读取之前并未被存放在页缓存中,那么就需要先将数据从磁盘读到页缓存中去。对于写操作来说,应用程序也会将数据先写到页缓存中去,数据是否被立即写到磁盘上去取决于应用程序所采用的写操作机制:如果用户采用的是同步写机制,那么数据会立即被写回到磁盘上,应用程序会一直等到数据被写完为止;如果用户采用的是延迟写机制,那么应用程序就完全不需要等到数据全部被 写回到磁盘,数据只要被写到页缓存中去就可以了。在延迟写机制的情况下,操作系统会定期地将放在页缓存中的数据刷到磁盘上。与异步写机制不同的是,延迟写机制在数据完全写到磁盘上得时候不会通知应用程序,而异步写机制在数据完全写到磁盘上得时候是会返回给应用程序的。所以延迟写机制本省是存在数据丢失的风险的,而异步写机制则不会有这方面的担心。
在应用程序中,我们经常也使用缓存来解决IO设备与CPU速度的不匹配,如下面的read和write函数:
1.
1
ssize_t read(
int
filedes,
void
*buf, size_t nbytes);
2.
2
ssize_t write(
int
filedes,
const
void
*buf, size_t nbytes);
我们调用read函数从文件读取数据或者调用write来写数据到文件中,一般都是一次性的写多个数据,很少一次写一个byte的数据(除了一些特殊的场景)。所以上面传递给read和write的buf参数其实就是我们的缓冲,这个缓冲的大小都是我们在写程序的时候自己定义的。但问题就来了,我们应该定义多大的缓冲大小才能使IO性能达到最大呢?对于一些不了解内核或者文件系统的人来说是很难知道这个大小的,下面是apue作者自己测试的不同缓冲大小对IO性能的影响的数据:
从上图可以知道当缓冲达到4096大小的时候,继续增加缓冲大小对IO性能影响不大。这个4096大小由文件系统的块大小决定,由于上面的测试所用的文件系统是Linux ext2,块大小是4096。
标准IO库则很好的解决了设置缓冲大小的问题,标准IO会选择最佳的缓存大小,使得我们不用再关心设置缓存大小的问题,事实上标准IO库会对每个IO流自动进行缓冲管理,从而避免了应用程序需要考虑这一点所带来的麻烦。不过使用标准IO库最大的问题也是它的缓冲,使用的不好很容易使系统出现BUG。
标准IO提供的三种类型的缓冲
全缓冲
在填满标准IO缓冲区后才进行实际的IO操作。对于在磁盘上的文件通常由标准IO库实施全缓冲的。
行缓冲
当在输入和输出中遇到换行符时,标准IO库执行IO操作。这允许我们一次输出一个字符(用标准IO函数fputc),但只有在写了一行之后才进行实际IO操作。通常涉及到终端(例如标准输入和标准输出)使用的是行缓冲。
对于行缓冲有两个限制。第一,因为标准IO库用来收集每一行的缓冲区的长度是固定的,所以只要填满了缓冲区,那么即使还没有写一个换行符,也进行IO操作。第二,任何时候只要通过标准IO库要求从(a)一个不带缓存的流,或者(b)一个行缓存的流(它要求从内核得到数据)得到输入数据,那么就会造成冲洗所有行缓冲输出流。其实第二种情况我们会经常遇到,当我们先调用printf输出一串不带换行符的字符时,执行完这条printf语句并不会立刻在屏幕中显示我们输出的数据,当我们接下来调用scanf从标准输入读取数据时,我们才看到前面输出的数据。
不带缓冲
标准IO库不对字符进行存储。例如,如果用标准IO函数fputs写15个字符到不带缓冲的流中,则该函数很可能直接调用write系统调用将这些字符立即写到相关的文件中。标准出错流stderr是不带缓冲的,这样为了让出错的信息可以尽快的显示出来。
修改默认IO缓冲
对于上面提到的每种文件流,IO库都默认分配一个对应的缓冲给它,但有时候我们想自己设置这些缓冲,不要默认的,那么我们可以使用下面两个函数来达到目的:
1.
1
void
setbuf(FILE *restrict fp,
char
*restrict buf);
2.
2
void
setvbuf(FILE *restrict fp,
char
*restrict buf,
int
mode, size_t size);
第一个函数用来打开或者关闭缓冲机制,如果buf为NULL,则关闭缓冲,否则buf指向缓冲区,不过缓冲区的类型和文件流有关。第二个函数可以精确的指定我们所需要的缓冲类型,如下图所示。
获取标准IO默认缓冲大小
下面我们编写一个小的程序来获得标准IO默认缓冲的大小,代码如下:
01.
1
#include <stdio.h>
02.
2
03.
3
int
stream_attribute(FILE *fp)
04.
4
{
05.
5
if
(fp->_flags & _IO_UNBUFFERED)
06.
6
{
07.
7
printf('The IO type is unbuffered
08.
');
09.
8
}
else
if
(fp->_flags & _IO_LINE_BUF){
10.
9
printf('The IO type is line buf
11.
');
12.
10
}
else
{
13.
11
printf('The IO type is full buf
14.
');
15.
12
}
16.
13
printf('The IO size : %d
17.
',fp->_IO_buf_end - fp->_IO_buf_base);
18.
14
return
0
;
19.
15
}
20.
16
int
main()
21.
17
{
22.
18
FILE *fp;
23.
19
stream_attribute(stdin);
24.
20
printf('___________________________________
25.
26.
');
27.
21
stream_attribute(stdout);
28.
22
printf('___________________________________
29.
30.
');
31.
23
stream_attribute(stderr);
32.
24
printf('___________________________________
33.
34.
');
35.
25
if
((fp = fopen(
'test'
,
'w+'
)) == NULL)
36.
26
perror(
'fail to fopen'
);
37.
27
stream_attribute(fp);
38.
28
return
0
;
39.
29
}
在ubuntu下运行结果为:
下面修改下代码,从键盘读入一串字符并写入到test文件中,代码如下:
01.
1
#include <stdio.h>
02.
2
03.
3
int
stream_attribute(FILE *fp)
04.
4
{
05.
5
if
(fp->_flags & _IO_UNBUFFERED)
06.
6
{
07.
7
printf('The IO type is unbuffered
08.
');
09.
8
}
else
if
(fp->_flags & _IO_LINE_BUF){
10.
9
printf('The IO type is line buf
11.
');
12.
10
}
else
{
13.
11
printf('The IO type is full buf
14.
');
15.
12
}
16.
13
printf('The IO size : %d
17.
',fp->_IO_buf_end - fp->_IO_buf_base);
18.
14
return
0
;
19.
15
}
20.
16
int
main()
21.
17
{
22.
18
FILE *fp;
23.
19
char
buf[
20
];
24.
20
25.
21
printf(
'input a string(<20):'
);
26.
22
scanf(
'%s'
, buf);
27.
23
stream_attribute(stdin);
28.
24
printf('___________________________________
29.
30.
');
31.
25
stream_attribute(stdout);
32.
26
fprintf(stderr, '___________________________________
33.
34.
');
35.
27
stream_attribute(stderr);
36.
28
printf('___________________________________
37.
38.
');
39.
29
if
((fp = fopen(
'test.txt'
,
'w+'
)) == NULL)
40.
30
perror(
'fail to fopen'
);
41.
31
fputs(buf, fp);
42.
32
stream_attribute(fp);
43.
33
return
0
;
44.
34
}
运行结果如下:
从上面的运行结果可以看出
。
在linux下,行缓冲的默认大小是1K,全缓冲的大小默认是4K,无缓冲的默认大小是1字节。