源码解析glibc中的pclose与fclose函数

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/SKY453589103/article/details/86603125

glibc源码版本:2.17

pclose 和 fclose 的阻塞问题

测试代码

int main()
{
    for(unsigned int i = 0; i < 100; ++i)
    {
        FILE *fpReader;
        fpReader = popen("sleep 10 &","re");
        if(NULL == fpReader)
        {
            printf("open pipe error \n");
        }
        printf("%d\n", i);
        sleep(1);
        // 下面这个函数阻塞了,但是没报错,为什么呢?
        fclose(fpReader);
    }

    return 0;
}

问题

  • 问题一:为什么popen打开的管道,能通过fclose 正常关闭,没有出错?
  • 问题二:为什么popen是不阻塞的,反而是fclose(或pclose)阻塞了呢?

pclose与fclose的关系

fclose函数的定义

libio/iofclose.c文件可以看到:

// fclose被重命名了
# define _IO_new_fclose fclose
// 函数参数类型是写在括号外面的,这种写法在现在已经不常见了,远古遗留的东西
int
_IO_new_fclose (fp)
     _IO_FILE *fp;
{
  int status;

  CHECK_FILE(fp, EOF);

#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
  /* We desperately try to help programs which are using streams in a
     strange way and mix old and new functions.  Detect old streams
     here.  */
  if (_IO_vtable_offset (fp) != 0)
    return _IO_old_fclose (fp);
#endif

  /* First unlink the stream.  */
  if (fp->_IO_file_flags & _IO_IS_FILEBUF)
    _IO_un_link ((struct _IO_FILE_plus *) fp);
  // 对文件执行加锁,因此flcose是线程安全的
  _IO_acquire_lock (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_release_lock (fp);
  // 调用文件虚表中的__finish函数
  // 虚表会在后面讲到
  _IO_FINISH (fp);
  if (fp->_mode > 0)
    {
#if _LIBC
      /* This stream has a wide orientation.  This means we have to free
	 the conversion functions.  */
      struct _IO_codecvt *cc = fp->_codecvt;

      __libc_lock_lock (__gconv_lock);
      __gconv_release_step (cc->__cd_in.__cd.__steps);
      __gconv_release_step (cc->__cd_out.__cd.__steps);
      __libc_lock_unlock (__gconv_lock);
#endif
    }
  else
    {
      // 清空备份空间,文件都关闭了,留着也没必要
      if (_IO_have_backup (fp))
	_IO_free_backup_area (fp);
    }
  // 如果不是标准输出、标准出错、标准输入,关闭之后就要释放空间。
  // 因为其他文件描述都是通过malloc分配得到内存的
  // 而这3个标准描述符是全局变量
  if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)
    {
      fp->_IO_file_flags = 0;
      free(fp);
    }

  return status;
}
/// 补充说明,标准输出,标准输入,标准出错声明在libio.h中
struct _IO_FILE_plus;
extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;
// 而实现在libio/stdfiles.c中
// DEF_STDFILE 是一个宏定义
DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);
// 我们所熟知的stdin,stdout,stderr 实际上是上面三个结构体的指针
// 定义在libio/stdio.h中
_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;
_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;

pclose函数的定义

函数pclose被宏定义了,在源文件libio/pclose.c

int
__new_pclose (fp)
     FILE *fp;
{
  // 实际上调用的是的fclose的函数
  return _IO_new_fclose (fp);
}

总结

  • fclose跟pclose的底层实现是一样的。

接下来让我们看看为什么会阻塞。

fclose中的block

有嫌疑的地方

从上面的fclose的源码中,没有任何有阻塞调用的痕迹,剩下的就是调用其他函数的时候发生了阻塞。

// 实际上啥也没做,不会造成阻塞
CHECK_FILE(fp, EOF);
// 其实现在libio/genops.c下,只是做文件标志清除,不会造成阻塞
_IO_un_link ((struct _IO_FILE_plus *) fp);
// 会调用虚表中的__close函数,可能造成阻塞
status = _IO_file_close_it (fp);
// 会调用虚表中的__finish函数,可能造成阻塞
_IO_FINISH (fp);
// 释放备份空间,不会造成阻塞
_IO_free_backup_area (fp);

这么一轮下来,基本可以确定就是虚表中的__close 或 __finish 函数造成了阻塞。下面我们再来分析一下文件指针中的虚表。

初识文件指针

  • C文件指针定义
    定义在libio/libio.h
struct _IO_FILE {
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;
  // 文件描述符,就是fd
  int _fileno;
  // 不知道干嘛的标志位
  int _flags2;
  
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  /*  char* _save_gptr;  char* _save_egptr; */

  // 文件锁
  _IO_lock_t *_lock;
};
  • 类C++风格文件指针定义
    定义在头文件libio/libioP.h

struct _IO_FILE_plus
{
  // C风格文件指针头部
  _IO_FILE file;
  // 虚表指针(很像c++中的函数表,但是两者并不相等)
  // 用整个结构体,存放一系列的函数指针,以实现类似C++中的多态
  const struct _IO_jump_t *vtable;
};

