从一些现象看printf的缓冲机制

一、C库的printf函数簇
这些函数其实大家最为熟悉,因为每个人都会写的hello world就是使用了printf这个C库函数。但是printf的实现并不见,如果有兴趣的同学可以看一下glibc中关于这个函数的哦实现,先不说各种格式化的处理以及文件的锁,其中的缓冲区管理及动态资源管理就有相当多的代码。
这里主要是想通过一些现象来看一下printf函数的缓冲区机制可能造成的一些看起来比较奇怪的问题。
二、缓冲区的大小
每个FILE结构的缓冲区大小在文件创建时分配,分配的函数在glibc-2.11.2\libio:filedoalloc.c
  size = _IO_BUFSIZ; 其中这个宏的值默认为8192
  if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0) >= 0)
    {
      if (S_ISCHR (st.st_mode))
    {
      /* Possibly a tty.  */
      if (
#ifdef DEV_TTY_P
          DEV_TTY_P (&st) ||
#endif
          isatty (fp->_fileno))
        fp->_flags |= _IO_LINE_BUF;
    }
#if _IO_HAVE_ST_BLKSIZE
      if (st.st_blksize > 0)
    size = st.st_blksize;
#endif
    }
这里比较有意思的是对于文件类型的识别,如果文件类型是终端设备,那么此时为使能行缓冲机制,行缓冲机制区别于默认的片缓冲区机制,前者是在遇到一个回车时一定进行缓冲区的冲刷,而对于后则则在整个缓冲区满了之后才会输出。对于这个行缓冲区标识的使用位置在glibc-2.11.2\libio\fileops.c
int
_IO_new_file_overflow (f, ch)
      _IO_FILE *f;
      int ch;

  if ((f->_flags & _IO_UNBUFFERED)
      || ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
    if (INTUSE(_IO_do_write) (f, f->_IO_write_base,
                  f->_IO_write_ptr - f->_IO_write_base) == EOF)
这里的缓冲区分配并不是在fopen的时候分配,而是在真正print的时候根据需要创建和分配,在glibc2.11的调用链为
(gdb) bt
#0  _IO_file_doallocate (fp=<value optimized out>) at filedoalloc.c:117
#1  0x0804cc7f in _IO_doallocbuf (fp=<value optimized out>) at genops.c:423
#2  0x0804bb91 in _IO_new_file_overflow (f=<value optimized out>, 
    ch=<value optimized out>) at fileops.c:842
#3  0x0804c636 in __overflow (f=<value optimized out>, 
    ch=<value optimized out>) at genops.c:248
#4  0x0806744d in _IO_vfprintf_internal (s=<value optimized out>, 
    format=<value optimized out>, ap=<value optimized out>) at vfprintf.c:1592
#5  0x08048f75 in __printf (format=<value optimized out>) at printf.c:35
#6  0x080482be in main () at buffer.c:16
(gdb) 
三、验证设备对缓冲区的影响
1、使用管道
[root@Harry filebuff]# cat typical.c 
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int i;
    for ( i = 1; i > 0 ; i++)
    {
        sleep(1);
        
        printf("%1024d",i);
    }
}
[root@Harry filebuff]# gcc typical.c -o typical
[root@Harry filebuff]# ./typical | tee
执行此命令之后,可以看到,在输出中是按照每4个数值(1024*4=4096)字节来一次性输出的,我们看一下管道的缓冲区大小
[root@Harry filebuff]# ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 8192
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 10240
cpu time               (seconds, -t) unlimited
max user processes              (-u) 1024
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited
[root@Harry filebuff]# 
2、伪终端
[root@Harry filebuff]# stat /proc/self/fd/1
  File: `/proc/self/fd/1' -> `/dev/pts/5'
  Size: 64            Blocks: 0          IO Block: 1024   symbolic link
