Unix-Like系统C语言标准I/O缓冲

Unix-Like系统C语言标准输入输出缓冲模式

####以及系统调用与C库函数混合编码需注意的Buffer问题


最近群里有同学讨论到一些关于缓冲的问题,便写几行超简单的代码简单解说一下:

/*buffer-test.c*/
#include <stdio.h>
#include <unistd.h>

int main(int a, char **b)
{
    printf("printf line break output:\t\n");    /*有换行符*/
    printf("printf output:\t");              /*无换行符*/
    write(STDOUT_FILENO, "write output:\t", 14);/*无换行符*/

    return 0;
}

编译:

wuqiong:tmp apple$ cc buffer-test.c -o buffer-test

运行:

wuqiong:tmp apple$ ./buffer-test
printf line break output:
write output:   printf output:  wuqiong:tmp apple$

此时我们可以看到第二条printf语句的输出跑到最后面去了,初学者看来程序没有按照预期的方式执行;其实这是没有任何问题而且是很正确的行为,因为有个稍微底层一点的关于标准输入输出的缓冲机制我们应该了解,那就是C库函数中的标准输出函数在终端(Terminal)默认是基于行缓冲的,C库运行时内部行缓冲之后才进入到操作系统内核缓冲中去,而系统调用write函数不属于库函数的一部分,不依赖C语言运行时,故他不需要经过这样一个行缓冲的过程,而直接进入内核缓冲。这样就导致了第二个不包含换行符号的printf函数的输出到看write系统调用的输出之后。

这样是不是稍微有点理解清楚了?下面我们再进行一次别样的测试:来个通过管道重定向输出看看是什么现象。执行命令如下:


wuqiong:tmp apple$ ./buffer-test | cat
write output:   printf line break output:
printf output:  wuqiong:tmp apple$

噢,又有变化了。write系统调用的输出更提前了。这是什么原因呢。不应该啊?呵呵。事情是这样的。重定向是通过一个内核里的一个管道来实现的。这一命令会创建2个进程。第一个进程的输出会写入到管道的输入端,管道的输出端的输出会写入第二个进程的输入端。这样一来printf数据到第二进程(cat)处理时就经过 3个缓冲区。第一个进程的C库缓冲→内核的管道缓冲→第二个进程的C库缓冲。而系统调用write的数据就只经过一个内核管道的缓冲就到了第二进程的read直接读出来了。示意图:


管道缓冲示意图


默认缓冲模式:

  1. stdin总是全缓冲模式
  2. stderr总是无缓冲模式
  3. stdout如果是输出到终端的话就会自动设置为行缓冲,否则就是全缓冲

引用:http://www.gnu.org/software/libc/manual/html_node/Buffering-Concepts.html


假如管道重定向有级联的情况又是什么样呢。比如这样的一条日志分析shell命令:

$ tail -f access.log | cut -d' ' -f1 | uniq

我们还是再看个示意图吧,有图有真相嘛:

管道重定向级联的缓冲示意图


那么,我们可不可以把C语言运行时里的缓冲机制给关闭掉呢?答案是肯定的!C库函数中有一系列操作缓冲机制的函数,我们可以根据我们的实际情况来关闭或者设置合适的缓冲模式。读者可以在Unix-like操作系统中的终端执行如下命令就可以看到各个函数的原型以及说明文档:


wuqiong:tmp apple$ man 3 setbuf

回车会输出如下结果:

SETBUF(3)                BSD Library Functions Manual                SETBUF(3)

NAME
     setbuf, setbuffer, setlinebuf, setvbuf -- stream buffering operations

LIBRARY
     Standard C Library (libc, -lc)

SYNOPSIS
     #include <stdio.h>

     void
     setbuf(FILE *restrict stream, char *restrict buf);

     void
     setbuffer(FILE *stream, char *buf, int size);

     int
     setlinebuf(FILE *stream);

     int
     setvbuf(FILE *restrict stream, char *restrict buf, int type, size_t size);

DESCRIPTION
     Three types of buffering are available: unbuffered, block buffered, and line buffered.  When an output stream is unbuffered, information
     appears on the destination file or terminal as soon as written; when it is block buffered, many characters are saved up and written as a
     block; when it is line buffered, characters are saved up until a newline is output or input is read from any stream attached to a termi-
     nal device (typically stdin).  The function fflush(3) may be used to force the block out early.  (See fclose(3).)

     Normally, all files are block buffered.  When the first I/O operation occurs on a file, malloc(3) is called and an optimally-sized buffer
     is obtained.  If a stream refers to a terminal (as stdout normally does), it is line buffered.  The standard error stream stderr is
     always unbuffered.

这几个设置缓冲的函数我就不多说了,大家自己阅读文档和亲手实验吧。Unix下man命令是manual的简写,3是库函数的手册,2是系统调用的手册。


介绍了这么些关于C语言标准输入输出的缓冲机制,C语言标准输入输出也几乎是每一个程序员初次学C语言最先接触的,希望本文对读者有或多或少的帮助。

标签:none

仅有 1 条评论

  1. manfeel

    学习了,持续关注大茶的Blog中^_^

添加新评论