陷阱和 SIGINT 的奇怪问题

dic*_*tto 6 shell bash signals trap background-process

请解释一下:

#!/bin/bash
# This is scripta.sh
./scriptb.sh &
pid=$!
echo $pid started
sleep 3
while true
do
    kill -SIGINT $pid
    echo scripta.sh $$
    sleep 3
done 
Run Code Online (Sandbox Code Playgroud)

——

#!/bin/bash
# This is scriptb.sh
trap "echo Ouch;" SIGINT

while true
do
 echo scriptb.sh $$
 sleep 1
done
Run Code Online (Sandbox Code Playgroud)

当我运行./scripta.sh陷阱不打印。如果我从 SIGINT 切换到任何其他信号(我尝试了 SIGTERM、SIGUSR1),陷阱会按预期打印“哎哟”。这怎么会发生?

G-M*_*ca' 11

如果我从 SIGINT 切换到任何其他信号(我尝试了 SIGTERM、SIGUSR1),陷阱会按预期打印“Ouch”。

显然你没有尝试 SIGQUIT;您可能会发现它的行为与 SIGINT 相同。

问题是作业控制。

在 Unix 的早期,每当 shell 将进程或管道置于后台时,它会将这些进程设置为忽略 SIGINT 和 SIGQUIT,因此如果用户随后键入Ctrl+ C(中断)或Ctrl+ \(退出),它们不会被终止一个前台任务。当作业控制出现时,它带来了进程组,所以现在shell需要做的就是把后台作业放到一个新的进程组中;只要它不是当前的终端进程组,进程就不会看到来自键盘的信号(Ctrl+ CCtrl+\Ctrl+ Z(SIGTSTP))。shell 可以使用默认信号处理离开后台进程。事实上,它可能必须,因为进程可以被杀死Ctrl+C当它们被带到前台时。

但是非交互式 shell 不使用作业控制。出于历史原因,非交互式 shell 会退回到忽略后台进程的 SIGINT 和 SIGQUIT 的旧行为是有道理的——即使向他们发送了键盘类型的信号,也允许后台进程继续运行. 并且 shell 脚本在非交互式 shell 中运行。

而且,如果您查看bash(1) 中trap命令下的最后一段,您会看到

进入外壳时被忽略的信号不能被捕获或重置。

因此,如果您./scriptb.sh?&交互式shell 的命令提示符运行,它的信号处置将被保留(即使它被置于后台),并且该trap命令按预期工作。但是,如果您运行./scripta.sh(使用或不使用&),它会在非交互式 shell 中运行脚本。当该非交互式 shell 运行时./scriptb.sh?&,它会将scriptb进程设置为忽略中断并退出。因此,trap命令 inscriptb.sh静默失败。


thr*_*rig 3

通过一些追踪:

strace -o aaa ./scripta
Run Code Online (Sandbox Code Playgroud)

我们可以观察到默认情况下

read(255, "#!/bin/bash\n# this is scripta.sh"..., 153) = 153
rt_sigprocmask(SIG_BLOCK, NULL, [], 8)  = 0
rt_sigprocmask(SIG_BLOCK, NULL, [], 8)  = 0
rt_sigprocmask(SIG_BLOCK, [INT CHLD], [], 8) = 0
lseek(255, -108, SEEK_CUR)              = 45
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f1ee9b7ca10) = 2483
Run Code Online (Sandbox Code Playgroud)

scripta阻塞INTCHLD信号也是如此;scriptb通过fork(此处称为clone)继承这些设置。以及正在scriptb做什么?如果我们从 via 运行它scripta

strace -o bbb ./scriptb &
Run Code Online (Sandbox Code Playgroud)

然后寻找信号相关的东西,我们发现:

rt_sigprocmask(SIG_BLOCK, NULL, [], 8)  = 0
rt_sigaction(SIGINT, {SIG_DFL, [], SA_RESTORER, 0x7fb1b2e75250}, {SIG_IGN, [], 0}, 8) = 0
rt_sigaction(SIGINT, {SIG_IGN, [], SA_RESTORER, 0x7fb1b2e75250}, {SIG_DFL, [], SA_RESTORER, 0x7fb1b2e75250}, 8) = 0
Run Code Online (Sandbox Code Playgroud)

这表明没有任何东西被阻止,然后该INT信号首先被给予默认处理,然后被忽略。scriptb直接从shell下运行strace对比显示:

rt_sigaction(SIGINT, {SIG_DFL, [], SA_RESTORER, 0x7f2c3f6d4250}, {SIG_DFL, [], 0}, 8) = 0
rt_sigaction(SIGINT, {SIG_DFL, [], SA_RESTORER, 0x7f2c3f6d4250}, {SIG_DFL, [], SA_RESTORER, 0x7f2c3f6d4250}, 8) = 0
rt_sigprocmask(SIG_BLOCK, [INT], [], 8) = 0
rt_sigaction(SIGINT, {0x45fbf0, [], SA_RESTORER, 0x7f2c3f6d4250}, {SIG_DFL, [], SA_RESTORER, 0x7f2c3f6d4250}, 8) = 0
rt_sigprocmask(SIG_BLOCK, [INT CHLD], [], 8) = 0
rt_sigprocmask(SIG_BLOCK, ~[RTMIN RT_1], [INT CHLD], 8) = 0
...
Run Code Online (Sandbox Code Playgroud)

或者在进入然后重复的睡眠呼叫处理之前从未被忽略。好的。呃。让我们在scripta和之间放置一个垫片scriptb,重置SIGINT为默认状态......

#include <getopt.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int ch;
    while ((ch = getopt(argc, argv, "h?")) != -1) {
        switch (ch) {
        case 'h':
        case '?':
        default:
            abort();
        }
    }
    argc -= optind;
    argv += optind;
    if (argc < 1) abort();

    signal(SIGINT, SIG_DFL);

    execvp(*argv, argv);
    abort();
    return 1;
}
Run Code Online (Sandbox Code Playgroud)

使用方法如下:

$ make defaultsig
cc     defaultsig.c   -o defaultsig
$ grep defaultsig scripta.sh
./defaultsig ./scriptb.sh &
$ ./scripta.sh 
12510 started
scriptb.sh 12510
scriptb.sh 12510
scriptb.sh 12510
scripta.sh 12509
Ouch
scriptb.sh 12510
scriptb.sh 12510
scriptb.sh 12510
scripta.sh 12509
Ouch
...
Run Code Online (Sandbox Code Playgroud)

是的,现在效果很好。但是,我不知道为什么bash会这样,也许向他们提交错误?中的代码sig.c看起来非常复杂,并且其他地方有更多的信号处理......