![如何在 shell 脚本中使用用户提示符处理 SIGINT 陷阱?](https://linux22.com/image/76558/%E5%A6%82%E4%BD%95%E5%9C%A8%20shell%20%E8%84%9A%E6%9C%AC%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%94%A8%E6%88%B7%E6%8F%90%E7%A4%BA%E7%AC%A6%E5%A4%84%E7%90%86%20SIGINT%20%E9%99%B7%E9%98%B1%EF%BC%9F.png)
我试图以这样的方式处理SIGINT
( CtrlC),如果用户不小心按下 ctrl-c,系统会提示他一条消息:“你想退出吗?(y/n)”。如果他输入 yes,则退出脚本。如果否,则从中断发生处继续。基本上,我需要CtrlC以类似于SIGTSTP
(CtrlZ)的方式工作,但方式略有不同。我尝试了各种方法来实现这一目标,但没有得到预期的结果。以下是我尝试过的几个场景。
情况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 的进程组。
对于有问题的脚本,
play.sh
可以通过以下方式完成:在
stop()
函数中替换exit 1
为kill -TERM -$$ # note the dash, negative PID, kills the process group
作为后台进程启动
loop.sh
(此处可以启动多个后台进程并由 管理play.sh
)loop.sh &
wait
在脚本末尾添加以等待所有子项。wait
当您的脚本启动一个进程时,该子进程将成为进程组的成员,其 PGID 等于$$
父 shell 中父进程的 PID。
例如,脚本在后台trap.sh
启动了三个进程,现在正在对它们进行 ing,请注意进程组 ID 列 (PGID) 与父进程的 PID 相同:sleep
wait
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]$