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

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

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

情况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"

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

输出:

$ 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 !!! 
$  

案例: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

脚本:loop.sh

#!/bin/sh
echo "going to sleep"
for i in {1..100}
do
  echo "$i"
  sleep 3   
done
echo "end of sleep"

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

输出:

$ 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
$

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

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

答案1

关于管理作业和信号的经典问题,并提供了很好的例子!我开发了一个精简的测试脚本来重点关注信号处理的机制。

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

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

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

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

    loop.sh &
    
  4. wait在脚本末尾添加以等待所有子项。

    wait
    

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

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

  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

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

kill -TERM -$$    # note the dash before $$

你必须给出一个信号编号或名称,否则某些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

这是一个示例运行:

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17111 17111 R+   ps -o pid,pgid,stat,args
$ 

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

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

检查正在运行的进程,记下进程组编号 ( 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
$

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

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

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

$ ps -o pid,pgid,stat,args
  PID  PGID STAT COMMAND
 8073  8073 Ss   /bin/bash
17159 17159 R+   ps -o pid,pgid,stat,args
$ 

请注意您的外壳:

我必须修改您的代码才能使其在我的系统上运行。您将其#!/bin/sh作为脚本中的第一行,但脚本使用了 /bin/sh 中不可用的扩展(来自 bash 或 zsh)。

答案2

“我知道当一个进程收到 SIGINT 信号时,它会将信号传播到所有子进程”

你从哪里得到这个错误的想法?

$ perl -E '$SIG{INT}=sub { say "ouch $$" }; if (fork()) { say "parent $$"; sleep 3; kill 2, $$ } else { say "child $$"; sleep 99 }'
parent 25831
child 25832
ouch 25831
$

如果正如所声称的那样存在传播,人们可能会期望子进程发出“哎呀”一声。

事实上:

“每当我们键入终端的中断键(通常是 DELETE 或 Control-C)或退出键(通常是 Control-反斜杠)时,都会导致中断信号或退出信号被发送到前台进程组中的所有进程”-- W理查德·史蒂文斯。 “UNIX® 环境中的高级编程”。艾迪生-韦斯利。 1993 年。第 246 页。

这可以通过以下方式观察到:

$ perl -E '$SIG{INT}=sub { say "ouch $$" }; fork(); sleep 99'
^Couch 25971
ouch 25972
$

由于前台进程组的所有进程都会SIGINT从 中吃掉 a Control+C,因此您需要设计所有进程以适当地处理或忽略该信号,或者可能让子进程成为新的前台进程组,以便父进程(例如外壳)看不到信号,因为它不再位于前台进程组中。

答案3

play.sh源循环文件中,如下所示:

source ./loop.sh

一旦由于陷阱而退出的子 shell 就无法在退出陷阱时返回。

答案4

问题是sleep

当你CTRL+C你杀了一个sleep——不是你的sh (即使你的语法#!/bin/sh有点可疑)。不过,整个进程组都会收到信号,因为您没有将处理程序安装在loop.sh(这是一个子 shell)中,它会立即终止。

你需要一个陷阱loop.sh。除非明确指定,否则每个启动的子 shell 都会清除陷阱trap ''SIG被家长忽视。

您还需要-m对父级进行工作控制监控,以便它跟踪其子级。wait例如,仅适用于作业控制。-m监视器模式是 shell 与终端交互的方式。

sh  -cm '  . /dev/fd/3' 3<<""                     # `-m`onitor is important
     sh -c ' kill -INT "$$"'&                     # get the sig#
     wait %%;INT=$?                               # keep it in $INT
     trap  " stty $(stty -g;stty -icanon)" EXIT   # lose canonical input
     loop()( trap    exit INT                     # this is a child
             while   [ "$#" -le 100 ]             # same as yours
             do      echo "$#"                    # writing the iterator
                     set "" "$@"                  # iterating
                     sleep 3                      # sleeping
             done
     )
     int(){  case    $1      in                   # handler fn
             ($INT)  printf  "\33[2K\rDo you want to stop playing? "
                     case    $(dd bs=1 count=1; echo >&3)  in
                     ([Nn])  return
             esac;   esac;   exit "$1"
     }       3>&2    >&2     2>/dev/null
     while   loop ||
             int "$?"
     do :;   done

0
1
2
3
4
Do you want to stop playing? n
0
1
2
3
Do you want to stop playing? N
0
1
Do you want to stop playing? y
[mikeserv@desktop tmp]$

相关内容