为什么此 pclose() 实现 return 早于 ECHILD,除非在 popen() 之后延迟调用?

Why does this pclose() implementation return early with ECHILD unless invocation is delayed after popen()?

我最近想了解如何 fork/exec 子进程并重定向标准输入、标准输出和标准错误,通过这种方式我编写了自己的 popen()pclose() 类函数命名为 my_popen()my_pclose(),灵感来自 Apple 的 open-source implementation of popen() and pclose().

通过人工检查 -- 例如运行ning ps 在不同的终端中寻找预期的子进程——popen() 似乎起作用,因为预期的子进程出现了。

问题:为什么在my_popen()后立即调用my_pclose()return会立即调用errno == 10 (ECHILD)?我的期望是 my_pclose() 会等到子进程结束。

问题: 鉴于上述,为什么 my_pclose() return 如预期的那样 - 在子进程正常结束后 - 如果我插入延迟在 my_popen()my_pclose() 之间?

问题:仅在子进程结束后,my_pclose() 需要什么更正 is/are 才能可靠地 return,而无需是否需要任何延迟或其他设计?


下面是 MCVE。

一些上下文:我希望 my_popen() 允许用户 1) 写入子进程'stdin,2) 读取子进程'stdout,3) 读取子进程' stderr, 4) 知道子进程' pid_t, 5) 运行 在 fork/exec 进程可能是子进程或孙进程的环境中,并且在后者的情况下能够杀死孙进程(因此 setpgid())。

// main.c

#include <errno.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

typedef int Pipe[2];

typedef enum PipeEnd {
  READ_END  = 0,
  WRITE_END = 1
} PipeEnd;

#define INVALID_FD (-1)
#define INVALID_PID (0)

typedef struct my_popen_t {
  bool  success;  ///< true if the child process was spawned.
  Pipe  stdin;    ///< parent -> stdin[WRITE_END] -> child's stdin
  Pipe  stdout;   ///< child -> stdout[WRITE_END] -> parent reads stdout[READ_END]
  Pipe  stderr;   ///< child -> stderr[WRITE_END] -> parent reads stderr[READ_END]
  pid_t pid;      ///< child process' pid
} my_popen_t;

/** dup2( p[pe] ) then close and invalidate both ends of p */
static void dupFd( Pipe p, const PipeEnd pe, const int fd ) {
  dup2( p[pe], fd);
  close( p[READ_END] );
  close( p[WRITE_END] );
  p[READ_END] = INVALID_FD;
  p[WRITE_END] = INVALID_FD;
}

/**
 * Redirect a parent-accessible pipe to the child's stdin, and redirect the
 * child's stdout and stderr to parent-accesible pipes.
 */
my_popen_t my_popen( const char* cmd ) {
  my_popen_t r = { false,
    { INVALID_FD, INVALID_FD },
    { INVALID_FD, INVALID_FD },
    { INVALID_FD, INVALID_FD },
    INVALID_PID };

  if ( -1 == pipe( r.stdin ) ) { goto end; }
  if ( -1 == pipe( r.stdout ) ) { goto end; }
  if ( -1 == pipe( r.stderr ) ) { goto end; }

  switch ( (r.pid = fork()) ) {
    case -1: // Error
      goto end;

    case 0: // Child process
      dupFd( r.stdin, READ_END, STDIN_FILENO );
      dupFd( r.stdout, WRITE_END, STDOUT_FILENO );
      dupFd( r.stderr, WRITE_END, STDERR_FILENO );
      setpgid( getpid(), getpid() );

      {
        char* argv[] = { (char*)"sh", (char*)"-c", (char*)cmd, NULL };

        // @todo Research why - as has been pointed out - _exit() should be
        // used here, not exit().
        if ( -1 == execvp( argv[0], argv ) ) { exit(0); }
      }
  }

  // Parent process
  close( r.stdin[READ_END] );
  r.stdin[READ_END] = INVALID_FD;
  close( r.stdout[WRITE_END] );
  r.stdout[WRITE_END] = INVALID_FD;
  close( r.stderr[WRITE_END] );
  r.stderr[WRITE_END] = INVALID_FD;
  r.success = true;

end:
  if ( ! r.success ) {
    if ( INVALID_FD != r.stdin[READ_END] ) { close( r.stdin[READ_END] ); }
    if ( INVALID_FD != r.stdin[WRITE_END] ) { close( r.stdin[WRITE_END] ); }
    if ( INVALID_FD != r.stdout[READ_END] ) { close( r.stdout[READ_END] ); }
    if ( INVALID_FD != r.stdout[WRITE_END] ) { close( r.stdout[WRITE_END] ); }
    if ( INVALID_FD != r.stderr[READ_END] ) { close( r.stderr[READ_END] ); }
    if ( INVALID_FD != r.stderr[WRITE_END] ) { close( r.stderr[WRITE_END] ); }

    r.stdin[READ_END] = r.stdin[WRITE_END] =
      r.stdout[READ_END] = r.stdout[WRITE_END] =
      r.stderr[READ_END] = r.stderr[WRITE_END] = INVALID_FD;
  }

  return r;
}

