zoukankan      html  css  js  c++  java
  • 从一些现象看printf的缓冲机制

    一、C库的printf函数簇
    这些函数其实大家最为熟悉,因为每个人都会写的hello world就是使用了printf这个C库函数。但是printf的实现并不见,如果有兴趣的同学可以看一下glibc中关于这个函数的哦实现,先不说各种格式化的处理以及文件的锁,其中的缓冲区管理及动态资源管理就有相当多的代码。
    这里主要是想通过一些现象来看一下printf函数的缓冲区机制可能造成的一些看起来比较奇怪的问题。
    二、缓冲区的大小
    每个FILE结构的缓冲区大小在文件创建时分配,分配的函数在glibc-2.11.2libio: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.2libiofileops.c
    int
    _IO_new_file_overflow (f, ch)
          _IO_FILE *f;
          int ch;

      if ((f->_flags & _IO_UNBUFFERED)
          || ((f->_flags & _IO_LINE_BUF) && ch == ' '))
        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 ", sig);
    exit(0);


    int main()  
    {
    signal(SIGINT,sighandler);
    FILE * file = fopen("/dev/tty","wr");
    printf("%p ",file);
    int icount = fprintf(file,"sdfksdkfsdkfsd");
    sleep(1000);
    printf("after before ");
    }
    [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--
     
     
     
     
     
  • 相关阅读:
    Mtk Ft6306 touch 驱动 .
    第一屏不显示懒加载的图片内容,这个方法可以搞定
    C#多线程中访问winform控件 (解决Winform 对象当前正在其他地方使用)
    变化的科技感十足的网站,推荐
    新年有感
    获取高精度时间注意事项 (QueryPerformanceCounter , QueryPerformanceFrequency)
    修改 TeamViewer ID 的方法
    VS2017离线安装包[百度云盘](收藏了)
    老子今天不加班,程序员也需要自由
    改变Eclipse 中代码字体大小
  • 原文地址:https://www.cnblogs.com/tsecer/p/10487534.html
Copyright © 2011-2022 走看看