超时 stty 挂起

超时 stty 挂起

我不明白为什么执行以下脚本的行为:

$ 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 --userwhich(与 不同timeout)能够捕获并杀死该命令生成的任何子进程,即使它们试图逃离其进程组或会话。

相关内容