IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    作业控制与前台进程组

    hongjiang发表于 2016-11-01 21:06:19
    love 0

    这篇文章是对之前的SIGTTIN信号量的疑惑?的解答,对于为何会有这种奇怪的用法,在另一篇shell下精确的定位一个命令 也介绍过了,这里想讨论的重点不在于怎么变通解决那个问题,而是导致SIGTTIN发生的机制是怎么引起的。我的同事对这个问题也产生了好奇,在stackoverflow上发帖,有人给出了解释,解答的人直接给出了bash的源码jobs.c里的initialize_job_control方法片段,指出SIGTTIN正是那里面的逻辑。不过如果你跟我一样对shell和linux系统调用都懂得很肤浅的话,这段代码并不容易懂,所以在这里更详细的解释一下这个问题的来龙去脉。

    刚开始碰到这个问题的时候,通过strace看到了是SIGTTIN信号量所致,因为这个信号量默认的行为是让进程STOP(暂停),即通过ps观察到的状态为T。对于SIGTTIN信号量《Linux/UNIX系统编程手册》上是这么说的:

    只有前台作业中的进程才能够从控制终端读取输入。这个限制条件避免了多个作业竞争读取终端输入。如果后台作业尝试从终端读取输入,就会接收到一个SIGTTIN信号。SIGTTIN信号的默认处理动作是停止作业。

    但我们的脚本里并没有后台进程,那两个进程也没有读取终端,跟上面的解释对不上。也没有在网上搜到其它引发SIGTTIN信号的情况,在这里困惑了很久。不过凭直觉知道这个问题应该跟作业控制有关,在脚本里显式的开启作业控制,是能够正常运行的:

    $ cat sleep.sh
    #!/bin/bash
    set -m
    bash -ic 'sleep 3'
    bash -ic 'sleep 2'

    所以一定是在进程某个状态上的不一致导致的。上周末的时候阅读了一下strace的log,对出问题的脚本:

    #!/bin/bash
    bash -ic 'sleep 3'
    bash -ic 'sleep 2'

    使用strace -f -e verbose=all -t ./sleep.sh 2>log 得到更详细的日志

    ...
    03:39:06 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f997f03ca10) = 9897
    ...
    [pid  9897] 03:39:06 execve("/usr/bin/bash", ["bash", "-ic", "sleep 3"], [/* 30 vars */]) = 0
    ...
    03:39:09 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f997f03ca10) = 9922
    ...
    [pid  9922] 03:39:09 execve("/usr/bin/bash", ["bash", "-ic", "sleep 2"], [/* 30 vars */]) = 0
    ...
    [pid  9922] 03:39:09 access("/usr/bin/bash", R_OK) = 0
    [pid  9922] 03:39:09 getpgrp()          = 9891
    [pid  9922] 03:39:09 ioctl(2, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0x7ffd356e49c0) = -1 ENOTTY (Inappropriate ioctl for device)
    [pid  9922] 03:39:09 open("/dev/tty", O_RDWR|O_NONBLOCK) = 3
    [pid  9922] 03:39:09 getrlimit(RLIMIT_NOFILE, {rlim_cur=1024, rlim_max=4*1024}) = 0
    [pid  9922] 03:39:09 fcntl(255, F_GETFD) = -1 EBADF (Bad file descriptor)
    [pid  9922] 03:39:09 dup2(3, 255)       = 255
    [pid  9922] 03:39:09 close(3)           = 0
    [pid  9922] 03:39:09 ioctl(255, TIOCGPGRP, [9897]) = 0
    [pid  9922] 03:39:09 rt_sigaction(SIGTTIN, {SIG_DFL, [], SA_RESTORER, 0x7f912a22b650}, {SIG_IGN, [], SA_RESTORER, 0x7f912a22b650}, 8) = 0
    [pid  9922] 03:39:09 kill(0, SIGTTIN)   = 0
    [pid  9896] 03:39:09 <... wait4 resumed> 0x7ffc5b5e6800, 0, NULL) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
    [pid  9922] 03:39:09 --- SIGTTIN {si_signo=SIGTTIN, si_code=SI_USER, si_pid=9922, si_uid=1000} ---
    [pid  9896] 03:39:09 --- SIGTTIN {si_signo=SIGTTIN, si_code=SI_USER, si_pid=9922, si_uid=1000} ---
    [pid  9922] 03:39:09 --- stopped by SIGTTIN ---
    [pid  9896] 03:39:09 --- stopped by SIGTTIN ---

    确认这个SIGTTIN信号是第二个bash -ic 'sleep 2'进程发出的,kill(0, SIGTTIN)表示它把这个信号发送到自己所在的进程组,整个进程组的进程都接收到这个信号,所以它和它的父进程sleep.sh都变成了stop状态。

    结合jobs.c的initialize_job_control方法里的代码:

    /* We can only have job control if we are interactive. */
    if (interactive == 0)
    {
      job_control = 0;
      original_pgrp = NO_PID;
      shell_tty = fileno (stderr);
    }
    else
    {
      shell_tty = -1;
    
      /* If forced_interactive is set, we skip the normal check that stderr
        is attached to a tty, so we need to check here.  If it's not, we
        need to see whether we have a controlling tty by opening /dev/tty,
        since trying to use job control tty pgrp manipulations on a non-tty
        is going to fail. */
      if (forced_interactive && isatty (fileno (stderr)) == 0)
        shell_tty = open ("/dev/tty", O_RDWR|O_NONBLOCK);
    
      /* Get our controlling terminal.  If job_control is set, or
        interactive is set, then this is an interactive shell no
         matter where fd 2 is directed. */
       if (shell_tty == -1)
        shell_tty = dup (fileno (stderr));/* fd 2 */
    
      shell_tty = move_to_high_fd (shell_tty, 1, -1);
    
      /* Compensate for a bug in systems that compiled the BSD
     rlogind with DEBUG defined, like NeXT and Alliant. */
      if (shell_pgrp == 0)
    {
      shell_pgrp = getpid ();
      setpgid (0, shell_pgrp);
      tcsetpgrp (shell_tty, shell_pgrp);
    }
    
      while ((terminal_pgrp = tcgetpgrp (shell_tty)) != -1)
    {
      if (shell_pgrp != terminal_pgrp)
        {
          SigHandler *ottin;
    
          ottin = set_signal_handler(SIGTTIN, SIG_DFL);
          kill (0, SIGTTIN);
          set_signal_handler (SIGTTIN, ottin);
          continue;
        }
      break;
    }
    ...

    关键点就在于shell_pgrp和terminal_pgrp这两个变量,shell_pgrp是当前进程组,而terminal_pgrp是占用当前控制终端的进程所在的进程组(前台进程组),这些状态都是可以通过ps观察到的,可以跟踪一下:

    $ ps xfo pid,ppid,pgid,sid,tpgid,stat,tty,command | awk "NR==1||/pts\/2/"
      PID  PPID  PGID   SID TPGID STAT TT       COMMAND
    12413 12410 12410 12410    -1 S    ?        sshd: hongjiang@pts/0
    12414 12413 12414 12414 12580 Ss   pts/0     \_ -bash
    12579 12414 12579 12414 12580 S    pts/0         \_ /bin/bash ./sleep.sh
    12580 12579 12580 12414 12580 S+   pts/0             \_ sleep 3
    
    $ ps xfo pid,ppid,pgid,sid,tpgid,stat,tty,command | awk "NR==1||/pts\/2/"
      PID  PPID  PGID   SID TPGID STAT TT       COMMAND
    12413 12410 12410 12410    -1 S    ?        sshd: hongjiang@pts/0
    12414 12413 12414 12414 12414 Ss+  pts/0     \_ -bash
    12579 12414 12579 12414 12414 T    pts/0         \_ /bin/bash ./sleep.sh
    12607 12579 12579 12414 12414 T    pts/0             \_ bash -ic sleep 2

    在第一次执行bash -ic 'sleep 3'的时候,sleep.sh父进程先clone出bash子进程(pid 12580),因为-i参数强制这个bash子进程用交互式运行,它会加载$HOME下的.bashrc等文件,这个过程可能会fork/clone出若干子进程(所以会看到第二次bash -ic sleep 2进程的ID跟第一次不是连续的),等这些配置文件加载完之后,它并不是fork/clone的形式执行sleep 3而是使用当前进程(12580)执行的sleep 3,这里很关键的信息是"PGID"和"TPGID"都是本身进程ID,而非父进程ID,跟第二次的状态不一样。

    因为脚本默认是关闭作业控制的,本来每个子进程并不会设置为独立的进程组,比如下面这个脚本:

    $ cat a.sh
    #!/bin/bash
    /usr/bin/sleep 10
    
    $ ps xfo pid,ppid,pgid,sid,tpgid,stat,tty,command | awk "NR==1||/pts\/2/"
      PID  PPID  PGID   SID TPGID STAT TT       COMMAND
    12668 12665 12665 12665    -1 S    ?        sshd: hongjiang@pts/2
    12669 12668 12669 12669 12736 Ss   pts/2     \_ -bash
    12736 12669 12736 12669 12736 S+   pts/2         \_ /bin/bash ./a.sh
    12737 12736 12736 12669 12736 S+   pts/2             \_ /usr/bin/sleep 10

    上面脚本执行时sleep子进程"PGID"和"TPGID"都是进程父进程a.sh的,并没有被设置为一个独立的进程组。也就是说因为"-i"参数使得bash -ic 'sleep 3'在非交互式脚本里运行时进程被设置成了独立的进程组,所以"TPGID"这个表示前台进程组的状态也被改为了bash -ic 'sleep 3'的进程组ID。

    那为什么在接下来的bash -ic 'sleep 2'子进程执行时却不像前面的那样呢?这正是最诡异的地方。它们所在的sleep.sh脚本是非交互式运行的,在每个子进程结束的时候,"TPGID"变量并不做修改,即它仍是上个进程bash -ic 'sleep 3'修改过的值。而bash -ic 'sleep 2'子进程也因为"-i"参数让自己以交互式运行,结果正好触发了jobs.c里的:

    if (shell_pgrp != terminal_pgrp)
    {
        SigHandler *ottin;
    
        ottin = set_signal_handler(SIGTTIN, SIG_DFL);
        kill (0, SIGTTIN);
        set_signal_handler (SIGTTIN, ottin);
        continue;
    }

    因为这段代码会认为终端被其他前台进程占用,对当前进程组发出SIGTTIN信号。在这个场景里,这恰好是一种误会!

    当我们显式的对sleep.sh脚本设置开启作业控制时:

    $ cat sleep.sh
    #!/bin/bash
    set -m
    bash -ic 'sleep 3'
    bash -ic 'sleep 2'
    
    $ ps xfo pid,ppid,pgid,sid,tpgid,stat,tty,command | awk "NR==1||/pts\/2/"
      PID  PPID  PGID   SID TPGID STAT TT       COMMAND
    12668 12665 12665 12665    -1 S    ?        sshd: hongjiang@pts/2
    12669 12668 12669 12669 12874 Ss   pts/2     \_ -bash
    12873 12669 12873 12669 12874 S    pts/2         \_ /bin/bash ./sleep.sh
    12874 12873 12874 12669 12874 S+   pts/2             \_ sleep 3
    
    $ ps xfo pid,ppid,pgid,sid,tpgid,stat,tty,command | awk "NR==1||/pts\/2/"
      PID  PPID  PGID   SID TPGID STAT TT       COMMAND
    12668 12665 12665 12665    -1 S    ?        sshd: hongjiang@pts/2
    12669 12668 12669 12669 12901 Ss   pts/2     \_ -bash
    12873 12669 12873 12669 12901 S    pts/2         \_ /bin/bash ./sleep.sh
    12901 12873 12901 12669 12901 S+   pts/2             \_ sleep 2

    它对每个子进程都设置为独立的进程组,并在每个进程(前台)结束的时候更新"TPGID"为父进程组ID,避免了jobs.c里发送SIGTTIN的逻辑。

    有很多shell的问题都是跟作业控制相关的,另一个例子参考tomcat进程意外退出的问题分析;作业控制可以玩出很多高阶花样,但它也大大增加了shell的复杂度,这个例子是一个典型的反面教材,最好不要在非交互式脚本里调用bash -ic来执行命令。



沪ICP备19023445号-2号
友情链接