我不明白为什么执行以下脚本的行为:
$ cat z.sh
saved_stty=$(stty -g)
echo "saved_stty: ${saved_stty}"
stty "${saved_stty}"
$ ./z.sh
saved_stty: 500:5:bf:8a3b:3:1c:7f:15:4:0:1:0:11:13:1a:0:12:f:17:16:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0
$ echo "${?}"
0
在超时调用 stty 后发生变化:
$ cat z.sh
saved_stty=$(stty -g)
echo "saved_stty: ${saved_stty}"
timeout 10 stty "${saved_stty}"
$ ./z.sh
saved_stty: 500:5:bf:8a3b:3:1c:7f:15:4:0:1:0:11:13:1a:0:12:f:17:16:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0
$ echo "${?}"
124
$ . ./z.sh
saved_stty: 500:5:bf:8a3b:3:1c:7f:15:4:0:1:0:11:13:1a:0:12:f:17:16:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0
$ echo "${?}"
0
在第一种情况下,调用./z.sh
立即完成(返回值 0),而在第二种情况下,调用./z.sh
需要 10 秒(并且超时,返回值 124)。但是,调用. ./z.sh
仍然立即完成(返回值 0)。
通过调试 stty,我观察到执行在系统调用处挂起
return INLINE_SYSCALL (ioctl, 3, fd, cmd, &k_termios);
int __tcsetattr (int fd, int optional_actions, const struct termios *termios_p)
在的函数中glibc/sysdeps/unix/sysv/linux/tcsetattr.c
.
我已经展示了一个问题的最小化示例。现在让我把它说得更现实一些:
$ cat z.scala
object Test {
def main(args: Array[String]): Unit = {
var j = 0
val k = 1000
for (i <- 1 to k) {
for (i <- 1 to k) {
j += i
}
}
println(j)
}
}
$ cat run.sh
timeout 10 scala ./z.scala
echo "${?}"
$ ./run.sh
500500000
124
$ cat z.scala
object Test {
def main(args: Array[String]): Unit = {
var j = 0
val k = 1000000
for (i <- 1 to k) {
for (i <- 1 to k) {
j += i
}
}
println(j)
}
}
$ cat run.sh
timeout --foreground 10 scala ./z.scala
echo "${?}"
$ ./run.sh
124
scala 的第一次调用会立即计算结果,但会在 stty 的内部调用中挂起,直到超时。第二次调用 scala 应该会因超时而终止,但执行 scala 的进程在脚本返回后仍在运行。这与 的文档相匹配timeout --foreground
,但我仍然想知道如何正确地使 scala 超时。
答案1
发生这种情况是因为timeout
该命令在后台、单独的进程组中运行。
当进程 a) 连接到控制终端且 b) 不在前台组中并尝试使用 更改终端设置时tcsetattr()
,它会收到一个SIGTTOU
停止它的信号。
这正是您的示例中发生的情况。
GNUtimeout
有一个选项:--foreground
,您可以安全地与试图干扰控制终端的简单程序一起使用它,但不要分叉,因为这样他们的孩子就不会被杀死。
可能的解决方法是
A)从其他地方重定向程序的 stdin/stdout/stderr,希望它们不会/dev/tty
显式打开并单独保留控制终端
b)如果他们不能被 a) 说服,那么给他们自己的伪终端来运行,你可以使用像script(1)
.请注意,您必须将 a) 处理应用于script(1)
,因为它尝试从 stdin 读取(这将获得一个SIGTTIN
信号),并且它尝试将其变成原始数据tcseattr()
(这将获得一个SIGTTOU
信号):
% cat <<'EOT' > sample.sh; chmod +x sample.sh
#!/bin/sh
t=$(stty -g -F /dev/tty)
sleep 1000 &
echo BEFORE; stty -F /dev/tty "$t"; echo AFTER
EOT
% sh -c 'timeout 2 ./sample.sh'
BEFORE
# hangs for 2 seconds and exits without writing 'AFTER'
%
% sh -c 'timeout 2 script /dev/null </dev/null -qc ./sample.sh'
BEFORE
AFTER
# and it exits immediately
% pgrep sleep
# nothing, the child was killed too
请注意,这script(1)
不是标准化的,并且它的语法在除 Linux 之外的其他系统上也不同,因此您必须调整该示例。
C)如果你有 systemd,使用systemd-run -t --user
which(与 不同timeout
)能够捕获并杀死该命令生成的任何子进程,即使它们试图逃离其进程组或会话。