【问题标题】:Is there a way to determine the "grandchild" pid_t of a process spawned with "sh -c"?有没有办法确定用“sh -c”生成的进程的“孙子”pid_t?
【发布时间】:2020-10-04 01:41:23
【问题描述】:

这个问题来自thisthis

为了加深我对生成进程和重定向管道的理解,我编写了类似popen 的函数popen2() -- 下面 -- 它返回生成的子进程的pid_t

注意:popen2() 的实现通过 execing sh -c cmd 而不仅仅是 cmd 生成子进程,因为在第二个链接问题中解释了支持这种方法。

底部的代码不是很长,但切入正题:a.out 生成 child.outps aux | grep child 以在打印出它认为是 @987654334 之前获得子进程统计信息的视觉确认@ 的 pid。

the second linked question 的评论者指出,通过 sh -c 产生的进程最终可能是 childgrandchild 进程,具体取决于 sh 是什么。
我无意中通过观察在我的主机上验证了这一点——sh 解析为/bin/bash——运行a.out 表明child.out 作为子进程运行:

$ g++ --version && gcc -Wall -Wextra -pedantic -Werror ./main.c && ./a.out
g++ (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.

p2 stdout:
user     3004534  0.0  0.0   4028   732 pts/14   S+   17:51   0:00 ./child.out
user     3004535  0.0  0.0  11176  2932 pts/14   S+   17:51   0:00 sh -c ps aux | grep child
user     3004537  0.0  0.0  12780   968 pts/14   S+   17:51   0:00 grep child

p.pid[3004534]

...而在同一主机上的 docker 容器中——sh 解析为 /bin/dash——运行 a.out 表明 child.out 作为孙进程运行:

Step 63/63 : RUN ./a.out
 ---> Running in 7a355740577b
p2 stdout:
root           7  0.0  0.0   2384   760 ?        S    00:55   0:00 sh -c ./child.out
root           8  0.0  0.0   2384   760 ?        S    00:55   0:00 sh -c ps aux | grep child
root           9  0.0  0.0   2132   680 ?        S    00:55   0:00 ./child.out
root          11  0.0  0.0   3080   880 ?        S    00:55   0:00 grep child

p.pid[7]

我的问题是:在a.out 的代码中,有没有办法以抽象实际命令是否为子项的方式获取已执行命令的pid_t > 进程”还是孙子进程?

给出一些上下文:我希望能够杀死child.out。通过观察,在我的popen2() 产生子进程和孙子进程的环境中,向 child 进程发送 SIGTERM 只会杀死 child 进程,即 sh -c child.out 但是不是 grandchild 进程,即child.out,这是我真正想要杀死的。


代码:

// main.c
#include <pthread.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

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

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

typedef int Pipe[2];

/** Encapsulates information about a created child process. */
typedef struct popen2_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
} popen2_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.
 */
popen2_t popen2( const char* cmd ) {
  popen2_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 );

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

        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 main( int argc, char* argv[] ) {
  (void)argc;
  (void)argv;
  popen2_t p = popen2( "./child.out" );
  int status = 0;

  {
    char buf[4096] = { '\0' };
    popen2_t p2 = popen2( "ps aux | grep child" );
    waitpid( p2.pid, &status, 0 );

    read( p2.stdout[READ_END], buf, sizeof buf );
    printf( "p2 stdout:\n%s\n", buf );
  }

  printf( "p.pid[%d]\n", p.pid );

  {
    pid_t wpid = waitpid( p.pid, &status, 0 );

    return wpid == p.pid && WIFEXITED( status ) ? WEXITSTATUS( status ) : -1;
  }
}
// child.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main( int argc, char* argv[] ) {
  char buf[128] = { '\0' };

  snprintf( buf, sizeof buf, "%s:%d\n", __FILE__, __LINE__ );
  write( STDOUT_FILENO, buf, strlen( buf ) );
  sleep( 1 );

  snprintf( buf, sizeof buf, "%s:%d\n", __FILE__, __LINE__ );
  write( STDOUT_FILENO, buf, strlen( buf ) );
  sleep( 1 );

  snprintf( buf, sizeof buf, "%s:%d\n", __FILE__, __LINE__ );
  write( STDOUT_FILENO, buf, strlen( buf ) );
  sleep( 1 );

  snprintf( buf, sizeof buf, "%s:%d\n", __FILE__, __LINE__ );
  write( STDOUT_FILENO, buf, strlen( buf ) );
  sleep( 1 );
  return 0;
}

【问题讨论】:

  • shell 运行的命令也可以是你的孙子。如果你想杀死在 shell 中运行的任何东西,你应该使用一个进程组。但这无论如何都是没有意义的:没有popen2() 有一个很好的理由:因为没有明显的方法来解决缓冲问题,而且除了一组选定的命令之外,它会导致死锁。而且您的代码还有很多其他问题(例如在分叉的孩子中使用 exit() 而不是 _exit() ,假设 waitpid 不会被中断等)。
  • @user414777 - 老兄!你让我得到了我所需要的:设置子进程的 pgid,然后向 进程组 发送信号。我从研究中学到了很多东西,你的评论启发了我去研究。谢谢。

标签: c linux process popen child-process


【解决方案1】:

这比我的工资等级略高,但由于没有任何其他答案,我将发布我最终所做的事情,这是基于 user414777 的评论,并且似乎有效。

我的做法不是获取孙进程的pid_t,而是将子进程设置为进程组组长。通过这样做,如果我向进程组 (killpg()) 发送信号,则会产生信号到达孙进程的效果。这反映在下面添加的setpgid() 中。

popen2_t popen2( const char* cmd ) {
  popen2_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 is the relevant change

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

        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;
}

【讨论】:

    猜你喜欢
    • 2021-11-13
    • 2020-09-08
    • 2015-01-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-14
    • 1970-01-01
    • 2010-12-10
    相关资源
    最近更新 更多