为什么不能通过击键杀死Bash脚本调用的超时?


11

[编辑:这看起来与其他一些问题类似,询问如何杀死所有产生的进程-答案似乎都是使用pkill。因此,我的问题的核心可能是:是否可以将Ctrl-C / Z传播到脚本生成的所有进程?]

rec使用timeout来自coreutils 的命令调用SoX 时(这里已讨论),一旦从Bash脚本中调用它,似乎没有任何办法可以通过击键杀死它。

例子:

timeout 10 rec test.wav

...可以用bash中的Ctrl+ CCtrl+ 杀死Z,但不能从脚本内部调用。

timeout 10 ping nowhere

...可以被杀死Ctrl+ CCtrl+ Z从庆典,并与Ctrl+ Z当它在脚本中运行。

我可以找到进程ID并以这种方式将其杀死,但是为什么不能使用标准的击键动作呢?并且有什么方法可以构造我的脚本,以便可以吗?


2
Ctrl + Z不会杀死进程,只会暂停它们。他们将继续运行,如果你给bgfg命令。无论如何,您的1d和3d示例之间有区别吗?
terdon

我没有安装timeout系统,但是sleep无论是直接在命令行上键入,源代码,执行代码还是显式地通过解释器,都可以执行杀死程序
凯文

@terdon谢谢,我已经阐明了例子。
见面会

Answers:


18

诸如Ctrl+的信号键C将信号发送到前台进程组中的所有进程。

在典型情况下,过程组是管道。例如,在中head <somefile | sort,进程运行head和进程运行sort与外壳位于同一个进程组中,因此它们都接收信号。在后台(somecommand &)中运行作业时,该作业位于其自己的进程组中,因此按Ctrl+ C不会对其产生影响。

timeout程序将自己置于自己的进程组中。从源代码:

/* Ensure we're in our own group so all subprocesses can be killed.
   Note we don't just put the child in a separate group as
   then we would need to worry about foreground and background groups
   and propagating signals between them.  */
setpgid (0, 0);

发生超时时,timeout将通过简单的方法来杀死它所属的进程组。由于它已将自己置于单独的进程组中,因此其父进程将不在该组中。在此处使用进程组可确保如果子应用程序派生到多个进程中,则其所有进程都将接收信号。

当您timeout直接在命令行上运行并按Ctrl+时Ctimeout子进程和子进程都将接收到结果SIGINT ,而不是作为timeout父进程的交互式shell 会接收到此消息。当timeout从脚本调用时,只有运行脚本的shell会接收到该信号:timeout由于它在不同的进程组中,所以没有得到它。

您可以使用trap内置脚本在shell脚本中设置信号处理程序。不幸的是,这不是那么简单。考虑一下:

#!/bin/sh
trap 'echo Interrupted at $(date)' INT
date
timeout 5 sleep 10
date

如果您在2秒钟后按Ctrl+ C,则仍会等待5秒钟,然后打印“ Interrupted”消息。这是因为在前台作业处于活动状态时,shell拒绝运行陷阱代码。

要解决此问题,请在后台运行作业。在信号处理程序中,调用kill以将信号中继到timeout过程组。

#!/bin/sh
trap 'kill -INT -$pid' INT
timeout 5 sleep 10 &
pid=$!
wait $pid

非常偷偷摸摸-做工精美!我现在更聪明了,怜悯!
见面会

13

以Gilles提供的出色答案为基础。超时命令具有前台选项,如果使用该选项,则CTRL + C将退出超时命令。

#!/bin/sh
trap 'echo caught interrupt and exiting;exit' INT
date
timeout --foreground 5 sleep 10
date

这是一个很好的答案-比接受的答案更简单,并且对我来说使用timeoutGNU coreutils可以正常工作。
RichVel

1

基本上Ctrl+ C发送SIGINT信号,而Ctrl+ Z发送SIGTSTP信号。

SIGTSTP只是停止该过程,SIGCONT它将继续。

这适用于在命令行上分叉的前台进程。

如果您的进程是后台进程,则必须以其他方式将该信号发送到进程。kill会做到的。从理论上讲,执行“杀死”操作的“-”操作符也应发出子进程信号,但这很少能按预期工作。

进一步的阅读:Unix-Signals


的确如此,至少到“很少能按预期进行”(取决于您的期望-您应该阅读有关流程组的信息)为止,但这是完全无关的。这个问题不会使人对SIGINT和SIGTSTP感到困惑。
吉尔斯(Gillles)“所以-别再邪恶了”

0

通过为现有脚本提供“查找和替换”方法来改善@Gilles的答案

  1. 在代码开头添加以下代码段:
declare -a timeout_pids
exec 21>&1; exec 22>&2 # backup file descriptors, see /superuser//a/1446738/187576
my_timeout(){
    local args tp ret
    args="$@"
    timeout $args &
    tp=$!
    echo "pid of timeout: $tp"
    timeout_pids+=($tp)
    wait $tp
    ret=$?
    count=${#timeout_pids[@]}
    for ((i = 0; i < count; i++)); do
        if [ "${timeout_pids[i]}" = "$tp" ] ; then
            unset 'timeout_pids[i]'
        fi
    done
    return $ret
}
  1. 将以下代码添加(或合并)到您的INT处理程序中:
pre_cleanup(){
    exec 1>&21; exec 2>&22 # restore file descriptors, see /superuser//a/1446738/187576
    echo "Executing pre-cleanup..."
    for i in "${timeout_pids[*]}"; do
        if [[ ! -z $i ]]; then
            #echo "Killing PID: $i"
            kill -INT -$i 2> /dev/null
        fi
    done
    exit
}

trap pre_cleanup INT
  1. 用脚本中的功能替换timeout命令my_timeout

这是一个示例脚本:

#!/bin/bash

# see "killing timeout": /unix//a/57692/65781
declare -a timeout_pids
exec 21>&1; exec 22>&2 # backup file descriptors, see /superuser//a/1446738/187576
my_timeout(){
    local args tp ret
    args="$@"
    timeout $args &
    tp=$!
    echo "pid of timeout: $tp"
    timeout_pids+=($tp)
    wait $tp
    ret=$?
    count=${#timeout_pids[@]}
    for ((i = 0; i < count; i++)); do
        if [ "${timeout_pids[i]}" = "$tp" ] ; then
            unset 'timeout_pids[i]'
        fi
    done
    return $ret
}

cleanup(){
    echo "-----------------------------------------"
    echo "Restoring previous routing table settings"
}

pre_cleanup(){
    exec 1>&21; exec 2>&22 # restore file descriptors, see /superuser//a/1446738/187576
    echo "Executing pre-cleanup..."
    for i in "${timeout_pids[*]}"; do
        if [[ ! -z $i ]]; then
            echo "Killing PID: $i"
            kill -INT -$i 2> /dev/null
        fi
    done
    exit
}

trap pre_cleanup INT
trap cleanup EXIT

echo "Executing 5 temporary timeouts..."
unreachable_ip="192.168.44.5"
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
my_timeout 1s ping -c 1 "$unreachable_ip" &> /dev/null 
echo "ctrl+c now to execute cleanup"
my_timeout 9s ping -c 1 "$unreachable_ip" &> /dev/null 
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.