具有超时命令的管道的退出状态在交互式 shell 与 shell 脚本中的行为不同

具有超时命令的管道的退出状态在交互式 shell 与 shell 脚本中的行为不同

交互式shshell 会话:

$ sh
$ timeout 1 yes | sed -n s/a/b/p ; echo $?
Terminated
143
$ 

非交互式脚本通过sh -c

$ sh -c 'timeout 1 yes | sed -n s/a/b/p ; echo $?'
0
$ 

为什么这两个示例会产生不同的退出代码?

答案1

timeouttimeout至少 GNU)默认情况下尝试在新进程组中运行该命令,并在超时时使用 SIGTERM 终止该进程组。

这样,如果该命令生成更多进程,它们也会在超时时被终止。

~$ strace -fze  '/[sg]etpg|exec|kill|exit' sh -c 'timeout 1 sleep 2 | sleep 3'
execve("/usr/bin/sh", ["sh", "-c", "timeout 1 sleep 2 | sleep 3"], 0x7fffe2349230 /* 69 vars */) = 0
strace: Process 316058 attached
strace: Process 316059 attached
[pid 316058] execve("/usr/bin/timeout", ["timeout", "1", "sleep", "2"], 0x561e384e43a8 /* 69 vars */) = 0
[pid 316059] execve("/usr/bin/sleep", ["sleep", "3"], 0x561e384e41c8 /* 69 vars */) = 0
[pid 316058] setpgid(0, 0)              = 0
strace: Process 316060 attached
[pid 316060] execve("/usr/bin/sleep", ["sleep", "2"], 0x7ffc3ef29910 /* 69 vars */) = 0
[pid 316058] --- SIGALRM {si_signo=SIGALRM, si_code=SI_TIMER, si_timerid=0, si_overrun=0, si_int=0, si_ptr=NULL} ---
[pid 316058] kill(316060, SIGTERM)      = 0
[pid 316060] --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=316058, si_uid=1000} ---
[pid 316058] kill(0, SIGTERM)           = 0
[pid 316058] --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=316058, si_uid=1000} ---
[pid 316060] +++ killed by SIGTERM +++
[pid 316058] kill(316060, SIGCONT <unfinished ...>
)                                       = 0
[pid 316058] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_KILLED, si_pid=316060, si_uid=1000, si_status=SIGTERM, si_utime=0, si_stime=0} ---
[pid 316058] kill(0, SIGCONT)           = 0
[pid 316058] --- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=316058, si_uid=1000} ---
[pid 316058] +++ exited with 124 +++
[pid 316057] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=316058, si_uid=1000, si_status=124, si_utime=0, si_stime=0} ---
[pid 316059] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=316059, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++

请参阅timeout执行 a setpgid(0, 0)(与 相同setpgrp())来创建一个新的进程组,您会看到它正在执行kill(316060, SIGTERM)Kill sleep 2,而且还kill(0, SIGTERM)杀死了它自己的进程组(它之前创建的),这也会杀死由它生成的进程sleep 2(如果有)(在本例中没有) )。

现在,当在 shell 中启用作业控制时(例如交互或使用-m/-o monitor选项调用时),在:

timeout 1 sleep 2 | sleep 3

shell 已经启动timeoutsleep 3在一个新的进程组中,并且在大多数 shell 中,进程组领导者将是运行的进程组timeout

因此,当timeout执行时setpgrp(),与 相同setpgid(0, 0),它不会创建新的进程组,而只是成为空操作。因此,进程组仍将包含timeout它生成的要运行的进程sleep 2,并且sleep 3该进程是由 shell 预先放置在那里的。

~$ strace -fze  '/[sg]etpg|exec|kill|exit' sh -o monitor  -c 'timeout 1 sleep 2 | sleep 3'
execve("/usr/bin/sh", ["sh", "-o", "monitor", "-c", "timeout 1 sleep 2 | sleep 3"], 0x7ffeb8e13640 /* 69 vars */) = 0
getpgrp()                               = 318590
setpgid(0, 318593)                      = 0
strace: Process 318594 attached
[pid 318593] setpgid(318594, 318594)    = 0
[pid 318594] setpgid(0, 318594)         = 0
strace: Process 318595 attached
[pid 318595] setpgid(0, 318594)         = 0
[pid 318593] setpgid(318595, 318594)    = 0
[pid 318595] execve("/usr/bin/sleep", ["sleep", "3"], 0x557fde7871c8 /* 69 vars */) = 0
[pid 318594] execve("/usr/bin/timeout", ["timeout", "1", "sleep", "2"], 0x557fde7873a8 /* 69 vars */) = 0
[pid 318594] setpgid(0, 0)              = 0
strace: Process 318596 attached
[pid 318596] execve("/usr/bin/sleep", ["sleep", "2"], 0x7ffc60f34c60 /* 69 vars */) = 0
[pid 318594] --- SIGALRM {si_signo=SIGALRM, si_code=SI_TIMER, si_timerid=0, si_overrun=0, si_int=0, si_ptr=NULL} ---
[pid 318594] kill(318596, SIGTERM)      = 0
[pid 318596] --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=318594, si_uid=1000} ---
[pid 318596] +++ killed by SIGTERM +++
[pid 318594] kill(0, SIGTERM <unfinished ...>
)                                       = 0
[pid 318595] --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=318594, si_uid=1000} ---
[pid 318594] --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=318594, si_uid=1000} ---
[pid 318595] +++ killed by SIGTERM +++
[pid 318594] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_KILLED, si_pid=318596, si_uid=1000, si_status=SIGTERM, si_utime=0, si_stime=0} ---
[pid 318593] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_KILLED, si_pid=318595, si_uid=1000, si_status=SIGTERM, si_utime=0, si_stime=0} ---
Terminated
[pid 318594] kill(318596, SIGCONT)      = 0
[pid 318594] kill(0, SIGCONT)           = 0
[pid 318594] --- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=318594, si_uid=1000} ---
[pid 318594] +++ exited with 124 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=318594, si_uid=1000, si_status=124, si_utime=0, si_stime=0} ---
setpgid(0, 318590)                      = 0
+++ exited with 143 +++

