我有以下脚本:
suspense_cleanup () {
echo "Suspense clean up..."
}
int_cleanup () {
echo "Int clean up..."
exit 0
}
trap 'suspense_cleanup' SIGTSTP
trap 'int_cleanup' SIGINT
sleep 600
如果我运行它并按Ctrl-C
, Int clean up...
show 并退出。
但是如果我按Ctrl-Z
,^Z
字符会显示在屏幕上,然后挂起。
我怎么能够:
- 在 上运行一些清理代码
Ctrl-Z
,甚至可能回显某些内容,并且 - 之后继续暂停吗?
随机阅读 glibc 文档,我发现了这一点:
禁用 SUSP 字符正常解释的应用程序应该为用户提供一些其他机制来停止作业。当用户调用此机制时,程序应该向该进程的进程组发送SIGTSTP信号,而不仅仅是向进程本身发送。
但我不确定这是否适用于此,而且无论如何它似乎不起作用。
语境:我正在尝试制作一个交互式 shell 脚本,它支持 Vim/Neovim 支持的所有与悬念相关的功能。即:
- 能够以编程方式挂起(使用
:suspend
,而不只是让用户按 Ctrl-z) - 能够在挂起之前执行操作(Vim 中自动保存)
- 能够在恢复之前执行操作(NeoVim 中的 VimResume)
编辑:更改sleep 600
为for x in {1..100}; do sleep 6; done
也不起作用。
编辑2:sleep 600
当替换为 时它起作用sleep 600 & wait
。我完全不确定它为什么或如何工作,或者这样的东西有什么局限性。
答案1
Linux 和其他类 UNIX 系统上的信号处理是一个非常复杂的主题,涉及许多参与者:内核终端驱动程序、父进程->子进程关系、进程组、控制终端、启用/禁用作业控制的 Shell 信号处理、各个进程中的信号处理程序等等。
首先,Control- C,
Control-Z键绑定不是由 shell 处理的,而是由内核处理的。您可以使用以下命令查看默认定义stty -a
:
$ stty -a
speed 38400 baud; rows 64; columns 212; line = 0;
intr = ^C; quit = ^\; erase = ^H; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; discard = ^O; min = 1; time = 0;
-parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany -imaxbel iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke -flusho -extproc
在这里我们看到intr = ^C
和susp = ^Z
。 stty 反过来使用 TCGETS ioctl 从内核获取此信息
系统调用:
$ strace stty -a |& grep TCGETS
ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
默认的键绑定在 Linux 内核代码中定义
#define INIT_C_CC { \
[VINTR] = 'C'-0x40, \
[VQUIT] = '\\'-0x40, \
[VERASE] = '\177', \
[VKILL] = 'U'-0x40, \
[VEOF] = 'D'-0x40, \
[VSTART] = 'Q'-0x40, \
[VSTOP] = 'S'-0x40, \
[VSUSP] = 'Z'-0x40, \
[VREPRINT] = 'R'-0x40, \
[VDISCARD] = 'O'-0x40, \
[VWERASE] = 'W'-0x40, \
[VLNEXT] = 'V'-0x40, \
INIT_C_CC_VDSUSP_EXTRA \
[VMIN] = 1 }
还定义了默认操作:
static void n_tty_receive_char_special(struct tty_struct *tty, unsigned char c,
bool lookahead_done)
{
struct n_tty_data *ldata = tty->disc_data;
if (I_IXON(tty) && n_tty_receive_char_flow_ctrl(tty, c, lookahead_done))
return;
if (L_ISIG(tty)) {
if (c == INTR_CHAR(tty)) {
n_tty_receive_signal_char(tty, SIGINT, c);
return;
} else if (c == QUIT_CHAR(tty)) {
n_tty_receive_signal_char(tty, SIGQUIT, c);
return;
} else if (c == SUSP_CHAR(tty)) {
n_tty_receive_signal_char(tty, SIGTSTP, c);
return;
}
信号最终到达 __kill_pgrp_info() ,其中显示:
/*
* __kill_pgrp_info() sends a signal to a process group: this is what the tty
* control characters do (^C, ^Z etc)
* - the caller must hold at least a readlock on tasklist_lock
*/
Control这对我们的故事很重要 - 使用-C和Control-生成的信号 Z被发送到前台进程组由父交互式 shell 创建,其领导者是新运行的脚本。脚本及其孩子们属于一个群体。
因此,正如用户评论中正确指出的那样卡米尔·马乔罗夫斯基,当您发送Control-Z启动脚本后 SIGTSTP 信号均被脚本接收和 sleep
因为当一个信号发送到一个组时,该组中的所有进程都会收到该信号。很容易看出您是否从代码中删除了陷阱,使其看起来像这样(顺便说一句,总是添加一个
https://en.wikipedia.org/wiki/shebang_(unix),没有定义如果没有 shebang 会发生什么))
#!/usr/bin/env bash
# suspense_cleanup () {
# echo "Suspense clean up..."
# }
# int_cleanup () {
# echo "Int clean up..."
# exit 0
# }
# trap 'suspense_cleanup' SIGTSTP
# trap 'int_cleanup' SIGINT
sleep 600
运行它(我将其命名为 sigtstp.sh)并停止它:
$ ./sigtstp.sh
^Z
[1]+ Stopped ./sigtstp.sh
$ ps aux | grep -e '[s]leep 600' -e '[s]igtstp.sh'
ja 27062 0.0 0.0 6908 3144 pts/25 T 23:50 0:00 sh ./sigstop.sh
ja 27063 0.0 0.0 2960 1664 pts/25 T 23:50 0:00 sleep 600
ja
是我的用户名,你的用户名会不同,PID 也会不同,但重要的是两个进程都处于停止状态,如字母“T”所示。从man ps
:
PROCESS STATE CODES
(...)
T stopped by job control signal
这意味着两个进程都收到了 SIGTSTP 信号。现在,如果两个进程(包括 sigstop.sh)都收到信号,为什么
suspense_cleanup()
信号处理程序不运行? Bash 直到终止才执行它sleep 600
。它的要求是由
POSIX:
当 shell 等待实用程序执行前台命令完成时接收到已设置陷阱的信号时,与该信号关联的陷阱只有在前台命令完成后才会执行。
(请注意,尽管在开源世界中,IT 一般标准只是提示的集合,并且没有法律要求强迫任何人遵循它们)。如果你睡得少一点,比如说 3 秒,那也是没有帮助的,因为sleep
无论如何进程都会停止,所以它永远不会完成。为了suspense_cleanup()
立即被调用,我们必须在后台运行它并wait
按照上面的 POSIX 链接中的说明运行:
#!/usr/bin/env bash
suspense_cleanup () {
echo "Suspense clean up..."
}
int_cleanup () {
echo "Int clean up..."
exit 0
}
trap 'suspense_cleanup' SIGTSTP
trap 'int_cleanup' SIGINT
sleep 600 &
wait
运行它并停止它:
$ ./sigstop.sh
^ZSuspense clean up...
请注意,sleep 600
和sigtstp.sh
现在都消失了:
$ ps aux | grep -e '[s]leep 600' -e '[s]igtstp.sh'
$
很明显为什么 sigtstp.sh 消失了 -wait
被信号中断,它是脚本中的最后一行,因此它退出。更令人惊讶的是,当您意识到如果您发送 SIGINT,即使在 sigtstp.sh 死亡后睡眠仍然会运行:
$ ./sigtstp.sh
^CInt clean up...
$ ps aux | grep -e '[s]leep 600' -e '[s]igtstp.sh'
ja 32354 0.0 0.0 2960 1632 pts/25 S 00:12 0:00 sleep 600
但是,由于其父级死亡,它将被 init 采用:
$ grep PPid /proc/32354/status
PPid: 1
原因是当 shell 在后台运行子进程时,它会禁用默认的 SIGINT 处理程序,该处理程序将终止其中的进程 (signal(7))](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html):
如果在 shell 执行异步列表时禁用作业控制(请参阅 set -m 的说明),则列表中的命令将从 shell 继承 SIGINT 和 SIGQUIT 信号的忽略 (SIG_IGN) 信号操作。在所有其他情况下,shell 执行的命令应继承与 shell 从其父级继承的信号操作相同的信号操作,除非信号操作被 trap 特殊内置函数修改(请参阅 trap)
一些SO参考: https://stackoverflow.com/questions/46061694/bash-why-cant-i-set-a-trap-for-sigint-in-a-background-shell/46061734#46061734, https://stackoverflow.com/questions/45106725/why-do-shells-ignore-sigint-and-sigquit-in-backgrounded-processes。如果您想在收到 SIGINT 后杀死所有子进程,则必须在陷阱处理程序中手动执行此操作。但是请注意,SIGINT 仍然会传递给所有子级,但只是被忽略 - 如果您没有使用 sleep,而是安装了自己的 SIGINT 处理程序的命令(例如,尝试 tcpdump)!glibc手册 说:
请注意,如果先前将给定信号设置为忽略,则此代码会避免更改该设置。这是因为非作业控制 shell 在启动子进程时通常会忽略某些信号,因此子进程尊重这一点非常重要。
但是,如果我们自己不杀死它,那么为什么在向它发送 SIGTSTP 后 sleep 就死掉了,而 SIGTSTP 应该只阻止它,而不是杀死它呢?属于孤立进程组的所有已停止进程从内核获取 SIGHUP:
如果进程的退出导致进程组成为孤立进程组,并且新孤立进程组的任何成员停止,则应向新孤立进程组中的每个进程发送一个 SIGHUP 信号,后跟一个 SIGCONT 信号。
如果没有安装任何自定义处理程序,SIGHUP 将终止该进程(signal(7)):
Signal Standard Action Comment
SIGHUP P1990 Term Hangup detected on controlling terminal
or death of controlling process
(请注意,如果您在 strace 下运行 sleep ,事情会变得更加复杂......)。
好的,那么回到你原来的问题怎么样:
我怎么能够:
按 Ctrl-Z 运行一些清理代码,甚至可能回显某些内容,然后继续暂停?
我会这样做的方式是:
#!/usr/bin/env bash
suspense_cleanup () {
echo "Suspense clean up..."
trap - SIGTSTP
kill -TSTP $$
trap 'suspense_cleanup' SIGTSTP
}
int_cleanup () {
echo "Int clean up..."
exit 0
}
trap 'suspense_cleanup' SIGTSTP
trap 'int_cleanup' SIGINT
sleep 600 &
while true
do
if wait
then
echo child died, exiting
exit 0
fi
done
Nowsuspense_cleanup()
将在停止进程之前调用:
$ ./sigtstp.sh
^ZSuspense clean up...
[1]+ Stopped ./sigtstp.sh
$ ps aux | grep -e '[s]leep 600' -e '[s]igtstp.sh'
ja 4129 0.0 0.0 6920 3196 pts/25 T 00:29 0:00 bash ./sigtstp.sh
ja 4130 0.0 0.0 2960 1660 pts/25 T 00:29 0:00 sleep 600
$ fg
./sigtstp.sh
^ZSuspense clean up...
[1]+ Stopped ./sigtstp.sh
$ fg
./sigtstp.sh
^CInt clean up...
$ ps aux | grep -e '[s]leep 600' -e '[s]igtstp.sh'
ja 4130 0.0 0.0 2960 1660 pts/25 S 00:29 0:00 sleep 600
$ grep PPid /proc/4130/status
PPid: 1
你可以少睡一点,比如说 10 秒,然后看看如果睡眠完成,脚本就会退出:
#!/usr/bin/env bash
suspense_cleanup () {
echo "Suspense clean up..."
trap - SIGTSTP
kill -TSTP $$
trap 'suspense_cleanup' SIGTSTP
}
int_cleanup () {
echo "Int clean up..."
exit 0
}
trap 'suspense_cleanup' SIGTSTP
trap 'int_cleanup' SIGINT
# sleep 600 &
sleep 10 &
while true
do
if wait
then
echo child died, exiting
exit 0
fi
done
运行:
$ time ./sigtstp.sh
child died, exiting
real 0m10.007s
user 0m0.003s
sys 0m0.004s
答案2
看来 Bash 作业控制会干扰正常的 tty 特殊字符。 SIGTSTP 可能会发送到 Bash,但不会发送到正在运行的进程。
从https://www.gnu.org/software/bash/manual/bash.html#Job-Control-Basics:
如果运行 Bash 的操作系统支持作业控制,则 Bash 包含使用它的工具。在进程运行时键入挂起字符(通常为“^Z”、Control-Z)会导致该进程停止并将控制权返回给 Bash。
继续(Ctrl-Q,start
在 中调用stty
)在 Bash 作业控制下不起作用:您需要fg
或bg
进程来取消停止它。
答案3
在阅读man 7 signal
, 或 时 https://www.man7.org/linux/man-pages/man7/signal.7.html
,人们会看到:
信号 SIGKILL 和 SIGSTOP 无法被捕获、阻止或忽略
所以,你不能。