防止 SIGINT 传播到父进程

防止 SIGINT 传播到父进程

考虑父程序(可以是 C++ 程序或 Shell 脚本)执行子 Shell 脚本的场景,当我们在子 Shell 脚本执行时按 Control+C(或任何配置为 INTR 字符的字符)时, SIGINT 被发送到前台进程组中的所有进程。这包括父进程。

来源 :POSIX.1-2008 XBD 第 11.1.9 节

有没有办法覆盖这种默认行为?子进程单独处理信号而不将其传播到父进程?

参考 :Stack Overflow Post - 当子进程被中断时父进程未完成(TRAP INT)

答案1

(受到吉尔斯回答的启发)

设置标志后,脚本在不获取其父进程的情况下获取的ISIG唯一方法是使其位于自己的进程组中。这可以通过选项来完成。ChildSIGINTSIGINTset -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"
    

另类想法

  1. 修改前台进程组中的其他进程,使其SIGINT在 的持续时间内忽略Child。这并不能解决你的问题,但它可能会给你带来你想要的东西。
  2. 修改Child为:
    1. 用于stty -g备份当前终端设置。
    2. 运行stty -isig时不生成带有INTRQUIT、 和SUSP字符的信号。
    3. 在后台,读取终端输入并根据需要自行发送信号(例如,读取+kill -QUIT 0时运行、读取+时运行)。这并不是一件小事,如果脚本或其运行的任何内容都是交互式的,则可能无法使其顺利工作。Control\kill -INT $$ControlCChild
    4. 退出前恢复终端设置(最好是从 上的陷阱EXIT)。
  3. 与 #2 相同,只是不是运行stty -isig,而是等待用户Enter在杀死之前按下或其他一些非特殊键Child
  4. 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字符”。

所以这是两个选择。第三种方法是在子进程自己的进程组中运行子进程。

相关内容