shell 当Bash作为PID 1运行时,为什么前台作业忽略作业控制信号?

omvjsjqw  于 2022-11-16  发布在  Shell
关注(0)|答案(1)|浏览(144)

bash作为pid 1直接通过内核选项init=/bin/bash --login调用时,它将在提示之前发出类似如下的消息:

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

并且键盘生成的信号(如^Z、^C、^\)均不起作用。
为了解决这个问题,我写了一个简单的程序init1.c如下:

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

将其编译为init1,然后将其调用为pid 1(即Bash运行为pid 1),前面的错误消息消失,一些信号(例如^C、^\)工作,但作业控制信号(例如^Z)仍然不工作(意外)。
因此,为了使作业控制信号正常工作,我将上面的代码修改为init2.c(只是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);
}

将其编译为init2并调用为pid 1(即最终Bash运行为除1以外的任意PID),这一次,作业控制信号工作!
但是我不明白为什么作业控制信号在init2(Bash不是pid 1)中工作,而在init1(Bash是pid 1)中不工作,为什么当Bash以PID 1运行时前台作业忽略作业控制信号?似乎pid 1有一些特殊之处。

2022年3月21日更新:

最近,我在github中发现了一个非常简单的shell mysh,它也实现了作业控制,只有949行!当我用init1init2运行它时,这个shell也有同样的问题!(多亏了它,我不需要阅读复杂的bash源代码来解决我的问题。Orz)问题在于waitpid(),它不当SIGTSTP(^Z)到达时,不能立即返回。所以这个问题不仅与bash有关,而且与实现作业控制的shell有关。但是,我不明白为什么当shell以PID 1运行时,如果SIGTSTP到达,waitpid()不返回...... 囧

axzmvihb

axzmvihb1#

在linux中,进程被赋予了默认的信号处理程序,各种各样的信号(比如SIGTERMSIGINT)都有立即退出的默认行为。
由于历史和系统的原因,pid 1没有定义这些默认的信号处理程序,因此没有任何行为。请注意,这并不妨碍您自己重新定义信号处理程序。
linux kernel man pages
只有“init”进程已经建立了信号处理程序的信号才能被PID名称空间的其他成员发送到“init”进程。这个限制甚至适用于特权进程,并防止PID名称空间的其他成员意外地杀死“init”进程。
类似地,祖先命名空间中的进程可以-服从kill(2)中描述的常规权限检查-仅当“init”进程已经为该信号建立了处理程序时,才向子PID命名空间的“init”进程发送信号。(在处理程序内,sigaction(2)中描述的siginfo_t si_pid字段将为零。)SIGKILL或SIGSTOP被例外地处理:当从祖先PID名字空间发送时,这些信号被强制传递。这些信号都不能被“init”进程捕获,因此将导致与这些信号相关联的通常动作(分别是终止和停止进程)。

相关问题