这次,它kill(0, SIGTERM)会终止正在运行的进程sleep 3,因为它也在 318594 进程组中(由 shell 使用 放置在那里setpgid(318595, 318594))。

在:

sleep 2 | timeout 1 sleep 3 | sleep 4

你会发现管道持续了 4 秒,仅sleep 3在 1 秒后被杀死,因为此时 shell 创建的进程组是由正在运行的进程引导的sleep 2,因此timeout能够为自己(及其子进程)创建一个新的进程组sleep 3)这与 shell 为管道中的 3 个命令创建的进程组不同(因此,您会发现Ctrl+CCtrl+在运行Z时无法正常工作,因为未在前台进程组中运行)。timeouttimeout

当使用 运行时--foregroundtimeout会跳过创建额外的进程组,并且不会执行kill(0, SIGTERM)杀死自己的进程组的操作,因此行为更加一致,但意味着孙进程不会被杀死。

$ strace -fze  '/[sg]etpg|ioctl|exec|kill|exit' sh -o monitor  -c 'timeout --foreground 1 sh -c "sleep 2; exit" | sleep 3'
execve("/usr/bin/sh", ["sh", "-o", "monitor", "-c", "timeout --foreground 1 sh -c \"sl"...], 0x7ffe289c2910 /* 69 vars */) = 0
ioctl(10, TIOCGPGRP, [331754])          = 0
getpgrp()                               = 331754
setpgid(0, 331757)                      = 0
ioctl(10, TIOCSPGRP, [331757])          = 0
strace: Process 331758 attached
[pid 331757] setpgid(331758, 331758)    = 0
[pid 331758] setpgid(0, 331758)         = 0
[pid 331758] ioctl(10, TIOCSPGRP, [331758]) = 0
strace: Process 331759 attached
[pid 331757] setpgid(331759, 331758)    = 0
[pid 331759] setpgid(0, 331758)         = 0
[pid 331759] ioctl(10, TIOCSPGRP, [331758]) = 0
[pid 331759] execve("/usr/bin/sleep", ["sleep", "3"], 0x55dc52f76418 /* 69 vars */) = 0
[pid 331758] execve("/usr/bin/timeout", ["timeout", "--foreground", "1", "sh", "-c", "sleep 2; exit"], 0x55dc52f763a8 /* 69 vars */) = 0
strace: Process 331760 attached
[pid 331760] execve("/usr/bin/sh", ["sh", "-c", "sleep 2; exit"], 0x7fff756826c0 /* 69 vars */) = 0
strace: Process 331761 attached
[pid 331761] execve("/usr/bin/sleep", ["sleep", "2"], 0x557447790168 /* 69 vars */) = 0
[pid 331758] --- SIGALRM {si_signo=SIGALRM, si_code=SI_TIMER, si_timerid=0, si_overrun=0, si_int=0, si_ptr=NULL} ---
[pid 331758] kill(331760, SIGTERM)      = 0
[pid 331760] --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=331758, si_uid=1000} ---
[pid 331760] +++ killed by SIGTERM +++
[pid 331758] +++ exited with 124 +++
[pid 331757] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=331758, si_uid=1000, si_status=124, si_utime=0, si_stime=0} ---
[pid 331761] +++ exited with 0 +++
[pid 331759] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=331759, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
ioctl(10, TIOCSPGRP, [331757])          = 0
ioctl(10, TIOCSPGRP, [331754])          = 0
setpgid(0, 331754)                      = 0
+++ exited with 0 +++

sh被杀了,但没有sleep 2

这也解释了为什么在终端的交互式 shell 中:

sh -c 'timeout 10 cat; exit'

或者:

sleep 10 | timeout 10 cat /dev/tty

cat无法从终端读取。当它尝试时最终会被挂起,因为它位于新的进程组中,因此不再位于终端的前台进程组中。

同样,添加该--foreground选项可以避免该问题。

相关内容