为什么 SIGINT 在脚本的后台进程上不起作用?

eZa*_*oto 5 process signals shell-script interrupt background-process

我在脚本中有以下内容:

yes >/dev/null &
pid=$!
echo $pid
sleep 2
kill -INT $pid
sleep 2
ps aux | grep yes
Run Code Online (Sandbox Code Playgroud)

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

> yes >/dev/null &
[1] 9967
> kill -INT 9967
> ps aux | grep yes
sean ... 0:00 grep yes
Run Code Online (Sandbox Code Playgroud)

为什么 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)
}
Run Code Online (Sandbox Code Playgroud)

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

sean ... 0:01 [yes] <defunct>
sean ... 0:00 bash -c ps aux | grep yes
sean ... 0:00 grep yes
Run Code Online (Sandbox Code Playgroud)

但是,./main &在脚本中运行会给出以下结果:

sean ... 0:03 yes
sean ... 0:00 bash -c ps aux | grep yes
sean ... 0:00 grep yes
Run Code Online (Sandbox Code Playgroud)

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

thr*_*rig 6

使用什么 shell 是一个问题,因为不同的 shell 处理作业控制的方式不同(而且作业控制很复杂;根据job.cbash目前在 C 中占 3,300 行cloc)。例如,Mac OS X 10.11 上的pdksh5.2.14 与bash3.2 显示:

$ 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
$ 
Run Code Online (Sandbox Code Playgroud)

这里还相关的是不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
$ 
Run Code Online (Sandbox Code Playgroud)

— SIGINT 的触发与父 shell 无关,因为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$ 
Run Code Online (Sandbox Code Playgroud)

我们发现该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$ 
Run Code Online (Sandbox Code Playgroud)

perl代码重复这个测试,你应该看到它SIGINT没有被忽略,或者pdkshbash. 打开“监视器模式”就像在 中处于交互模式一样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$ 
Run Code Online (Sandbox Code Playgroud)

  • 我使用 bash、dash、ksh、posh、pdksh、sh、yash 和 zsh 对此进行了测试,除了 posh 之外,所有其他命令都在非交互模式下的后台进程中忽略 SIGINT 和 SIGQUIT。我想知道这的历史原因是什么(尝试在这里询问/sf/ask/3157470781/)。 (2认同)