如果分配了伪tty,为什么在ssh上运行后台任务会失败?

时间:2015-09-03 19:41:21

标签: bash ssh jobs tty pty

在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

我目前的理论是,由于触摸过程正在被覆盖,因此伪进程在进程有机会运行之前被分配和未分配。当然,添加一秒睡眠可让触摸按预期运行:

[bob@pidora ~]$ ssh -t localhost 'touch foobar & sleep 1'
Connection to localhost closed.
[bob@pidora ~]$ ls foobar
foobar

如果有人有明确的解释,我会非常有兴趣听到它。感谢。

2 个答案:

答案 0 :(得分:36)

哦,那是一个很好的。

这与进程组的工作方式,bash在作为具有-c的非交互式shell调用时的行为方式以及&在输入命令中的效果有关。

答案假设您熟悉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已设置且$ - 如果bash为,则包含i   交互式,允许shell脚本或启动文件来测试它   状态。

此外,重要的是要知道在非交互模式下启动bash时作业控制被禁用。这意味着bash不会创建一个单独的进程组来运行该命令,因为禁用了作业控制,不需要在前台和后台之间移动此命令,因此它可能只是保留在与bash相同的进程组中。无论您是否使用-t强制在ssh上进行PTY分配,都会发生这种情况。

但是,使用&会导致shell不等待命令终止(即使禁用了作业控制)。从联机帮助页:

  

如果命令由控制操作员&,shell终止   在子shell中在后台执行命令。外壳确实如此   不等待命令完成,返回状态为0。   由a分隔的命令;按顺序执行; shell等待   为每个命令轮流终止。返回状态是退出   最后一个命令的执行状态。

因此,在这两种情况下,bash都不会等待命令执行,而touch(1)将在与bash(1)相同的进程组中执行。

现在,考虑会话领导者退出时会发生什么。引自setpgid(2)联机帮助页:

  

如果会话具有控制终端,并且具有CLOCAL标志   终端未设置,终端发生挂断,然后是会话   领导者发送了一个SIGHUP。 如果会话负责人退出,则为SIGHUP   信号也将被发送到前台进程中的每个进程   控制终端组

(强调我的)

当您不使用-t

当您不使用-t时,远程端没有PTY分配,因此bash不是会话负责人,实际上没有创建新会话。因为sshd作为守护进程运行,所以分叉+ exec()的bash进程将没有控制终端。因此,即使shell很快终止(可能在touch(1)之前),也没有SIGHUP发送到进程组,因为bash不是会话领导者(并且没有控制终端) )。所以一切正常。

使用-t

-t强制PTY分配,这意味着ssh远程端会调用setsid(2),用forkpty(3)分配一个伪终端+ fork一个新进程,连接PTY主设备输入并输出到通向您机器的套接字端点,最后执行bash(1)forkpty(3)在forked进程中打开PTY slave端,这将成为bash;由于当前会话没有控制终端,终端设备正在打开,因此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`'

答案 1 :(得分:-1)

关键是将子进程的stdin / stdout / stderr流与原始bash / ssh会话解耦;那么就不再需要伪tty分配(ssh -t),以允许子代在ssh连接终止后继续生存。有关完整答案,请参见here