我有以下脚本:
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
。我完全不确定它为什么或如何工作,或者这样的东西有什么局限性。
Linux 和其他类 UNIX 系统上的信号处理是一个非常复杂的主题,涉及许多参与者:内核终端驱动程序、父进程 -> 子进程关系、进程组、控制终端、启用/禁用作业控制的 shell 信号处理、信号单个进程或更多进程中的处理程序。
首先,Control- C, Control-Z键绑定不是由 shell 处理的,而是由内核处理的。您可以使用以下命令查看默认定义
stty -a
:在这里我们看到
intr = ^C
和susp = ^Z
。stty 又使用 TCGETS ioctl syscall从内核获取此信息:默认的键绑定在 Linux 内核代码中定义
还定义了默认操作:
信号最终到达 __kill_pgrp_info() ,其中显示:
Control这对我们的故事很重要 - 使用-C和Control-生成的信号 Z被发送到由父交互式 shell 创建的前台进程组,其领导者是新运行的脚本。该脚本及其子脚本属于一组。
因此,正如用户Kamil Maciorowski在评论中正确指出的那样Control,当您在启动脚本后发送ZSIGTSTP 信号时,脚本都会收到信号,并且
sleep
因为当信号发送到组时,该组中的所有进程都会收到该信号。很容易看出您是否从代码中删除了陷阱,使其看起来像这样(顺便说一句,始终添加 https://en.wikipedia.org/wiki/shebang_(unix),它没有定义如果存在应该发生什么不是shebang))运行它(我将其命名为 sigtstp.sh)并停止它:
ja
是我的用户名,你的用户名会不同,PID 也会不同,但重要的是两个进程都处于停止状态,如字母“T”所示。来自man ps
:这意味着两个进程都收到了 SIGTSTP 信号。现在,如果两个进程(包括 sigstop.sh)都收到信号,为什么
suspense_cleanup()
信号处理程序不运行?Bash 直到终止才执行它sleep 600
。这是POSIX强加的要求 :(请注意,尽管在开源世界中,IT 一般标准只是提示的集合,并且没有法律要求强迫任何人遵循它们)。如果你睡得少一点,比如说 3 秒,那也是没有帮助的,因为
sleep
无论如何进程都会停止,所以它永远不会完成。为了suspense_cleanup()
立即被调用,我们必须在后台运行它并wait
按照上面的 POSIX 链接中的说明运行:运行它并停止它:
请注意, 和
sleep 600
现在sigtstp.sh
都消失了:很明显为什么 sigtstp.sh 消失了 -
wait
被信号中断,它是脚本中的最后一行,因此它退出。更令人惊讶的是,当您意识到如果您发送 SIGINT,即使在 sigtstp.sh 死亡后睡眠仍然会运行:但是,由于其父级死亡,它将被 init 采用:
原因是当 shell 在后台运行子进程时,它会禁用默认的 SIGINT 处理程序,该处理程序将终止其中的进程 (signal(7))]( https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02 .html ):
一些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手册 说:
但是,如果我们自己不杀死它,那么为什么在向它发送 SIGTSTP 后 sleep 就死掉了,而 SIGTSTP 应该只阻止它,而不是杀死它呢?属于孤立进程组的所有已停止进程都会从内核收到 SIGHUP:
如果没有安装任何自定义处理程序,SIGHUP 将终止该进程(signal(7)):
(请注意,如果您在 strace 下运行 sleep ,事情会变得更加复杂......)。
好的,那么回到你原来的问题怎么样:
我会这样做的方式是:
Now
suspense_cleanup()
将在停止进程之前调用:你可以少睡一点,比如说 10 秒,然后看看如果睡眠完成,脚本就会退出:
运行:
看来 Bash 作业控制会干扰正常的 tty 特殊字符。SIGTSTP 可能会发送到 Bash,但不会发送到正在运行的进程。
来自https://www.gnu.org/software/bash/manual/bash.html#Job-Control-Basics:
继续(Ctrl-Q,
start
在 中调用stty
)在 Bash 作业控制下不起作用:您需要fg
或bg
进程来取消停止它。在阅读
man 7 signal
, 或 时https://www.man7.org/linux/man-pages/man7/signal.7.html
,人们会看到:所以,你不能。