有三种类型的缓冲策略:
无缓冲,块缓冲和行缓冲。
当输出流无缓冲时,信息在写的同时出现于目标文件或终端上;
当是块缓冲时,字符被暂存,然后一起写入;
当是行缓冲时,字符被暂存,直到要输出一个新行符,或者从任何与终端设备连接的流中 (典型的是 stdin) 读取输入时才输出。
函数 fflush(3) 可以用来强制提前输出。(参见 fclose(3)) 通常所有文件都是块缓冲的。
当文件 I/O 操作在文件上发生时,将调用 malloc(3) ,获得一个缓冲。
如果流指向一个终端 (通常 stdout 都是这样),那么它是行缓冲的。
标准错误流 stderr 默认总是无缓冲的。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
setvbuf 可以用在任何打开的流上,改变它的缓冲。函数声明:int setvbuf(FILE *stream, char *buf, int mode , size_t size); 参数说明:stream - 流指针buf - 缓冲区mode - 必须是下列三个宏之一:_IONBF 无缓冲_IOLBF 行缓冲_IOFBF 完全缓冲size - 缓冲区大小 |
除非是无缓冲的文件,否则参数 buf 应当指向一个长度至少为 size 字节的缓冲;这个缓冲将取代当前的缓冲。如果参数 buf 是 NULL ,只有这个模式会受到影响;下次 read 或 write 操作还将分配一个新的缓冲。
函数 setvbuf 只能在打开一个流,还未对它进行任何其他操作之前使用。
我们出现了一个问题:程序的日志输出到了终端上,但是没有输出到日志中。
问题分析:
1.输出到了终端上,因为指向终端的流是行缓冲的。
2.写入文件的日志由于是块缓冲,但是该程序的日志比较少,没有写满缓冲块的时候则不会写入文件。
问题解决:
1.通fflush可以把缓冲区内容刷到文件中
2.通过setvbuf接管缓冲区,自己控制缓冲区大小,以及缓冲模式
3.另外当程序由于终止时(收到结束信号等),也不会把缓冲内容刷到缓冲区中。
测试程序以及验证方法:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
test.c:#include "stdio.h"#include "stdlib.h"#include "stdarg.h"#include "time.h"static FILE *log_file = NULL;int dbgprintf(const char *format, ...){ va_list args; va_start(args, format); if (log_file) vfprintf(log_file, format, args); else vfprintf(stdout, format, args); va_end(args);}int main(void){ time_t curtime; struct tm *now; log_file = fopen("/tmp/logtest.txt", "a+"); //设置自己的缓冲区 char buf[1000]; setvbuf(log_file, buf,_IOFBF,1000); //注意句柄和mode //循环执行,每次输出当前时间以及循环次数 int nLoopTime = 0; while(1) { nLoopTime++; time(&curtime); now = localtime(&curtime);//dbgprintf是输出到文件中 dbgprintf( "
%d-%d-%d %d:%d:%d-------------------
", now->tm_year+1900,now->tm_mon+1,now->tm_mday,now->tm_hour,now->tm_min,now->tm_sec ); dbgprintf("hihi,%d.
", nLoopTime); dbgprintf("hihi,%s.
", "ohyeah1"); dbgprintf("hihi,%s.
", "ohyeah2"); dbgprintf("hihi,%s.
", "ohyeah3"); dbgprintf("hihi,%s.
", "ohyeah4"); //printf是stdout,输出到终端 printf("loop:nLoopTime=%d.
", nLoopTime); sleep(1); } return 1;} |
从以上程序可以看到,当前的缓冲模式为_IOFBF,也就是块缓冲,当缓冲写满1000时,才会刷到文件中。
程序编译:
|
1
|
gcc -o ack test.c |
执行程序,可以看到printf在终端上的输出:
|
1
2
3
4
5
6
7
8
9
10
|
[root@localhost logtest]# ./ack loop:nLoopTime=1.loop:nLoopTime=2.loop:nLoopTime=3.loop:nLoopTime=4.loop:nLoopTime=5.loop:nLoopTime=6.loop:nLoopTime=7.loop:nLoopTime=8.loop:nLoopTime=9. |
查看日志文件:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
[root@localhost tmp]# tail -f logtest.txt 2013-11-14 10:18:30-------------------hihi,ohyeah1.hihi,ohyeah2.hihi,ohyeah3.hihi,ohyeah4.2013-11-14 10:18:31-------------------hihi,ohyeah1.hihi,ohyeah2.hihi,ohyeah32013-11-14 10:28:19-------------------hihi,1.hihi,ohyeah1.hihi,ohyeah2.hihi,ohyeah3.hihi,ohyeah4.2013-11-14 10:28:20-------------------hihi,2.hihi,ohyeah1.hihi,ohyeah2.hihi,ohyeah3.hihi,ohyeah4.2013-11-14 10:28:21------------------- |
仔细观察可以发现,printf和日志是不同步的,日志中的信息是一块一块刷出来的。
换个模式试试,把setvbuf的mode改为_IOLBF或者_IONBF,则会发现日志立刻就写进去了。
当然,也可以试试用fflush刷进去,效果是一样的。
比较简单,就不演示了。
下面转了一些概念过来:
对于写操作通常我们会遇到两个两个缓冲 (buffer):
一个是内核缓冲。 当我们调用write写文件时,write返回之后其实内容并没有立刻写到硬盘上,而是写到了内核的缓存中。什么时候写到磁盘?内核有一套刷缓存的机制。这样做有很明显的好处,比如我们调用1次write写1kb和调用1k次write每次写1b的数据,所花的时间是差不多的。后者所花的用户态/内核态切换时间多些,但是写磁盘的次数却是一样的。这样就大大提高了效率。
另外一个是glibc维护的用户态缓冲。 这个缓冲又是用来干什么的呢?内核和硬盘是两个相对独立的系统,内核缓冲在这两个之间避免了很多不必要的同步。那么同样,内核和用户程序也是两个相对独立的系统,每次系统调用也是要花代价的。所以上面1次write写1kb和调用1k次write每次写1b的数据的例子,前后两种方法还是有差距的,差距就在于后者需要做1k此用户态和内核态的切换。所以,glibc在用户态上又做了一个缓冲。当我们调用glibc提供的printf输出的时候,并没有直接映射到一次write系统调用,而是存在了glibc管理的缓冲中,当条件满足时(下面会说上面时候满足)再调用一次write,把用户态的缓冲写到内核态去。所以,调用1此printf到文件1kb字符和1k此print每次1个字符,所花的时间就真差不多了。
块缓冲:
第一次执行 I/O 操作时,ANSI 标准的文件管理函数通过调用
malloc 函数获得需使用的缓冲区。默认大小为 8192。
行缓冲:
在这种情况下,当在输入和输出中遇到换行符时,标准 I/O 库执行 I/O
系统调用操作。当流涉及一个终端时(例如标准输入和标准输出),使用行缓冲区。因为标准I/O 库收集的每行的缓冲区长度是固定的,只要填满了缓冲区,即使还没有遇到换行符,也将执行 I/O 系统调用操作。默认行缓冲区大小为 128 字节。
无缓冲:
标准 I/O 库不对字符进行缓存。如果用标准 I/O 函数写若干字符到不带
缓冲区的流中,则相当于用 write 系统调用函数将这些字符写至相关联的打开文件。