如果分配了伪 tty,为啥通过 ssh 运行后台任务会失败?
Posted
技术标签:
【中文标题】如果分配了伪 tty,为啥通过 ssh 运行后台任务会失败?【英文标题】:Why does running a background task over ssh fail if a pseudo-tty is allocated?如果分配了伪 tty,为什么通过 ssh 运行后台任务会失败? 【发布时间】:2015-11-29 18:59:23 【问题描述】:我最近在通过 ssh 运行命令时遇到了一些奇怪的行为。我很想听听对以下行为的任何解释。
运行ssh localhost 'touch foobar &'
会按预期创建一个名为foobar
的文件:
[bob@server ~]$ ssh localhost 'touch foobar &'
[bob@server ~]$ ls foobar
foobar
但是运行相同的命令但使用-t
选项强制伪tty 分配无法创建foobar
:
[bob@server ~]$ ssh -t localhost 'touch foobar &'
Connection to localhost closed.
[bob@server ~]$ echo $?
0
[bob@server ~]$ ls foobar
ls: cannot access foobar: No such file or directory
我目前的理论是,因为触摸进程正在后台运行,所以在进程有机会运行之前分配和取消分配伪 tty。当然,添加一秒钟的睡眠可以让触摸按预期运行:
[bob@pidora ~]$ ssh -t localhost 'touch foobar & sleep 1'
Connection to localhost closed.
[bob@pidora ~]$ ls foobar
foobar
如果有人有明确的解释,我会很感兴趣。谢谢。
【问题讨论】:
我想就是这样。后台进程连接到 tty 并且 tty 死亡将其杀死。试试nohup touch foobar &
看看是否可行和/或touch foobar </dev/null >/dev/null 2>&1
。
ssh -t localhost 'touch foobar < /dev/null > /dev/null 2>&1 & '
和 ssh -t localhost 'nohup touch foobar & '
产生相同的行为。
他们无法创建文件?我得到了这里创建的文件,即使是为了记录的原始版本。
是的,这两个命令都没有创建文件。
touch foobar </dev/null >/dev/null 2>&1
不会阻止进程由其控制 TTY 发出信号。 nohup 情况可能是一种竞争条件——nohup
在阻止信号之前被发出信号。
【参考方案1】:
哦,这个不错。
这与进程组的工作方式、使用 -c
作为非交互式 shell 调用时 bash 的行为方式以及输入命令中 &
的效果有关。
答案假定您熟悉 UNIX 中作业控制的工作原理;如果你不是,这里是一个高级视图:每个进程都属于一个进程组(同一组中的进程通常作为命令管道的一部分放在那里,例如cat file | sort | grep 'word'
将放置运行cat(1)
的进程, sort(1)
和 grep(1)
在同一进程组中)。 bash
和其他进程一样,它也属于一个进程组。进程组是会话的一部分(会话由一个或多个进程组组成)。在一个会话中,最多有一个进程组,称为前台进程组,可能还有许多后台进程组。前台进程组拥有终端的控制权(如果有控制终端连接到会话);会话负责人 (bash) 使用 tcsetpgrp(3)
将进程从后台移动到前台,从前台移动到后台。发送到进程组的信号会传递到该组中的每个进程。
如果流程组和作业控制的概念对您来说是全新的,我认为您需要阅读它才能完全理解这个答案。 UNIX 环境中的高级编程(第 3 版)的第 9 章是一个很好的学习资源。
话虽如此,让我们看看这里发生了什么。我们必须将拼图的每一部分拼凑在一起。
在这两种情况下,ssh 远程端都会调用 bash(1)
和 -c
。 -c
标志导致 bash(1)
作为非交互式 shell 运行。从手册页:
交互式 shell 是在没有非选项参数的情况下启动的,并且 没有 -c 选项,其标准输入和错误都是 连接到终端(由 isatty(3) 确定),或者一个启动 使用 -i 选项。 PS1 已设置并且 $- 包括 i 如果 bash 是 交互式,允许一个 shell 脚本或一个启动文件来测试它 状态。
另外,重要的是要知道在非交互模式下启动 bash 时会禁用作业控制。这意味着 bash 不会创建单独的进程组来运行该命令,因为禁用了作业控制,因此无需在前台和后台之间移动此命令,因此它还不如保留在与 bash 相同的进程组中.无论您是否使用 -t
在 ssh 上强制分配 PTY,都会发生这种情况。
但是,使用&
具有导致shell 不等待命令终止的副作用(即使禁用了作业控制)。从手册页:
如果一个命令被控制操作符 & 终止,shell 在子shell 的后台执行命令。外壳可以 不等待命令完成,返回状态为 0。 用 ; 分隔的命令依次执行;外壳等待 每个命令依次终止。返回状态为退出 最后执行的命令的状态。
因此,在这两种情况下,bash 都不会等待命令执行,touch(1)
将在与bash(1)
相同的进程组中执行。
现在,考虑一下当会话负责人退出时会发生什么。引用 setpgid(2)
手册页:
如果会话具有控制终端,并且该会话的 CLOCAL 标志 未设置终端,并且发生终端挂断,则会话 领导者被发送一个 SIGHUP。 如果会话负责人退出,则 SIGHUP 信号也会被发送到前台进程中的每个进程 控制终端组。
(强调我的)
当你不使用-t
当你不使用-t
时,远程端没有PTY分配,所以bash不是会话负责人,实际上并没有创建新的会话。因为 sshd 作为守护进程运行,所以 fork + exec()'d 的 bash 进程将没有控制终端。因此,即使 shell 很快终止(可能在 touch(1)
之前),也没有 SIGHUP
发送到进程组,因为 bash 不是会话负责人(并且没有控制终端)。所以一切正常。
当您使用-t
-t
强制PTY分配,也就是说ssh远程端会调用setsid(2)
,分配一个伪终端+用forkpty(3)
fork一个新进程,连接PTY主设备输入输出到socket端点这导致你的机器,最后执行bash(1)
。 forkpty(3)
在会变成 bash 的 fork 进程中打开 PTY 从端;由于当前会话没有控制终端,并且正在打开终端设备,因此 PTY 设备成为会话的控制终端,而 bash 成为会话领导者。
然后同样的事情再次发生:touch(1)
在同一个进程组中执行,等等,yadda yadda。重点是,这一次,有一个会话领导者和一个控制终端。因此,由于 bash 不会因为&
而烦恼等待,所以当它退出时,SIGHUP
被传递到进程组,touch(1)
过早死亡。
关于nohup
nohup(1)
在这里不起作用,因为仍然存在竞争条件。如果bash(1)
在nohup(1)
有机会设置必要的信号处理和文件重定向之前终止,它将不起作用(这可能是发生的情况)
一种可能的解决方法
强制重新启用作业控制可以修复它。在 bash 中,您可以使用 set -m
执行此操作。这有效:
ssh -t localhost 'set -m ; touch foobar &'
或者强制 bash 等待 touch(1)
完成:
ssh -t localhost 'touch foobar & wait `pgrep touch`'
【讨论】:
感谢您给出如此清晰详细的解释,非常感谢。 @user414310 很高兴我能帮上忙。我得说,我喜欢这个问题。谢谢你提出这么好的问题:)【参考方案2】:@Filipe Gonçalves 的答案很棒,但它有问题。我没有足够的声誉在那里发表评论,所以我在这里更正/丰富内容:
当你不使用 -t 时,
@Filipe 说:
不使用 -t 时,远程端没有 PTY 分配,所以 bash 不是会话负责人,实际上不会创建新会话。 ...
实际上,bash 是一个会话负责人,并创建了新会话。
让我们测试一下:
# run sleep background process first, then call ps directly:
[root@90fb1c3f30ce ~]# ssh localhost 'sleep 66 & ps -o pid,ppid,pgid,sess,tpgid,tty,args'
PID PPID PGID SESS TPGID TT COMMAND
184074 67 184074 184074 -1 ? sshd: root@notty
184076 184074 184076 184076 -1 ? bash -c sleep 66 & ps -o pid,ppid,pgid,sess,tpgid,tty,args
184081 184076 184076 184076 -1 ? sleep 66
184082 184076 184076 184076 -1 ? ps -o pid,ppid,pgid,sess,tpgid,tty,args
Notice ^^^^^ ^^^^^
我们可以看到这些 bash/sleep/ps 进程具有相同的 PGID/SESS 等于 bash 进程的 PID 184076,但是 sshd 父进程具有不同的 PGID /SESS。这里,bash 进程是新会话的领导者,而 bash/sleep/ps 进程属于另一个进程组。
另外,我们可以发现 ssh 命令并没有立即返回,它仍然等待大约 66 秒。你可以在这里找到它的原因:Getting ssh to execute a command in the background on target machine
在ssh命令等待期间,我们可以打开另一个会话并运行:
[root@90fb1c3f30ce ~]# ps -eo pid,ppid,pgid,sess,tpgid,tty,args
PID PPID PGID SESS TPGID TT COMMAND
# unrelated lines removed #
184074 67 184074 184074 -1 ? sshd: root@notty
184081 1 184076 184076 -1 ? sleep 66
Notice ^^^^^ ^^^^^
[root@90fb1c3f30ce ~]# ps -e | grep 184076
[root@90fb1c3f30ce ~]#
我们可以看到bash进程(pid 184076)已经走了,但是sleep后台进程的PGID/SESS保持不变。没关系,APUE session 9.4:
每个过程组可以有一个过程组负责人。领导者由其进程组 ID 标识,该 ID 等于其进程 ID。
进程组负责人可以创建进程组,在组中创建进程,然后终止。进程组仍然存在,只要组中至少有一个进程,无论组长是否终止。
那么,为什么这个睡眠进程不会死掉呢?
不使用-t时,远程端没有PTY分配,所以远程端的进程组不是前台进程组(没有终端,没有前台或后台的意义)。因此,即使 shell 很快终止,也没有 SIGHUP 发送到它的进程组,因为进程组不是前台进程组。 (SIGHUP 信号将发送到控制终端的前台进程组中的每个进程)。
【讨论】:
【参考方案3】:关键是将子进程的 stdin/stdout/stderr 流与原始 bash/ssh 会话解耦;然后不再需要伪 tty 分配 (ssh -t
) 以允许子进程在 ssh 连接终止后幸存下来。完整答案见here...
【讨论】:
以上是关于如果分配了伪 tty,为啥通过 ssh 运行后台任务会失败?的主要内容,如果未能解决你的问题,请参考以下文章
结束 ssh 会话后在后台运行 python/matplotlib 的问题