我在脚本中有以下内容:
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 显示:bash
cloc
pdksh
bash
$ 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 或strace
Linux 上的此处进行观察:
-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
。当“监视模式”打开时,就像在 中的交互模式一样bash
,yes
被杀死。
-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 从其父级继承的值。当作业控制无效时,异步命令会忽略
SIGINT
并SIGQUIT
除了这些继承的处理程序之外。由于命令替换而运行的命令会忽略键盘生成的作业控制信号SIGTTIN
、SIGTTOU
和SIGTSTP
。
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
更多相关内容这里。