将 SIGINT 发送到运行脚本的 forked exec 进程不会杀死它

Sending SIGINT to forked exec process which runs script does not kill it

我正在用 C 编写 shell 应用程序并遇到一个问题,即发送 SIGINT 来处理 运行 脚本不会停止它。

向普通可执行文件发送相同的信号就可以了。

示例:

Bash 模仿长脚本的脚本 (work.sh):

#! /bin/bash

COUNTER=0
while true
do
    ((COUNTER+=1))
    echo "#${COUNTER} Working..."
    sleep 1
done

C代码:

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        char* cmds[] = { "./work.sh", NULL };
        if (execvp(cmds[0], cmds) == -1) {
            exit(1);
        }
    } else {
        sleep(5);
        kill(pid, SIGINT);
    }

    return 0;
}

发送信号后主进程结束但脚本继续打印。

脚本中的信号处理是否有所不同?您需要如何停止子进程而忽略什么是 运行?

在尝试了您的示例之后,我得出了这些结论。

即使在终端中 运行 交互,work.sh 脚本也不会 对从另一个终端发送的 SIGINT 做出反应;但它会在被打断时 使用 [Ctrl][c] 来自它自己的终端。
我想 bash 在终端管理中有某种魔法...

在脚本中添加 SIGINT 的显式管理似乎 让它真正对 SIGINT 做出反应(无论来源如何,甚至 来自你的 C 代码)。

#! /bin/bash

must_stop=""
function sigint_handler()
{
    echo "SIGINT"
    must_stop="1"
}
trap sigint_handler SIGINT

COUNTER=0
while [ -z "${must_stop}" ]
do
    ((COUNTER+=1))
    echo "#${COUNTER} Working..."
    sleep 1
done

如评论中所述,如果唯一目的是停止此脚本, 可能是 SIGHUP 或 SIGTERM 应该被考虑; SIGINT 通常是 与在终端中交互点击 [Ctrl][c] 比 从程序明确发送信号。

另请注意,使用 trap 捕获信号可提供以下功能 干净地退出循环而不是在中间猛烈地退出 任何指令。

一个解决方案:

将此行添加到您的 work.sh 脚本 trap exit SIGINT 以获得显式 SIGINT 处理程序:

#! /bin/bash

trap exit SIGINT

COUNTER=0
while true
do
    ((COUNTER+=1))
    echo "#${COUNTER} Working..."
    sleep 1
done

运行 work 可执行文件现在打印:

#1 Working...
#2 Working...
#3 Working...
#4 Working...
#5 Working...

之后它 returns 回到 shell。

问题:

我在已接受的答案中找到了这个 webpage linked in a comment to this question on Unix stackexchange (For the sake of completeness, here also the webpage linked。)这里有一段话可以解释发生了什么:

bash is among a few shells that implement a wait and cooperative exit approach at handling SIGINT/SIGQUIT delivery. When interpreting a script, upon receiving a SIGINT, it doesn't exit straight away but instead waits for the currently running command to return and only exits (by killing itself with SIGINT) if that command was also killed by that SIGINT. The idea is that if your script calls vi for instance, and you press Ctrl+C within vi to cancel an action, that should not be considered as a request to abort the script.

So imagine you're writing a script and that script exits normally upon receiving SIGINT. That means that if that script is invoked from another bash script, Ctrl-C will no longer interrupt that other script.

This kind of problem can be seen with actual commands that do exit normally upon SIGINT by design.

编辑:

我发现另一个 Unix stackexchange answer 可以更好地解释它。如果您查看 bash(1) 手册页,以下内容也很有解释性:

Non-builtin commands run by bash have signal handlers set to the values inherited by the shell from its parent. When job control is not in effect, asynchronous commands ignore SIGINT and SIGQUIT in addition to these inherited handlers.

尤其是考虑到这一点时:

Signals ignored upon entry to the shell cannot be trapped, reset or listed.

基本上,运行 work.sh 在单独的执行环境中运行它:

When a simple command other than a builtin or shell function is to be executed, it is invoked in a separate execution environment.

这包括信号处理程序(如果未明确存在)默认情况下将忽略 SIGINTSIGQUIT

您还可以:

  1. 更改kill(pid, SIGINT); killpg(getpid(), SIGINT);这就是按 ctrl-c 会做的事情;但它也会影响你的 main()。
  2. 如果你不想要你的主打,就在 execvp() 之前执行一个 setpgid(0,0),并将 kill(pid, SIGINT) 更改为 killpg(pid, SIGINT)。

那么shell脚本就不用“帮忙”了。 更公开一点,不要管你的 shell 脚本,而是将你的程序更改为:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
    pid_t pid = fork();
    if (pid == 0) {
        char* cmds[] = { "./work.sh", NULL };
        setpgid(0,0);
        if (execvp(cmds[0], cmds) == -1) {
            exit(1);
        }
    } else {
        sleep(5);
        killpg(pid, SIGINT);
    }

    return 0;
}

请允许我指出关闭方法可以改进,因为 SIGINT 单独使用时不是最佳选择并且不能保证关闭。

事实上,SIGINT不应该用作退出信号。

SIGINT 可用于通知子进程软关闭(例如,当 运行 进程是服务器脚本时)。

这将允许子进程正常关闭。

硬关机需要 SIGKILL,这是一个不容忽视的信号,由 OS 强制执行。

例如,C 代码可能如下所示:

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        char* cmds[] = { "./work.sh", NULL };
        if (execvp(cmds[0], cmds) == -1) {
            exit(1);
        }
    } else {
        int result = 0;
        sleep(5);
        kill(pid, SIGINT); /* soft shutdown */
        sleep(1);
        if(waitpid(pid, &result, WNOHANG) == -1) {
           /* we didn't exit? perform a hard shutdown */
           sleep(4);
           kill(pid, SIGKILL);
        }
    }
    return 0;
}

这将通知子进程软关机和硬关机,为两者留出时间。

...

当然,更好的方法可能是使用sigtimedwait