popen导致的问题及分析

  1. 背景
  2. 学习
    1. popen与pclose
    2. popen原理
    3. pclose原理
  • 后记
  • 背景

    前段时间,因为某些原因,不能使用glibc提供的popen,所以只能自己模拟popen的实现写了一个新的popen(方便区分,就叫yzwddsg_popen吧)。然后就在前两天,突然爆出来一个问题,使用自己写的yzwddsg_popen fork出来的子进程没有回收,全都变成僵尸进程了,被自己蠢到了。

    先说根因:自己实现了yzwddsg_popen,然而关闭FILE的时候还是使用的glibc的pclose。实际上glibc中的pclose除了关闭FILE之外,还会调用waitpid回收子进程,那么pclose怎么根据传递的FILE找到子进程的pid进行回收的呢?就在在glibc的popen中做的。而自己实现的yzwddsg_popen不会构建这个FILE和pid的关系,因此glibc的pclose也就不会回收yzwddsg_popen fork出来的子进程。

    因此,本文有两个目的:一是警醒自己以后再写这些函数的时候一定得先彻底弄清楚原本的实现,尤其是像这种open close、up down、get put、add sub等配对使用的函数,指不定就隐含一些别的操作;二是通过这个事故了解下glibc中popen pclose的实现,通过俩函数窥探一下glibc的函数的组织和实现方式。

    学习

    (本文参考的glibc的源码版本是2.1.2版本,虽然老,但足够学习使用)

    popen与pclose

    看源码前,总得先看一下这俩函数的功能。其实就是其实就是创建一个pipe流to/from一个子进程。

    popen, pclose - pipe stream to or from a process

    所以其实很明显,popen和pclose一般用来做什么呢?比如在某个程序中起个子进程执行个shell命令之类的,然后把结果传回进程,或者进程向这个子进程传送消息之类的。通过manual的解释也能基本知道popen做了什么,就是创建pipe+fork。

    再看一下使用方式,popen有两个参数,一个会传给/bin/sh带-c去执行的shell命令,一个type标识是用于读还是用于写,返回一个FILE*。pclose就传入FILE*,去关闭popen打开的pipe。

    #include <stdio.h>
    
    FILE *popen(const char *command, const char *type);
    
    int pclose(FILE *stream);
    
    The  command argument is a pointer to a null-terminated string containing a shell command line.  This command is passed
    to /bin/sh using the -c flag; interpretation, if any, is performed by the shell.  The type argument is a pointer  to  a
    null-terminated string which must contain either the letter 'r' for reading or the letter 'w' for writing.
    
    popen原理

    接下来看一下glibc中关于popen的实现。

    首先先看下面这段,从这里我们要扒出popen调用的函数。

    strong_alias的作用是强别名,strong_alias (_IO_new_popen, __new_popen)指的也就是_IO_new_popen和__new_popen其实是同一个函数。

    default_symbol_version指的是版本控制,default_symbol_version (__new_popen, popen, GLIBC_2.1);指的是在glibc的2.1版本中,__new_popen是popen的实现,这俩指向的是同一函数。

    weak_alias是指,当没有定义对应的实现或别名的时候,这个弱别名生效。

    所以,可以知道,在当前的版本中,popen执行的其实就是__new_popen,而__new_popen实际上就是_IO_new_popen函数。所以执行popen的时候,实质上执行的就是_IO_new_popen。

    这么版本控制、强别名、弱别名搞这么复杂有什么用呢?一是,可以用于版本兼容;二是,可以隐藏内部实现;三是,可以允许用户自己定义一些实现来覆盖弱别名。

    #if defined PIC && DO_VERSIONING
    strong_alias (_IO_new_popen, __new_popen)
    default_symbol_version (_IO_new_popen, _IO_popen, GLIBC_2.1);
    default_symbol_version (__new_popen, popen, GLIBC_2.1);
    default_symbol_version (_IO_new_proc_open, _IO_proc_open, GLIBC_2.1);
    default_symbol_version (_IO_new_proc_close, _IO_proc_close, GLIBC_2.1);
    #else
    # ifdef strong_alias
    strong_alias (_IO_new_popen, popen)
    # endif
    # ifdef weak_alias
    weak_alias (_IO_new_popen, _IO_popen)
    weak_alias (_IO_new_proc_open, _IO_proc_open)
    weak_alias (_IO_new_proc_close, _IO_proc_close)
    # endif
    #endif
    

    如上,可以知道,我们程序中的popen,在glibc中实际上执行的就是_IO_new_popen,接下来看看这块内容

    #相关结构体,其中_IO_jump_t* vtable相当于cpp的虚函数表,后面我们会看到怎么用
    struct _IO_FILE_plus
    {
      _IO_FILE file;
      const struct _IO_jump_t *vtable;
    };
    
    #这个结构体包含一个_IO_FILE_plus,一个pid和一个指向自己的指针
    #所以,这个结构体把FILE和pid联系了起来,并且组成一个链表管理
    struct _IO_proc_file
    {
      struct _IO_FILE_plus file;
      /* Following fields must match those in class procbuf (procbuf.h) */
      _IO_pid_t pid;
      struct _IO_proc_file *next;
    };
    
    
    #剔除一些不重要的判断和条件编译,只要最主要的流程
    _IO_FILE *
    _IO_new_popen (command, mode)
         const char *command;
         const char *mode;
    {
      #定义一个locked_FILE结构体
      struct locked_FILE
      {
        struct _IO_proc_file fpx;
      } *new_f;
      _IO_FILE *fp;
      #给这个结构体分配内存
      new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));
      if (new_f == NULL)
        return NULL;
      #给fp赋值为_IO_proc_file中_IO_FILE_plus的file
      fp = &new_f->fpx.file.file;
      _IO_init (fp, 0);
      _IO_JUMPS (fp) = &_IO_proc_jumps;
      #这里会设置offset 状态之类,以及执行_IO_link_in
      #_IO_link_in的作用是设置flag为linked然后把file放到全局链表中_IO_list_all,表示已经link in
      _IO_new_file_init (fp);
      #如果没有_IO_UNIFIED_JUMPTABLES特性的话,就把vtable置NULL
    #if  !_IO_UNIFIED_JUMPTABLES
      new_f->fpx.file.vtable = NULL;
    #endif
      if (_IO_new_proc_open (fp, command, mode) != NULL)
        return fp;
      #这里相当于失败处理,如果前面失败了,unlink然后free调刚才malloc的locked_FILE
      _IO_un_link (fp);
      free (new_f);
      return NULL;
    }
    
    #可以看到_IO_init就是初始化了_IO_FILE的字段
    void
    _IO_init (fp, flags)
         _IO_FILE *fp;
         int flags;
    {
      fp->_flags = _IO_MAGIC|flags;
      fp->_IO_buf_base = NULL;
      fp->_IO_buf_end = NULL;
      fp->_IO_read_base = NULL;
      fp->_IO_read_ptr = NULL;
      fp->_IO_read_end = NULL;
      fp->_IO_write_base = NULL;
      fp->_IO_write_ptr = NULL;
      fp->_IO_write_end = NULL;
      fp->_chain = NULL; /* Not necessary. */
    
      fp->_IO_save_base = NULL;
      fp->_IO_backup_base = NULL;
      fp->_IO_save_end = NULL;
      fp->_markers = NULL;
      fp->_cur_column = 0;
    #if _IO_JUMPS_OFFSET
      fp->_vtable_offset = 0;
    #endif
    #ifdef _IO_MTSAFE_IO
      _IO_lock_init (*fp->_lock);
    #endif
    }
    
    #那么_IO_JUMPS做了什么?可以看到就是把_IO_FILE所在的结构体的vtable赋值,这里就是_IO_proc_jumps
    #define _IO_JUMPS(THIS) ((struct _IO_FILE_plus *) (THIS))->vtable
    #所以这里就是相当于给虚函数表中的ops赋上函数指针
    struct _IO_jump_t _IO_proc_jumps = {
      JUMP_INIT_DUMMY,
      JUMP_INIT(finish, _IO_new_file_finish),
      JUMP_INIT(overflow, _IO_new_file_overflow),
      JUMP_INIT(underflow, _IO_new_file_underflow),
      JUMP_INIT(uflow, _IO_default_uflow),
      JUMP_INIT(pbackfail, _IO_default_pbackfail),
      JUMP_INIT(xsputn, _IO_new_file_xsputn),
      JUMP_INIT(xsgetn, _IO_default_xsgetn),
      JUMP_INIT(seekoff, _IO_new_file_seekoff),
      JUMP_INIT(seekpos, _IO_default_seekpos),
      JUMP_INIT(setbuf, _IO_new_file_setbuf),
      JUMP_INIT(sync, _IO_new_file_sync),
      JUMP_INIT(doallocate, _IO_file_doallocate),
      JUMP_INIT(read, _IO_file_read),
      JUMP_INIT(write, _IO_new_file_write),
      JUMP_INIT(seek, _IO_file_seek),
      JUMP_INIT(close, _IO_new_proc_close),
      JUMP_INIT(stat, _IO_file_stat),
      JUMP_INIT(showmanyc, _IO_default_showmanyc),
      JUMP_INIT(imbue, _IO_default_imbue)
    };
    
    # define JUMP_INIT(NAME, VALUE) VALUE
    # define JUMP_INIT_DUMMY JUMP_INIT(dummy, 0), JUMP_INIT (dummy2, 0)
    
    
    #_IO_new_proc_open函数的主要内容如下,我们把它精简一下
    _IO_FILE *
    _IO_new_proc_open (fp, command, mode)
         _IO_FILE *fp;
         const char *command;
         const char *mode;
    {
      volatile int read_or_write;
      volatile int parent_end, child_end;
      int pipe_fds[2];
      _IO_pid_t child_pid;
      #创建pipe
      if (_IO_pipe (pipe_fds) < 0)
        return NULL;
      #根据传来的type设置parent_end和child_end
      #众所周知,比如如果是要读的话,也就是父进程读子进程的输出
      #那么就设置parent_end为pipe0,即读,设置child_end为pipe1,就是写
      #后面fork子进程出来,就可以在父进程关闭child_end,在子进程关闭parent_end
      #形成一个子进程输出,父进程读取的单向管道(因为父进程的写和子进程的读都关闭了)
      if (mode[0] == 'r' && mode[1] == '\0')
        {
          parent_end = pipe_fds[0];
          child_end = pipe_fds[1];
          read_or_write = _IO_NO_WRITES;
        }
      else if (mode[0] == 'w' && mode[1] == '\0')
        {
          parent_end = pipe_fds[1];
          child_end = pipe_fds[0];
          read_or_write = _IO_NO_READS;
        }
      。。。
      #fork子进程并设置_IO_proc_file中的pid
      #这里就把子进程的pid和file关联上了
      ((_IO_proc_file *) fp)->pid = child_pid = _IO_fork ();
      if (child_pid == 0)
        {
          。。。
          #如刚才所说,关闭parent_end
          _IO_close (parent_end);
          #如果child_end不是标准输入输出,需要转成标准输入输出,因为这是pipe
          if (child_end != child_std_end)
            {
              _IO_dup2 (child_end, child_std_end);
              _IO_close (child_end);
            }
          #exec执行传入的shell命令,然后退出
          _IO_execl ("/bin/sh", "sh", "-c", command, (char *) 0);
          _IO__exit (127);
        }
      #如刚才所说,父进程关闭child_end
      _IO_close (child_end);
      #处理fork失败的情况
      #众所周知,fork成功的话,子进程返回0,就是上面的处理
      #fork成功,父进程返回子进程的pid,fork失败,不产生子进程,父进程返回值小于0
      #(所以,上面执行((_IO_proc_file *) fp)->pid = child_pid = _IO_fork ();,在父进程中赋值的是子进程的pid)
      if (child_pid < 0)
        {
          _IO_close (parent_end);
               return NULL;
        }
      #把fd关联上file
      _IO_fileno (fp) = parent_end;
    
      #链接到proc_file_chain中
      ((_IO_proc_file *) fp)->next = proc_file_chain;
      proc_file_chain = (_IO_proc_file *) fp;
    。。。
    }
    

    所以,用户程序中的popen在glibc中的执行过程可以总结如下:

      • 初始化_IO_FILE字段,比如flag _IO_buf_base等,也就是设置file本身的一些属性,比如读写基址之类
      • 设置虚函数表,包括一些函数的实现,比如read write close之类(其实就类似VFS的作用)
      • 把file link到全局链表
      • 创建pipe
      • fork子进程(这里会把pid传给_IO_proc_file中的pid,也就实现了pid和file的关联)
      • 子进程执行设定的shell命令
      • 把_IO_proc_file链接到proc_file_chain中
    pclose原理

    把上面追完之后,再看pclose其实就简单多了

    #如此,可知pclose实际就是执行__new_pclose
    default_symbol_version (__new_pclose, pclose, GLIBC_2.1);
    
    #而__new_pclose函数中是执行_IO_new_fclose函数,所以我们直接看_IO_new_fclose函数
    #以下,可以看出,_IO_new_fclose其实为了兼容fp做了一些判断的,而我们这里的pipe流肯定不是fulebuf,因此直接走_IO_FINISH
    _IO_new_fclose (fp)
         _IO_FILE *fp;
    {
    。。。
      if (fp->_IO_file_flags & _IO_IS_FILEBUF)
        status = _IO_file_close_it (fp);
      else
        status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
      _IO_FINISH (fp);
    。。。
      if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)
        {
          fp->_IO_file_flags = 0;
          free(fp);
        }
    。。。
    }
    #总之这里一系列宏定义其实就是最终找到虚函数表中的函数指针去执行对应的函数,也就是_IO_new_file_finish
    #define _IO_FINISH(FP) JUMP1 (__finish, FP, 0)
    # define JUMP1(FUNC, THIS, X1) _IO_JUMPS_FUNC(THIS)->FUNC (THIS, X1)
    #define _IO_JUMPS(THIS) ((struct _IO_FILE_plus *) (THIS))->vtable
    #if _IO_JUMPS_OFFSET
    # define _IO_JUMPS_FUNC(THIS) \
     (*(struct _IO_jump_t **) ((void *) &((struct _IO_FILE_plus *) (THIS))->vtable\
                               + (THIS)->_vtable_offset))
    #else
    # define _IO_JUMPS_FUNC(THIS) _IO_JUMPS(THIS)
    #endif
    
    
    _IO_new_file_finish (fp, dummy)
         _IO_FILE *fp;
         int dummy;
    {
      if (_IO_file_is_open (fp))
        {
          #flush
          _IO_do_flush (fp);
          if (!(fp->_flags & _IO_DELETE_DONT_CLOSE))
          #然后调用_IO_SYSCLOSE,这个其实也是一个函数指针,跳到__close,这里就是_IO_new_proc_close
            _IO_SYSCLOSE (fp);
        }
      #这里做的就是把FILE的一些属性进行清理,对应之前的_IO_new_file_init的操作
      #然后把fp unlink
      _IO_default_finish (fp, 0);
    }
    
    
    _IO_new_proc_close (fp)
         _IO_FILE *fp;
    {
      #从proc_file_chain unlink
      /* Unlink from proc_file_chain. */
      for ( ; *ptr != NULL; ptr = &(*ptr)->next)
        {
          if (*ptr == (_IO_proc_file *) fp)
            {
              *ptr = (*ptr)->next;
              status = 0;
              break;
            }
        }
    
      #关闭fp
      if (status < 0 || _IO_close (_IO_fileno(fp)) < 0)
        return -1;
      #waitpid等待回收子进程
      do
        {
          wait_pid = _IO_waitpid (((_IO_proc_file *) fp)->pid, &wstatus, 0);
        }
      while (wait_pid == -1 && errno == EINTR);
      if (wait_pid == -1)
        return -1;
      return wstatus;
    }
    

    所以,pclose的执行过程可以总结为:

      • flush,确保没有残留数据
      • 从proc_file_chain中unlink _IO_proc_file结构体
      • 关闭fd
      • waitpid回收子进程
      • 把fp从全局文件链表unlink

    注:

    可以看到popen有linkin挂链表操作,pclose对应两次unlink操作。这俩链表的作用:proc_file_chain就是相当于专门用于popen的一个静态全局链表,用于链接_IO_proc_file,而_IO_proc_file结构体的主要作用其实上面也说了,其实就是为popen关联pid和file;而_IO_list_all链表可不只用于popen,它是维护一个一个全局的(当然,是进程级别)打开文件的链表,维护管理所有打开的FILE*流。

    后记

    1.看glibc中的popen这些绕来绕去,调这个那个的函数,搞虚函数表这些,其实,说到底,在c里实现多态就是用函数指针,glibc的虚函数表也是,内核的vfs 各种驱动也是

    2.如之前所说,glibc看上去这么复杂,各种weak alias、strong alias等。weak alias的其中一个好处是可以允许用户自定义一些函数的实现来覆盖glibc的默认实现。一个典型例子是tcmalloc,在运行程序时通过LD_PRELOAD优先加载libtcmalloc.so,在调用malloc的时候就会去调用tcmalloc的malloc实现而不是glibc的。而实现这个的基础就是,glibc中的这些函数全是weak alias的。如下

    weak_alias (__libc_calloc, __calloc) weak_alias (__libc_calloc, calloc)
    weak_alias (__libc_free, __cfree) weak_alias (__libc_free, cfree)
    weak_alias (__libc_free, __free) weak_alias (__libc_free, free)
    weak_alias (__libc_malloc, __malloc) weak_alias (__libc_malloc, malloc)
    weak_alias (__libc_memalign, __memalign) weak_alias (__libc_memalign, memalign)
    weak_alias (__libc_realloc, __realloc) weak_alias (__libc_realloc, realloc)
    weak_alias (__libc_valloc, __valloc) weak_alias (__libc_valloc, valloc)
    weak_alias (__libc_pvalloc, __pvalloc) weak_alias (__libc_pvalloc, pvalloc)
    weak_alias (__libc_mallinfo, __mallinfo) weak_alias (__libc_mallinfo, mallinfo)
    weak_alias (__libc_mallopt, __mallopt) weak_alias (__libc_mallopt, mallopt)
    

    转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 857879363@qq.com