struct _IO_jump_t
{
  size_t __dummy
  size_t __dummy2
  // 下面这些字段都是函数类型,指向不同的函数对象代表执行不同的操作。
  // typedef void (*_IO_finish_t) (_IO_FILE *, int);
  _IO_finish_t __finish
  // typedef int (*_IO_overflow_t) (_IO_FILE *, int);
  _IO_overflow_t __overflow
  // typedef int (*_IO_underflow_t) (_IO_FILE *);
  _IO_underflow_t __underflow
  _IO_underflow_t __uflow
  // typedef int (*_IO_pbackfail_t) (_IO_FILE *, int);
  _IO_pbackfail_t __pbackfail
  // typedef _IO_size_t (*_IO_xsputn_t) (_IO_FILE *FP, const void *DATA, _IO_size_t N);
  _IO_xsputn_t __xsputn
  // typedef _IO_size_t (*_IO_xsgetn_t) (_IO_FILE *FP, void *DATA, _IO_size_t N);
  _IO_xsgetn_t __xsgetn
  // typedef _IO_off64_t (*_IO_seekoff_t) (_IO_FILE *FP, _IO_off64_t OFF, int DIR, int MODE);
  _IO_seekoff_t __seekoff
  // typedef _IO_off64_t (*_IO_seekpos_t) (_IO_FILE *, _IO_off64_t, int);
  _IO_seekpos_t __seekpos
  // typedef _IO_FILE* (*_IO_setbuf_t) (_IO_FILE *, char *, _IO_ssize_t);
  _IO_setbuf_t __setbuf
  // typedef int (*_IO_sync_t) (_IO_FILE *);
  _IO_sync_t __sync
  // typedef int (*_IO_doallocate_t) (_IO_FILE *);
  _IO_doallocate_t __doallocate
  // typedef _IO_ssize_t (*_IO_read_t) (_IO_FILE *, void *, _IO_ssize_t);
  _IO_read_t __read
  // typedef _IO_ssize_t (*_IO_write_t) (_IO_FILE *, const void *, _IO_ssize_t);
  _IO_write_t __write
  // typedef _IO_off64_t (*_IO_seek_t) (_IO_FILE *, _IO_off64_t, int);
  _IO_seek_t __seek
  // typedef int (*_IO_close_t) (_IO_FILE *);
  _IO_close_t __close
  // typedef int (*_IO_stat_t) (_IO_FILE *, void *);
  _IO_stat_t __stat
  // typedef int (*_IO_showmanyc_t) (_IO_FILE *);
  _IO_showmanyc_t __showmanyc
  // typedef void (*_IO_imbue_t) (_IO_FILE *, void *);
  _IO_imbue_t __imbue
};

glibc 中就是通过虚表中指向不同函数来实现类似虚函数那样的动作的。

popen与fopen的魔术

fopen的实现

fopen的实现定义在libio/iofopen.c函数中

_IO_FILE *
_IO_new_fopen (filename, mode)
     const char *filename;
     const char *mode;
{
  return __fopen_internal (filename, mode, 1);
}

_IO_FILE *
__fopen_internal (filename, mode, is32)
     const char *filename;
     const char *mode;
     int is32;
{
  // 定义结构体
  struct locked_FILE
  {
  	// 使用具有虚表的函数指针
    struct _IO_FILE_plus fp;
#ifdef _IO_MTSAFE_IO
    _IO_lock_t lock;
#endif
    struct _IO_wide_data wd;
  } *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));

  if (new_f == NULL)
    return NULL;
#ifdef _IO_MTSAFE_IO
  new_f->fp.file._lock = &new_f->lock;
#endif
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
  _IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
#else
  _IO_no_init (&new_f->fp.file, 1, 0, NULL, NULL);
#endif
  // 给虚表赋值
  // _IO_file_jumps 是一个全局变量,定义了文件操作相关的函数
  _IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
  _IO_file_init (&new_f->fp);
#if  !_IO_UNIFIED_JUMPTABLES
  new_f->fp.vtable = NULL;
#endif
  if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)
    return __fopen_maybe_mmap (&new_f->fp.file);

  _IO_un_link (&new_f->fp);
  free (new_f);
  return NULL;
}
// _IO_file_jumps 定义在libio/fileops.c中
const struct _IO_jump_t _IO_file_jumps =
{
  JUMP_INIT_DUMMY,
  _IO_file_finish,  // 先flush流,再调用_IO_file_close
  _IO_file_overflow,
  _IO_file_underflow,
  _IO_default_uflow,
  _IO_default_pbackfail,
  _IO_file_xsputn,
  _IO_file_xsgetn,
  _IO_new_file_seekoff,
  _IO_default_seekpos,
  _IO_new_file_setbuf,
  _IO_new_file_sync,
  _IO_file_doallocate,
  _IO_file_read,
  _IO_new_file_write,
  _IO_file_seek,
  _IO_file_close,  // 实际上调用的是更底层的close函数,不阻塞
  _IO_file_stat,
  _IO_default_showmanyc,
  _IO_default_imbue
};
  • 小结
  1. fopen的函数中,可以知道fopen实际上是一个非阻塞的函数,在创建文件指针的时候,会将非阻塞的finish函数和close函数填入到虚表中,因此fclose实际上是非阻塞的。

