问题描述
我最近想弄清楚如何派生/执行子进程并重定向其stdin,stdout和stderr,以此编写自己的popen()
和pclose()
受到open-source implementation of popen() and pclose()启发的类似函数my_popen()
和my_pclose()
。
通过人工检查-例如在另一个终端中运行ps
来查找期望的子进程-popen()
似乎可以正常工作,因为期望的子进程出现了。
问题:如果我在my_pclose()
之后立即调用errno == 10 (ECHILD)
,为什么my_popen()
立即返回?我的期望是my_pclose()
会等到子进程结束。
问题:鉴于上述情况,为什么my_pclose()
会按预期方式返回-在子进程正常结束之后-如果我在my_popen()
和{{之间插入延迟, 1}}?
问题:my_pclose()
需要进行哪些更正才能仅在子进程结束后才可靠地返回,而无需任何延迟或其他麻烦?>
下面的MCVE。
某些情况:我希望my_pclose()
允许用户1)写入子进程“ my_popen()
,2)读取子进程” stdin
,3)读取子进程进程'stdout
,4)知道子进程'stderr
,5)在fork / exec'ed进程可能是子进程或子进程的环境中运行,并且能够杀死其中的子进程后者的情况(因此pid_t
)。
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_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,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;
}
更新:
This link让我想知道$ 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)
之后在父进程中添加setpgid( pid,0 )
是否相关。这样做似乎是可行的,因为添加后,在fork()
之后立即调用my_pclose()
似乎要等到该过程完成。
老实说,我不太明白为什么会有所不同。如果一个知识渊博的社区成员能够提供见解,我将不胜感激。
my_popen()
解决方法
my_pclose()
的问题在于您试图执行进程组等待而不是等待特定的子进程。这个:
pid = waitpid( -1 * (p->pid),&wstatus,0 );
试图等待属于进程组p->pid
的孩子,但是如果没有您以后添加的setpgid()
调用,这是极不可能的。分叉的子代最初将与其父代位于同一个进程组中,并且该组的进程组号几乎可以肯定与该子进程的进程号不同。
此外,目前尚不清楚为什么首先要等待流程组。您知道要等待的特定进程,而my_pclose()
收集另一个进程是不正确的,无论它是否属于同一进程组。您应该等待该特定过程:
pid = waitpid(p->pid,0 );
无论有没有setpgid()
调用都可以使用,但是几乎可以肯定,您应该在这样的通用函数中忽略该调用。