在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
如果有人有明确的解释,我会非常有兴趣听到它。感谢。
答案 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。