popen的实现

popend函数的定义在libio/iopopen.c中

_IO_FILE *
_IO_new_popen (command, mode)
     const char *command;
     const char *mode;
{
  // _IO_proc_file结构体并不定义在该函数内部,只是为了阅读方便
  // 
  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;
  };
  // locked_FILE 这个结构体是定义在函数内部的
  struct locked_FILE
  {
    struct _IO_proc_file fpx;
#ifdef _IO_MTSAFE_IO
    _IO_lock_t lock;
#endif
  } *new_f;
  _IO_FILE *fp;

  new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));
  if (new_f == NULL)
    return NULL;
#ifdef _IO_MTSAFE_IO
  new_f->fpx.file.file._lock = &new_f->lock;
#endif
  fp = &new_f->fpx.file.file;
  _IO_init (fp, 0);
  // 虚表的赋值在这里
  // _IO_proc_jumps 是一个全局变量定义在libio/iopopen.c中
  _IO_JUMPS (&new_f->fpx.file) = &_IO_proc_jumps;
  _IO_new_file_init (&new_f->fpx.file);
#if  !_IO_UNIFIED_JUMPTABLES
  new_f->fpx.file.vtable = NULL;
#endif
  // _IO_new_proc_open  在这里会执行fork操作,创建子进程执行管道任务
  if (_IO_new_proc_open (fp, command, mode) != NULL)
    return (_IO_FILE *) &new_f->fpx.file;
  _IO_un_link (&new_f->fpx.file);
  free (new_f);
  return NULL;
}

// _IO_proc_jumps  定义在libio/iopopen.c中
static const struct _IO_jump_t _IO_proc_jumps = {
  JUMP_INIT_DUMMY,
  _IO_new_file_finish,  // 先flush流,在执行_IO_new_proc_close函数
  _IO_new_file_overflow,
  _IO_new_file_underflow,
  _IO_default_uflow,
  _IO_default_pbackfail,
  _IO_new_file_xsputn,
  _IO_default_xsgetn,
  _IO_new_file_seekoff,
  _IO_default_seekpos,
  _IO_new_file_setbuf,
  _IO_new_file_sync,
  _IO_file_doallocate,
  _IO_file_read,
  _IO_new_file_write,
  _IO_file_seek,
  _IO_new_proc_close, // 执行wait_pid函数,会导致阻塞
  _IO_file_stat,
  _IO_default_showmanyc,
  _IO_default_imbue
};

int
_IO_new_proc_close (fp)
     _IO_FILE *fp;
{
  /* This is not name-space clean. FIXME! */
  int wstatus;
  _IO_proc_file **ptr = &proc_file_chain;
  _IO_pid_t wait_pid;
  int status = -1;

  /* Unlink from proc_file_chain. */
#ifdef _IO_MTSAFE_IO
  _IO_cleanup_region_start_noarg (unlock);
  _IO_lock_lock (proc_file_chain_lock);
#endif
  for ( ; *ptr != NULL; ptr = &(*ptr)->next)
    {
      if (*ptr == (_IO_proc_file *) fp)
	{
	  *ptr = (*ptr)->next;
	  status = 0;
	  break;
	}
    }
#ifdef _IO_MTSAFE_IO
  _IO_lock_unlock (proc_file_chain_lock);
  _IO_cleanup_region_end (0);
#endif

  if (status < 0 || _IO_close (_IO_fileno(fp)) < 0)
    return -1;
  /* POSIX.2 Rationale:  "Some historical implementations either block
     or ignore the signals SIGINT, SIGQUIT, and SIGHUP while waiting
     for the child process to terminate.  Since this behavior is not
     described in POSIX.2, such implementations are not conforming." */
  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;
}
  • 小结
  1. popen是非阻塞的函数,调用fork创建子进程生成管道,执行任务。在创建文件指针时会重新给虚表赋值
  2. 重新赋值的虚表中的__close函数被赋值为_IO_new_proc_close该函数会调用waitpid函数导致阻塞。

总结

管道的打开与关闭

  1. pclosefclose函数的底层实现是一样,所以调用fclose去关闭管道是不会出错的。
  2. popen返回文件指针在close时是阻塞的,但是fopen返回的则不会。
  3. popenfclose函数的对创建的文件指针赋予不同的虚表,导致了close时的差异。
  4. popen返回的文件指针中,其虚表中的close函数调用waitpid,是引起阻塞的根本原因。

猜你喜欢

转载自blog.csdn.net/SKY453589103/article/details/86603125