如何在 shell 脚本中使用用户提示处理 SIGINT 陷阱?

v3n*_*3nM 7 linux shell signals trap interrupt

我试图以这样一种方式处理 SIGINT/CTRL+C 中断,如果用户不小心按下了 ctrl-c,他会收到一条消息提示,“你想退出吗?(y/n)”。如果他输入 yes,则退出脚本。如果否,则从中断发生的地方继续。基本上,我需要 Ctrl-C 与 Ctrl-Z/SINTSTP 类似,但方式略有不同。我尝试了各种方法来实现这一点,但没有得到预期的结果。以下是我尝试过的几个场景。

情况1

脚本:play.sh

#!/bin/sh
function stop()
{
while true; do 
    read -rep $'\nDo you wish to stop playing?(y/n)' yn
    case $yn in
        [Yy]* ) echo "Thanks for playing !!!"; exit 1;;
        [Nn]* ) break;;
        * ) echo "Please answer (y/n)";;
    esac
done
} 
trap 'stop' SIGINT 
echo "going to sleep"
for i in {1..100}
do
  echo "$i"
  sleep 3   
done
echo "end of sleep"
Run Code Online (Sandbox Code Playgroud)

当我运行上面的脚本时,我得到了预期的结果。

输出:

$ play.sh 
going to sleep
1
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!

$ play.sh 
going to sleep
1
2
^C
Do you wish to stop playing?(y/n)n
3
4
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!! 
$  
Run Code Online (Sandbox Code Playgroud)

案例:2 我将 for 循环移动到一个新的脚本 loop.sh,因此 play.sh 成为父进程,而 loop.sh 成为子进程。

脚本:play.sh

#!/bin/sh
function stop()
{
while true; do 
    read -rep $'\nDo you wish to stop playing?(y/n)' yn
    case $yn in
        [Yy]* ) echo "Thanks for playing !!!"; exit 1;;
        [Nn]* ) break;;
        * ) echo "Please answer (y/n)";;
    esac
done
}
trap 'stop' SIGINT 
loop.sh
Run Code Online (Sandbox Code Playgroud)

脚本:loop.sh

#!/bin/sh
echo "going to sleep"
for i in {1..100}
do
  echo "$i"
  sleep 3   
done
echo "end of sleep"
Run Code Online (Sandbox Code Playgroud)

这种情况下的输出与预期不符。

输出:

$ play.sh 
going to sleep
1
2
^C
Do you wish to stop playing?(y/n)y
Thanks for playing !!!

$ play.sh 
going to sleep
1
2
3
4
^C
Do you wish to stop playing?(y/n)n
$
Run Code Online (Sandbox Code Playgroud)

我知道当一个进程收到一个 SIGINT 信号时,它会将信号传播到所有子进程,因此我的第二种情况失败了。有什么方法可以避免将 SIGINT 传播到子进程,从而使 loop.sh 完全按照第一种情况的方式工作?

注意: 这只是我实际应用的一个例子。我正在开发的应用程序在 play.sh 和 loop.sh 中有几个子脚本。我应该确保接收 SIGINT 的应用程序不应终止,但应向用户提示一条消息。

Rob*_*rtL 6

用很好的例子来管理工作和信号的经典问题!我开发了一个精简的测试脚本,专注于信号处理的机制。

为此,在后台启动子进程 (loop.sh) 后,调用wait,并在收到 INT 信号后,调用killPGID 等于您的 PID 的进程组。

  1. 对于有问题的脚本play.sh,可以通过以下方式完成:

  2. stop()函数中替换exit 1

    kill -TERM -$$  # note the dash, negative PID, kills the process group
    
    Run Code Online (Sandbox Code Playgroud)
  3. loop.sh作为后台进程启动(可以在此处启动多个后台进程并由 管理play.sh

    loop.sh &
    
    Run Code Online (Sandbox Code Playgroud)
  4. wait在脚本末尾添加以等待所有子项。

    wait
    
    Run Code Online (Sandbox Code Playgroud)

当您的脚本启动一个进程时,该子进程将成为进程组的成员,其 PGID 等于$$父 shell 中父进程的 PID 。

例如,脚本在后台trap.sh启动了三个sleep进程,现在正在wait运行它们,注意进程组 ID 列 (PGID) 与父进程的 PID 相同:

  PID  PGID STAT COMMAND
17121 17121 T    sh trap.sh
17122 17121 T    sleep 600
17123 17121 T    sleep 600
17124 17121 T    sleep 600
Run Code Online (Sandbox Code Playgroud)

在Unix和Linux,你可以通过调用发送信号到进程组的每个进程kill具有的负值PGID。如果你给出kill一个负数,它将被用作 -PGID。由于脚本的 PID ( $$) 与它的 PGID 相同,因此您可以kill在 shell 中使用

kill -TERM -$$    # note the dash before $$
Run Code Online (Sandbox Code Playgroud)

您必须提供信号编号或名称,否则某些 kill 实现会告诉您“非法选项”或“无效信号规范”。

下面的简单代码说明了所有这些。它设置一个trap信号处理程序,产生 3 个子kill进程,然后进入一个无限的等待循环,等待信号处理程序中的进程组命令杀死自己。

$ cat trap.sh
#!/bin/sh

signal_handler() {
        echo
        read -p 'Interrupt: ignore? (y/n) [Y] >' answer
        case $answer in
                [nN]) 
                        kill -TERM -$$  # negative PID, kill process group
                        ;;
        esac
}

trap signal_handler INT 

for i in 1 2 3
do
    sleep 600 &
done

wait  # don't exit until process group is killed or all children die
Run Code Online (Sandbox Code Playgroud)

这是一个示例运行:

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17111 17111 R+   ps -o pid,pgid,stat,args
$ 
Run Code Online (Sandbox Code Playgroud)

OK 没有额外的进程在运行。启动测试脚本,中断它(^C),选择忽略中断,然后挂起它(^Z):

$ sh trap.sh 
^C
Interrupt: ignore? (y/n) [Y] >y
^Z
[1]+  Stopped                 sh trap.sh
$
Run Code Online (Sandbox Code Playgroud)

检查正在运行的进程,注意进程组号 ( PGID):

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17121 17121 T    sh trap.sh
17122 17121 T    sleep 600
17123 17121 T    sleep 600
17124 17121 T    sleep 600
17143 17143 R+   ps -o pid,pgid,stat,args
$
Run Code Online (Sandbox Code Playgroud)

将我们的测试脚本带到前台(fg)并^C再次中断(),这次选择忽略:

$ fg
sh trap.sh
^C
Interrupt: ignore? (y/n) [Y] >n
Terminated
$
Run Code Online (Sandbox Code Playgroud)

检查正在运行的进程,不再休眠:

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17159 17159 R+   ps -o pid,pgid,stat,args
$ 
Run Code Online (Sandbox Code Playgroud)

请注意您的外壳:

我不得不修改你的代码才能让它在我的系统上运行。您#!/bin/sh在脚本中使用了第一行,但脚本使用了 /bin/sh 中不可用的扩展名(来自 bash 或 zsh)。