为什么 SIGINT 不能在脚本中的后台进程上工作?

为什么 SIGINT 不能在脚本中的后台进程上工作?

我在脚本中有以下内容:

yes >/dev/null &
pid=$!
echo $pid
sleep 2
kill -INT $pid
sleep 2
ps aux | grep yes

当我运行它时,输出显示yes脚本结束时仍在运行。但是,如果我以交互方式运行命令,则进程将成功终止,如下所示:

> yes >/dev/null &
[1] 9967
> kill -INT 9967
> ps aux | grep yes
sean ... 0:00 grep yes

为什么 SIGINT 在交互式实例中会终止进程,而在脚本实例中却不会?

编辑

以下是一些可能有助于诊断问题的补充信息。我编写了下面的Go程序来模拟上面的脚本。

package main

import (
    "fmt"
    "os"
    "os/exec"
    "time"
)

func main() {
    yes := exec.Command("yes")
    if err := yes.Start(); err != nil {
        die("%v", err)
    }

    time.Sleep(time.Second*2)

    kill := exec.Command("kill", "-INT", fmt.Sprintf("%d", yes.Process.Pid))
    if err := kill.Run(); err != nil {
        die("%v", err)
    }

    time.Sleep(time.Second*2)

    out, err := exec.Command("bash", "-c", "ps aux | grep yes").CombinedOutput()
    if err != nil {
        die("%v", err)
    }
    fmt.Println(string(out))
}

func die(msg string, args ...interface{}) {
    fmt.Fprintf(os.Stderr, msg+"\n", args...)
    os.Exit(1)
}

我将其构建为脚本并在脚本中main运行 ,并以交互方式运行并给出相同的以下输出:./main./main./main &

sean ... 0:01 [yes] <defunct>
sean ... 0:00 bash -c ps aux | grep yes
sean ... 0:00 grep yes

但是,./main &在脚本中运行会出现以下情况:

sean ... 0:03 yes
sean ... 0:00 bash -c ps aux | grep yes
sean ... 0:00 grep yes

这让我相信,尽管我在 Bash shell 中运行所有这些,但这种差异与 Bash 自己的作业控制关系不大。

答案1

使用什么 shell 是一个问题,因为不同的 shell 处理作业控制的方式不同(并且作业控制很复杂;根据 ,目前 C 的重量为 3,300 行job.c)。例如,Mac OS X 10.11 上的5.2.14 与3.2 显示:bashclocpdkshbash

$ cat code
pkill yes
yes >/dev/null &
pid=$!
echo $pid
sleep 2
kill -INT $pid
sleep 2
pgrep yes
$ bash code
38643
38643
$ ksh code
38650
$ 

这里还相关的是,yes不执行信号处理,因此继承从父 shell 进程继承的任何内容;相比之下,如果我们确实执行信号处理——

$ cat sighandlingcode 
perl -e '$SIG{INT} = sub { die "ouch\n" }; sleep 5' &
pid=$!
sleep 2
kill -INT $pid
$ bash sighandlingcode 
ouch
$ ksh sighandlingcode 
ouch
$ 

- 无论父 shell 是什么,SIGINT 都会被触发,因为perl这里不像yes改变了信号处理。有一些与信号处理相关的系统调用,可以通过 DTrace 或straceLinux 上的此处进行观察:

-bash-4.2$ cat code
pkill yes
yes >/dev/null &
pid=$!
echo $pid
sleep 2
kill -INT $pid
sleep 2
pgrep yes
pkill yes
-bash-4.2$ rm foo*; strace -o foo -ff bash code
21899
21899
code: line 9: 21899 Terminated              yes > /dev/null
-bash-4.2$ 

我们发现该yes过程最终被SIGINT忽略:

-bash-4.2$ egrep 'exec.*yes' foo.21*
foo.21898:execve("/usr/bin/pkill", ["pkill", "yes"], [/* 24 vars */]) = 0
foo.21899:execve("/usr/bin/yes", ["yes"], [/* 24 vars */]) = 0
foo.21903:execve("/usr/bin/pgrep", ["pgrep", "yes"], [/* 24 vars */]) = 0
foo.21904:execve("/usr/bin/pkill", ["pkill", "yes"], [/* 24 vars */]) = 0
-bash-4.2$ grep INT foo.21899
rt_sigaction(SIGINT, {SIG_DFL, [], SA_RESTORER, 0x7f18ebee0250}, {SIG_DFL, [], SA_RESTORER, 0x7f18ebee0250}, 8) = 0
rt_sigaction(SIGINT, {SIG_DFL, [], SA_RESTORER, 0x7f18ebee0250}, {SIG_DFL, [], SA_RESTORER, 0x7f18ebee0250}, 8) = 0
rt_sigaction(SIGINT, {SIG_IGN, [], SA_RESTORER, 0x7f18ebee0250}, {SIG_DFL, [], SA_RESTORER, 0x7f18ebee0250}, 8) = 0
--- SIGINT {si_signo=SIGINT, si_code=SI_USER, si_pid=21897, si_uid=1000} ---
-bash-4.2$ 

使用代码重复此测试perl,人们应该看到它SIGINT没有被忽略,或者也pdksh没有像 中那样设置忽略bash。当“监视模式”打开时,就像在 中的交互模式一样bashyes被杀死。

-bash-4.2$ cat monitorcode 
#!/bin/bash
set -m
pkill yes
yes >/dev/null &
pid=$!
echo $pid
sleep 2
kill -INT $pid
sleep 2
pgrep yes
pkill yes
-bash-4.2$ ./monitorcode 
22117
[1]+  Interrupt               yes > /dev/null
-bash-4.2$ 

答案2

后台工作有不应该被束缚在启动它们的外壳上。如果您退出 shell,它们将继续运行。因此,它们不应该被 中断SIGINT,而不是默认情况下。启用作业控制后,会自动完成,因为后台作业在单独的进程组中运行。当禁用作业控制时(通常在非交互式 shell 中),bash使异步命令忽略SIGINT.

文档的相关部分:

Bash 启动的非内置命令将信号处理程序设置为 shell 从其父级继承的值。当作业控制无效时,异步命令会忽略SIGINTSIGQUIT除了这些继承的处理程序之外。由于命令替换而运行的命令会忽略键盘生成的作业控制信号SIGTTINSIGTTOUSIGTSTP

https://www.gnu.org/software/bash/manual/html_node/Signals.html

为了便于实现作业控制的用户界面,操作系统维护当前终端进程组ID的概念。该进程组的成员(进程组 ID 等于当前终端进程组 ID 的进程)接收键盘生成的信号,例如SIGINT。据说这些进程位于前台。后台进程是指进程组ID与终端进程组ID不同的进程;这些进程不受键盘生成的信号的影响。仅允许前台进程读取或写入终端(如果用户使用 stty tostop 指定)。试图从终端读取(当 stty tostop 有效时写入)的后台进程会被内核的终端驱动程序发送一个SIGTTIN( ) 信号,除非被捕获,否则该进程将挂起。SIGTTOU

https://www.gnu.org/software/bash/manual/html_node/Job-Control-Basics.html

更多相关内容这里

相关内容