Search code examples
clinuxstdin

Why is STDIN not propagated to child process of different process group?


Below is the source of a program that executes cat:

#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main()
{
  pid_t pid = fork();
  if (!pid) {
    // create a new process group
    setpgid(0, 0);

    execlp("cat", "cat", NULL);
  }

  pid_t reaped = wait(NULL);
  printf("reaped PID = %d\n", (int) reaped);
  return 0;
}

Note the call to setpgid(0, 0).

When run in a shell (either sh or bash), I expected:

  1. the parent to spawn a child process of cat, and
  2. the child to start interacting with the terminal.

However, what happens is:

  1. the parent spawns cat with no problem, but
  2. the child is stopped (with process state code T in ps), and
  3. the child accepts no input from the terminal, and
  4. the child does not respond to any of SIGINT, SIGSTOP or SIGQUIT, only to be killed by SIGKILL.

When the call to setpgid() is commented out, everything is as expected.

I suspect the behavior is caused by:

  • the child cat is trying to read stdin and is waiting (thus stopped), but
  • the input to the terminal is first passed to bash, and then to the program above, however not to the cat, possibly because bash does not recognize its grandchildren, cat, because of its different process group.

Of course, removing setpgid() call is the simplest solution. Unfortunately there are some reasons; mainly to intercept some signals (such as SIGINT or SIGSTOP) in the parent. In other words, a <Ctrl-C> should not kill the cat but somehow signal the program above. (There are no signal handlers in the program above, yes, for illustrative purpose.)

I'd like to ask:

  1. Is this correct?
  2. Whether it is or not, how can I have cat to receive inputs from stdin?

Solution

  • As suggested in the comments, the foreground process group (which assumes all STDIN) can be changed via tcsetpgrp().

    The function may as well be called from the child. Otherwise the parent will have to wait for the child to do a successful setpgid() call and a concurrency issue will happen.

    However, as described in this SO question, when the child (which is not yet foreground) call tcsetpgrp, it will get a signal of SIGTTOU, according to the manual of tcsetpgrp. The default action for SIGTTOU is to stop the process, and this should be manually ignored.

    #include <signal.h>
    #include <unistd.h>
    #include <sys/wait.h>
    #include <stdio.h>
    
    int main()
    {
      // ignore SIGTTOU
      signal(SIGTTOU, SIG_IGN);
    
      pid_t pid = fork();
      if (!pid) {
        // create a new process group
        setpgid(0, 0);
        tcsetpgrp(STDIN_FILENO, getpgid(0));
    
        execlp("cat", "cat", NULL);
      }
    
      pid_t reaped = wait(NULL);
      printf("reaped PID = %d\n", (int) reaped);
      return 0;
    }
    

    Now the underlying cat starts to interact with the terminal, and the problem is solved.

    uuu@hhh:~$ ./a
    sajkfla
    sajkfla
    wjkelfaw
    wjkelfaw
    reaped PID = 774
    uuu@hhh:~$ ./a