有三种类型的缓冲策略:
无缓冲,块缓冲和行缓冲。
当输出流无缓冲时,信息在写的同时出现于目标文件或终端上;
当是块缓冲时,字符被暂存,然后一起写入;
当是行缓冲时,字符被暂存,直到要输出一个新行符,或者从任何与终端设备连接的流中 (典型的是 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(
"\n%d-%d-%d %d:%d:%d-------------------\n"
,
now->tm_year+
1900
,now->tm_mon+
1
,now->tm_mday,now->tm_hour,now->tm_min,now->tm_sec );
dbgprintf(
"hihi,%d.\n"
, nLoopTime);
dbgprintf(
"hihi,%s.\n"
,
"ohyeah1"
);
dbgprintf(
"hihi,%s.\n"
,
"ohyeah2"
);
dbgprintf(
"hihi,%s.\n"
,
"ohyeah3"
);
dbgprintf(
"hihi,%s.\n"
,
"ohyeah4"
);
//printf是stdout,输出到终端
printf(
"loop:nLoopTime=%d.\n"
, 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,ohyeah3
2013
-
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 系统调用函数将这些字符写至相关联的打开文件。