int my_pclose( my_popen_t* p ) {
  if ( ! p )                    { return -1; }
  if ( ! p->success )           { return -1; }
  if ( INVALID_PID == p->pid )  { return -1; }

  {
    pid_t pid = INVALID_PID;
    int wstatus;

    do {
      pid = waitpid( -1 * (p->pid), &wstatus, 0 );
    } while ( -1 == pid && EINTR == errno );

    return ( -1 == pid ? pid : wstatus );
  }
}

int main( int argc, char* argv[] ) {
  my_popen_t p = my_popen( "sleep 3" );
  //sleep( 1 ); // Uncomment this line for my_pclose() success.
  int res = my_pclose( &p );

  printf( "res: %d, errno: %d (%s)\n", res, errno, strerror( errno ) );

  return 0;
}

执行失败:

$ gcc --version && gcc -g ./main.c && ./a.out
gcc (Debian 6.3.0-18+deb9u1) 6.3.0 20170516
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

res: -1, errno: 10 (No child processes)

参考文献:1, ,


更新:
This link 让我想知道在 fork()ing 之后在父进程中添加 setpgid( pid, 0 ) 是否相关。它似乎确实有效,因为在添加之后,在 my_popen() 之后立即调用 my_pclose() 似乎要等到过程完成。

老实说,我不太明白为什么这会产生影响;如果知识渊博的社区成员可以提供见解,我将不胜感激。

my_popen_t my_popen( const char* cmd ) {
  my_popen_t r = { false,
    { INVALID_FD, INVALID_FD },
    { INVALID_FD, INVALID_FD },
    { INVALID_FD, INVALID_FD },
    INVALID_PID };

  if ( -1 == pipe( r.stdin ) ) { goto end; }
  if ( -1 == pipe( r.stdout ) ) { goto end; }
  if ( -1 == pipe( r.stderr ) ) { goto end; }

  switch ( (r.pid = fork()) ) {
    case -1: // Error
      goto end;

    case 0: // Child process
      dupFd( r.stdin, READ_END, STDIN_FILENO );
      dupFd( r.stdout, WRITE_END, STDOUT_FILENO );
      dupFd( r.stderr, WRITE_END, STDERR_FILENO );
      //setpgid( getpid(), getpid() ); // This looks unnecessary

      {
        char* argv[] = { (char*)"sh", (char*)"-c", (char*)cmd, NULL };

        // @todo Research why - as has been pointed out - _exit() should be
        // used here, not exit().
        if ( -1 == execvp( argv[0], argv ) ) { exit(0); }
      }
  }

  // Parent process
  setpgid( r.pid, 0 ); // This is the relevant change
  close( r.stdin[READ_END] );
  r.stdin[READ_END] = INVALID_FD;
  close( r.stdout[WRITE_END] );
  r.stdout[WRITE_END] = INVALID_FD;
  close( r.stderr[WRITE_END] );
  r.stderr[WRITE_END] = INVALID_FD;
  r.success = true;

end:
  if ( ! r.success ) {
    if ( INVALID_FD != r.stdin[READ_END] ) { close( r.stdin[READ_END] ); }
    if ( INVALID_FD != r.stdin[WRITE_END] ) { close( r.stdin[WRITE_END] ); }
    if ( INVALID_FD != r.stdout[READ_END] ) { close( r.stdout[READ_END] ); }
    if ( INVALID_FD != r.stdout[WRITE_END] ) { close( r.stdout[WRITE_END] ); }
    if ( INVALID_FD != r.stderr[READ_END] ) { close( r.stderr[READ_END] ); }
    if ( INVALID_FD != r.stderr[WRITE_END] ) { close( r.stderr[WRITE_END] ); }

    r.stdin[READ_END] = r.stdin[WRITE_END] =
      r.stdout[READ_END] = r.stdout[WRITE_END] =
      r.stderr[READ_END] = r.stderr[WRITE_END] = INVALID_FD;
  }

  return r;
}

您的 my_pclose() 的问题是您试图执行 process-group 等待而不是等待特定的子进程。这个:

      pid = waitpid( -1 * (p->pid), &wstatus, 0 );

尝试等待属于进程组 p->pid 的子进程,但如果没有您稍后添加的 setpgid() 调用,这极不可能工作。分叉的子进程最初将与其父进程位于同一进程组中,并且该组的进程组号几乎肯定会与子进程号不同。

此外,不清楚您为什么首先尝试等待进程组。您知道要等待的具体进程,my_pclose() 收集不同的进程是不正确的,无论它是否属于同一进程组。您应该等待该特定过程:

      pid = waitpid(p->pid, &wstatus, 0 );

无论有没有 setpgid() 调用,它都可以工作,但几乎可以肯定的是,您应该在 general-purpose 函数中省略该调用,例如这个。