Device: 3h/3d    Inode: 111229      Links: 1
Access: (0700/lrwx------)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2013-08-05 22:54:42.070907990 +0800
Modify: 2013-08-05 22:54:42.070907990 +0800
Change: 2013-08-05 22:54:42.070907990 +0800
[root@Harry filebuff]# ./typical 
输出以1为单位输出,即1024字节为缓冲区。
3、普通文件
[root@Harry filebuff]# ./typical > typical.out &
[1] 15287
[root@Harry filebuff]# tail -f typical.out 
[root@Harry filebuff]# stat typical.out 
  File: `typical.out'
  Size: 16384         Blocks: 32         IO Block: 4096   regular file
Device: fd00h/64768d    Inode: 529035      Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2013-08-05 22:56:28.999031207 +0800
Modify: 2013-08-05 22:56:33.015530737 +0800
Change: 2013-08-05 22:56:33.015530737 +0800
也是以8KB为单位。
四、缓冲区是否会丢失
既然进程进行了缓冲,那么会不会进程退出之后这些信息丢失呢?
1、正常情况下
不会丢失,因为C库会将进程所有打开的FILE结构组成一个链表,在exit之后执行这些文件的缓冲区冲刷清理工作,所以不会丢失。
struct _IO_FILE {
  int _flags;        /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;    /* Current read pointer */
  char* _IO_read_end;    /* End of get area. */
  char* _IO_read_base;    /* Start of putback+get area. */
  char* _IO_write_base;    /* Start of put area. */
  char* _IO_write_ptr;    /* Current put pointer. */
  char* _IO_write_end;    /* End of put area. */
  char* _IO_buf_base;    /* Start of reserve area. */
  char* _IO_buf_end;    /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;


  struct _IO_FILE *_chain;
genops.c
INTDEF(_IO_un_link)

void
_IO_link_in (fp)
     struct _IO_FILE_plus *fp;
{
  if ((fp->file._flags & _IO_LINKED) == 0)
    {
      fp->file._flags |= _IO_LINKED;
#ifdef _IO_MTSAFE_IO
      _IO_cleanup_region_start_noarg (flush_cleanup);
      _IO_lock_lock (list_all_lock);
      run_fp = (_IO_FILE *) fp;
      _IO_flockfile ((_IO_FILE *) fp);
#endif
      fp->file._chain = (_IO_FILE *) INTUSE(_IO_list_all);
      INTUSE(_IO_list_all) = fp;
      ++_IO_list_all_stamp;
#ifdef _IO_MTSAFE_IO
      _IO_funlockfile ((_IO_FILE *) fp);
      run_fp = NULL;
      _IO_lock_unlock (list_all_lock);
      _IO_cleanup_region_end (0);
#endif
    }
}
2、异常情况
大家可以看到,这个实现是依赖于在main函数退出之后执行,在exit的代码中执行冲刷,如果进程不幸是被kill掉或者其它信号导致的问题,那么此时输出就真的丢失了。
这一点和操作系统的缓写机制不同,后者在进程被kill掉依然会写回硬盘,只要没有断点。而对于printf来说,它的缓存对操作系统来说透明,所以异常终止就丢失输出。
[root@Harry filebuff]# cat buffer.c 
#include <stdio.h>
#include <errno.h>
#include <signal.h>
#include <stdlib.h>

void sighandler(int sig)

printf("sighand %d\n", sig);
exit(0);


int main()  
{
signal(SIGINT,sighandler);
FILE * file = fopen("/dev/tty","wr");
printf("%p\n",file);
int icount = fprintf(file,"sdfksdkfsdkfsd");
sleep(1000);
printf("after before\n");
}
[root@Harry filebuff]# gcc buffer.c -o buffer
[root@Harry filebuff]# ./buffer 
0x9c4b008
^Csighand 2
sdfksdkfsdkfsd[root@Harry filebuff]# 
可以看到,如果捕捉了一场信号,此时缓冲区中的内容会在最后被打印出来。但是如果是通过其它没有被注册的信号杀死进程,则内容丢失。
sdfksdkfsdkfsd[root@Harry filebuff]# ./buffer &
0x922e008
[2] 15361
[root@Harry filebuff]# kill -TERM 15361
[root@Harry filebuff]# kill -TERM 15361
bash: kill: (15361) - No such process
[2]+  Terminated              ./buffer
[root@Harry filebuff]# 
五、缓冲区的独立性
我们知道,通常进程在启动的时候,stdin,stdout,stderr是三个与创建的FILE结构,虽然通常它们对应的是同一个设备,但是由于它们使用的是各自的缓冲区,所以它们在代码里出现的顺序和最终看到的顺序可能并不相同。
例如我们如果将标准输入和输出都重定向到相同的文件,那么文件的最终顺序和输出顺序同样可能不同。
[root@Harry filebuff]# cat fork.c 
#include <stdio.h>
int main()
{
    int i;
    for (i = 0; i < 1024; i++)
    {
        printf("stdin");
        fprintf(stderr,"stderr");
    }
}
[root@Harry filebuff]# gcc fork.c -o fork
[root@Harry filebuff]# ./fork >fork.ort 2>&1  虽然同一个文件,但是由于使用独立缓冲区,所以输出是大杂居,小聚居的分布。
[root@Harry filebuff]# cat  fork.ort 
stderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstderrstder
六、一个grep缓冲的例子
[root@Harry filebuff]# ( for((loop = 0 ; loop<200; loop++)) do echo int; done ; yes ) | grep int | more
grep默认是不会处理缓冲区的,也即是使用C库的默认缓冲机制,通过上面的例子可以看到,这个命令始终不会有输出,如果去掉最后的管道,则马上会有输出。因为去掉管道之后,此时输出为一个tty设备,此时tty使用行缓冲,每个匹配都会打印出来。
而对于管道,它缓冲区大小为4KB,所以虽然有200个匹配项,但是输出内容没有达到4KB,所以还是没有输出。不过grep有一个使用行缓冲的选项,
[root@Harry filebuff]# ( for((loop = 0 ; loop<200; loop++)) do echo int; done ; yes ) | grep int --line-buffered | more
int
int
int
int
int
int
int
int
int
int
int
int
int
int
int
int
int
int
int
int
int
int
int
--More--
 
 
 
 
 

posted on 2019-03-07 09:30  tsecer  阅读(428)  评论(0编辑  收藏  举报

导航