Why does foreground job ignore job control signals when Bash is running as PID 1?

1.2k Views Asked by At

When bash is invoked as pid 1 directly through the kernel option init=/bin/bash --login, it will issue something like this before prompting:

bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell

And no keyboard-generated signals (e.g ^Z, ^C, ^\) work.

To solve this problem, I wrote a simple program init1.c as following:

/* init1.c */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>

int main(int argc, char **argv)
{
  char *options[] = {"--login", NULL};
  int tty_fd = -1;

  printf("\n----- Bash Init 1 -----\n\n");

  /* Make bash as session leader. */
  if (setsid() == -1)
    {
      fprintf(stderr, "%s : %d : %s\n", "setsid()", __LINE__, strerror(errno));
      exit(EXIT_FAILURE);
    }

  /* Make /dev/tty1 as controlling terminal of Bash. */
  tty_fd = open("/dev/tty1", O_RDWR);
  if (tty_fd == -1)
    {
      fprintf(stderr, "%s : %d : %s\n", "open()", __LINE__, strerror(errno));
      exit(EXIT_FAILURE);
    }

  /* Re-connect stdin, stdout, stderr to the controlling terminal. */
  dup2(tty_fd, STDIN_FILENO);
  dup2(tty_fd, STDOUT_FILENO);
  dup2(tty_fd, STDERR_FILENO);
  close(tty_fd);
  
  execv("/bin/bash", options);
}

Compiled it as init1, then invoked it as pid 1 (i.e Bash running as pid 1), the preceding error messages disappear and some signals (e.g ^C, ^\) work, but job control signals (e.g ^Z) still not (unexpected).

So to make job control signals work, I revised the code above as init2.c (just fork()):

/* init2.c */
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>

int main(int argc, char **argv)
{
  char *options[] = {"--login", NULL};
  pid_t pid = -1;
  int tty_fd = -1;
  
  printf("\n----- Bash Init 2 -----\n\n");
  
  pid = fork();
  if (pid < 0)
    {
      fprintf(stderr, "%s : %d : %s\n", "fork()", __LINE__, strerror(errno));
      exit(EXIT_FAILURE);
    }
    
  /* Parent. */
  if (pid > 0)
   {
     /* Wait for its child, otherwise all processes would be killed ! */
     while (wait(NULL) > 0)
       ;
     exit(EXIT_SUCCESS);
   }
      
  /* Child. */
  if (setsid() == -1)
    {
      fprintf(stderr, "%s : %d : %s\n", "setsid()", __LINE__, strerror(errno));
      exit(EXIT_FAILURE);
    }        
  
  /* Make /dev/tty1 as controlling terminal of Bash. */
  tty_fd = open("/dev/tty1", O_RDWR);
  if (tty_fd == -1)
    {
      fprintf(stderr, "%s : %d : %s\n", "open()", __LINE__, strerror(errno));
      exit(EXIT_FAILURE);
    }

  /* Re-connect stdin, stdout, stderr to the controlling terminal. */
  dup2(tty_fd, STDIN_FILENO);
  dup2(tty_fd, STDOUT_FILENO);
  dup2(tty_fd, STDERR_FILENO);
  close(tty_fd);
  
  execv("/bin/bash", options);
}

Compiled it as init2 and invoked as pid 1 (i.e. finally Bash running as arbitrary PID other than 1), and this time, the job control signals work!

But I didn't figure out why the job control signals work in init2 (Bash isn't pid 1) but not init1 (Bash is pid 1), why does foreground job ignore job control signals when Bash is running as PID 1? It seems that there is something special with pid 1.



Update 3/21/2022:

Recently, I found a very simple shell mysh in github which also implements job control, only 949 lines! When I ran it with init1 and init2, this shell also has the same problem! (Thanks to it, I don't have to read the complicated bash source code for figuring out my question. Orz) And the problem lies in waitpid() which doesn't return immediately when SIGTSTP(^Z) reaches. So this issue is not only relative to bash, but also the shells that implement job control. However, I don't understand why does't waitpid() return if SIGTSTP reaches when shell is running as PID 1... 囧

1

There are 1 best solutions below

1
On

In linux, processes are given default signal handlers. A variety of signals (like SIGTERM and SIGINT), have the default behavior of immediately exiting.

For historical and system reasons, pid 1 just does not get these default signal handlers defined, so there's no behavior there. Note that this doesn't stop you from redefining signal handlers yourself.

From the linux kernel man pages

Only signals for which the "init" process has established a signal handler can be sent to the "init" process by other members of the PID namespace. This restriction applies even to privileged processes, and prevents other members of the PID namespace from accidentally killing the "init" process.

Likewise, a process in an ancestor namespace can—subject to the usual permission checks described in kill(2)—send signals to the "init" process of a child PID namespace only if the "init" process has established a handler for that signal. (Within the handler, the siginfo_t si_pid field described in sigaction(2) will be zero.) SIGKILL or SIGSTOP are treated exceptionally: these signals are forcibly delivered when sent from an ancestor PID namespace. Neither of these signals can be caught by the "init" process, and so will result in the usual actions associated with those signals (respectively, terminating and stopping the process).