考虑父程序(可以是 C++ 程序或 Shell 脚本)执行子 Shell 脚本的场景,当我们在子 Shell 脚本执行时按 Control+C(或任何配置为 INTR 字符的字符)时, SIGINT 被发送到前台进程组中的所有进程。这包括父进程。
来源 :POSIX.1-2008 XBD 第 11.1.9 节
有没有办法覆盖这种默认行为?子进程单独处理信号而不将其传播到父进程?
答案1
(受到吉尔斯回答的启发)
设置标志后,脚本在不获取其父进程的情况下获取的ISIG
唯一方法是使其位于自己的进程组中。这可以通过选项来完成。Child
SIGINT
SIGINT
set -m
如果在 shell 脚本中打开该-m
选项Child
,它将执行作业控制而无需交互。这将导致它在单独的进程组中运行内容,从而防止父进程在读取字符SIGINT
时接收到。INTR
这里是-m
选项的 POSIX 描述:
-m
如果实现支持用户可移植性实用程序选项,则应支持此选项。所有作业都应在其自己的进程组中运行。在后台作业完成后 shell 立即发出提示之前,应将报告后台作业退出状态的消息写入标准错误。如果前台作业停止,shell 应向标准错误写入一条消息以达到该效果,其格式如作业实用程序所描述。此外,如果作业更改状态而不是退出(例如,如果它停止输入或输出或被 SIGSTOP 信号停止),则 shell 应在写入下一个提示之前立即写入类似的消息。默认情况下,交互式 shell 会启用此选项。
该-m
选项与 类似-i
,但它几乎不会像 那样改变 shell 的行为-i
。
例子:
剧本
Parent
:#!/bin/sh trap 'echo "PARENT: caught SIGINT; exiting"; exit 1' INT echo "PARENT: pid=$$" echo "PARENT: Spawning child..." ./Child echo "PARENT: child returned" echo "PARENT: exiting normally"
剧本
Child
:#!/bin/sh -m # ^^ # notice the -m option above! trap 'echo "CHILD: caught SIGINT; exiting"; exit 1' INT echo "CHILD: pid=$$" echo "CHILD: hit enter to exit" read foo echo "CHILD: exiting normally"
这是当您在等待输入时按Control+时会发生的情况:CChild
$ ./Parent
PARENT: pid=12233
PARENT: Spawning child...
CHILD: pid=12234
CHILD: hit enter to exit
^CCHILD: caught SIGINT; exiting
PARENT: child returned
PARENT: exiting normally
请注意父级的SIGINT
处理程序是如何永远不会被执行的。
或者,如果您想修改Parent
而不是Child
,您可以这样做:
剧本
Parent
:#!/bin/sh trap 'echo "PARENT: caught SIGINT; exiting"; exit 1' INT echo "PARENT: pid=$$" echo "PARENT: Spawning child..." sh -m ./Child # or 'sh -m -c ./Child' if Child isn't a shell script echo "PARENT: child returned" echo "PARENT: exiting normally"
脚本
Child
(正常;不需要-m
):#!/bin/sh trap 'echo "CHILD: caught SIGINT; exiting"; exit 1' INT echo "CHILD: pid=$$" echo "CHILD: hit enter to exit" read foo echo "CHILD: exiting normally"
另类想法
- 修改前台进程组中的其他进程,使其
SIGINT
在 的持续时间内忽略Child
。这并不能解决你的问题,但它可能会给你带来你想要的东西。 - 修改
Child
为:- 用于
stty -g
备份当前终端设置。 - 运行
stty -isig
时不生成带有INTR
、QUIT
、 和SUSP
字符的信号。 - 在后台,读取终端输入并根据需要自行发送信号(例如,读取+
kill -QUIT 0
时运行、读取+时运行)。这并不是一件小事,如果脚本或其运行的任何内容都是交互式的,则可能无法使其顺利工作。Control\kill -INT $$
ControlCChild
- 退出前恢复终端设置(最好是从 上的陷阱
EXIT
)。
- 用于
- 与 #2 相同,只是不是运行
stty -isig
,而是等待用户Enter在杀死之前按下或其他一些非特殊键Child
。 setpgid
用 C、Python、Perl 等编写您自己的实用程序,您可以使用它来调用setpgid()
.这是一个粗略的 C 实现:#define _XOPEN_SOURCE 700 #include <unistd.h> #include <signal.h> int main(int argc, char *argv[]) { // todo: add error checking void (*backup)(int); setpgid(0, 0); backup = signal(SIGTTOU, SIG_IGN); tcsetpgrp(0, getpid()); signal(SIGTTOU, backup); execvp(argv[1], argv + 1); return 1; }
示例用法来自
Child
:#!/bin/sh [ "${DID_SETPGID}" = true ] || { # restart self after calling setpgid(0, 0) exec env DID_SETPGID=true setpgid "$0" "$@" # exec failed if control reached this point exit 1 } unset DID_SETPGID # do stuff here
答案2
正如您引用 POSIX 的章节所解释的那样,SIGINT 被发送到整个前台进程组。因此,为了避免杀死程序父进程,请安排它在自己的进程组中运行。
Shell 不能通过setpgrp
内置或语法结构进行访问,但有一种间接的方法可以实现它,即交互式运行 shell。 (谢谢史蒂芬·希门尼斯为了技巧。)
ksh -ic '
… the part that needs to be interruptible without bothering the parent …
'
答案3
好吧,从您引用的堆栈溢出问题中,它清楚地表明需要配置父级来处理信号。
其次,POSIX参考明确指出“如果设置了ISIG,则处理时应丢弃INTR字符”。
所以这是两个选择。第三种方法是在子进程自己的进程组中运行子进程。