关于文件缓冲的问题

有三种类型的缓冲策略:

无缓冲,块缓冲和行缓冲。

 

当输出流无缓冲时,信息在写的同时出现于目标文件或终端上;

当是块缓冲时,字符被暂存,然后一起写入;

当是行缓冲时,字符被暂存,直到要输出一个新行符,或者从任何与终端设备连接的流中 (典型的是 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 系统调用函数将这些字符写至相关联的打开文件。

 

posted on 2013-11-14 13:05  solohac  阅读(299)  评论(0编辑  收藏  举报

导航