0

This question follows from this and this.

To bolster my understanding of spawning processes and redirecting pipes, I've written popen-like function popen2() -- below -- that returns the pid_t of the spawned child process.

Note: the implementation of popen2() spawns the child process by execing sh -c cmd instead of just cmd because of the explanations in favor of this approach at the second linked question.

The code at bottom is not terribly long, but to cut to the chase: a.out spawns child.out as well as ps aux | grep child to get visual confirmation of child processes' stats before printing out what it thinks is child.out's pid.

A commenter at the second linked question pointed out that processes spawned via sh -c may end up being either child or grandchild processes, depending on what sh is.
I unintentionally verified this by observing that on my host -- where sh resolves to /bin/bash -- running a.out shows that child.out is run as a child process:

$ 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]

...whereas in a docker container on the same host -- where sh resolves to /bin/dash -- running a.out shows that child.out is run as a grandchild process:

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]

My question is: in a.out's code, is there a way to get the pid_t of the executed command in a way that abstracts whether the actual command is a child process' or a grandchild process?

To give some context: I want to be able to kill child.out. By observation, in the environment where my popen2() spawns child and grandchild processes, sending the child process a SIGTERM kills only the child process, i.e. sh -c child.out but not the grandchild process, i.e. child.out, which is what I really want to kill.


The code:

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

StoneThrow
  • 5,314
  • 4
  • 44
  • 86
  • 1
    The command run by the shell also can be your grand-grandchild. If you want to be able kill whatever stuff runs in the shell, you should use a *process group*. But that's pointless anyway: there's a very good reason why there's no `popen2()`: because there's no obvious way to fix the buffering issue, and except for a selected set of commands, it will result in a deadlock. And your code has a lot of other problems (as using exit() instead of _exit() in a forked child, assuming that waitpid won't be interrupted, etc). –  Oct 04 '20 at 01:56
  • @user414777 - dude! You led me to exactly what I needed: setting the pgid of the child process, then sending a signal to the _process group_. I learned a bunch from the research your comment inspired me to look into. Thank you. – StoneThrow Oct 04 '20 at 03:25

1 Answers1

0

This is a little above my paygrade, but since there haven't been any other answers, I'll post what I ended up doing, which is based on user414777's comment, and appears to work.

My approach was not to get the pid_t of the grandchild process, but to set the child process to be a process group leader. By doing so, if I send a signal to the process group (killpg()), that has the effect of the signal reaching the grandchild process. This is reflected in the addition of setpgid() below.

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;
}
StoneThrow
  • 5,314
  • 4
  • 44
  • 86