为什么此 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 函数中省略该调用,例如这个。
我最近想了解如何 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 函数中省略该调用,例如这个。