关于文件缓冲的问题
有三种类型的缓冲策略:
无缓冲,块缓冲和行缓冲。
当输出流无缓冲时,信息在写的同时出现于目标文件或终端上;
当是块缓冲时,字符被暂存,然后一起写入;
当是行缓冲时,字符被暂存,直到要输出一个新行符,或者从任何与终端设备连接的流中 (典型的是 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 系统调用函数将这些字符写至相关联的打开文件。