ssh远程执行nohup命令不退出

介绍


Linux系统下,使用默认用户root
远程target机器的主目录下有个脚本test.sh,可执行权限,内容只有一条命令:sleep 10

在本地机器上执行

ssh target "nohup ./test.sh &"

结果ssh不立即退出,等test.sh执行完毕之后才退出。

问题


一般我们使用nohup命令是为了在断开到某个服务器的ssh连接之后,之前执行的命令仍然正常地在服务器运行。
但是前面的现象其实与nohup命令没有什么关系,只是ssh本身的问题;nohup其作用的前提是用户使用ssh登录到服务器上。
至于跟nohup扯上关系,我猜是因为在大家的印象中上面这种nohup命令的执行方式应该是立即退出的,结果反差太大,所以当作了一个特别问题。

解决方法


手动在命令里面指定重定向
即上面的命令换成

ssh target "nohup ./test.sh >/dev/null 2>&1 &"

然后就OK

下面的分析表明了nohup命令与“ssh host “cmd””方式的ssh命令没有任何关系(因为这种方式不会涉及SIGHUP),所以换成

ssh target "./test.sh >/dev/null 2>&1 &" 就可以了。

分析


一般处理ssh远程执行某个命令的任务,在远程目标机器上先建立一个sshd的子进程(父进程是最初始的sshd),然后由这个sshd进程启动一个bash进程(如果使用bash进程)来执行传递过来的命令。 针对这次任务建立的sshd进程和bash进程在文件描述符方面有一定关系:通常bash进程的0 1 2三个文件描述符通过管道与sshd的相应文件描述符联系起来。这可以通过查找建立的sshd进程和bash进程在/proc文件系统下的相应进程的fd目录的详细情况。ssh远程执行命令这种建立ssh连接的方式在ps -ef 中显示的sshd进程是有”sshd root@notty”标记。此sshd进程的命令可以通过命令“ps -ef | grep -v grep | grep ‘sshd.*notty’ | awk ‘{print KaTeX parse error: Expected ‘EOF’, got ‘}’ at position 2: 2}̲’”得到,而相关bash进程的…$获取。远程执行下面命令可以一步到位,得到比较结果:

ssh target "TMPSPID=\$(ps -ef | grep -v grep | grep  -e 'sshd.*notty' | awk '{print \$2}');echo \$TMPSPID;ls -l /proc/\$TMPSPID/fd;echo \$\$;ls -l /proc/\$\$/fd" 

如果远程执行的命令是后台执行,那么可以发现新启动的bash进程的父进程成了1,而输入即描述符0重定向到了/dev/null。 nohup是防止进程被SIGHUP信号中断,正常使用的时候也会进行一些重定向操作,即当标准输入/输出/错误等是终端的时候,会对它们进行重定向。但是ssh远程执行命令时,这些条件都不满足,因为文件描述符0,1,2(正常情况下)都被重定向到管道了。所以远程执行nohup时不会进行相关重定向操作。而当远程执行后台命令的时候,虽然标准输入被重定向到了/dev/null,但是标准输出和错误还是管道, 所以针对这次任务启动的sshd进程还不会结束。所以执行远程命令时,还必须自己在命令行上重定向标准输出和标准错误才行。
对于上面的test.sh脚本,下面给出几种命令执行执行方式:

ssh target "./test.sh"           # 等待命令完成后退出;本地Ctrl+C中断ssh会话,不会中断test.sh的执行(bash父进程变为1)(与登录终端执行命令而终端连接断开时的行为不一样)
ssh target "./test.sh &"        # 等待命令完成后退出;本地Ctrl+C中断ssh会话,不会中断test.sh的执行(bash父进程本来就为1)
ssh target "nohup ./test.sh &"  # 等待命令完成后退出;本地Ctrl+C中断ssh会话,不会中断test.sh的执行(bash父进程本来就为1)
ssh target "nohup ./test.sh >/dev/null 2>&1 &"  # 启动test.sh执行后就会退出(bash父进程本来就为1)
ssh target "./test.sh >/dev/null 2>&1 &"              # 启动test.sh执行后就会退出(bash父进程本来就为1),这也表明ssh不退出与nohup命令本身没有什么关系

实际上如先ssh登录target,执行./test.sh &,然后正常退出ssh(即exit命令),那么./test.sh这个脚本也不会终止,而且会将父进程换成1;如果不正常退出,而是直接关闭连接,那么会导致./test.sh任务终止。

补充:


感觉上面的分析还不是很到位,因为简单命令还不能够显示出真实情况,比如执行

ssh target "./test.sh"

在远程机器上执行“ps -ef | grep ‘test|notty’”命令,结果如下

ps -ef | grep 'test\|notty'
root     35929  3306  0 19:20 ?        00:00:00 sshd: root@notty
root     35931 35929  0 19:20 ?        00:00:00 /bin/bash ./test.sh”

好像执行./test.sh的bash进程直接由显示的sshd进程创建,其实情况应该不是这样的。先执行一个稍复杂的命令:

ssh target "for w in a b c; do ./test.sh; done"

同样使用上面的查看命令可以看到如下结果:

ps -ef | grep 'test\|notty'
root     36219  3306  0 19:29 ?        00:00:00 sshd: root@notty
root     36221 36219  0 19:29 ?        00:00:00 bash -c for w in a b c; do ./test.sh; done
root     36228 36221  0 19:29 ?        00:00:00 /bin/bash ./test.sh

这就表明了其实有两层进程关系,sshd – bash – c – bash,即sshd 先创建一个bash以bash -c的方式执行传递过来的作为命令的字符串,然后再由这个bash创建执行./test.sh脚本的子bash进程(这个可以创建多个)。而本地执行ssh host “cmd”形式命令要能迅速返回,必须满足的条件是:该命令对象的sshd进程(一般是sshd: root@notty),没有子进程需要等待结束(靠将第一个bash搞成后台进程,或者第一个bash会立即执行完命令自然退出——即它启动一些后台子进程), 而且没有其他进程与它有管道连接关系(靠重定向解决,在第一个bash处或者所有第二层bash处都可以)。简而言之,要ssh host “cmd”形式命令立即返回,在整个命令最后面添加“>/dev/null 2>&1 &”,是有保证的。注意,对于组合的命令, 可能需要放到{}中才行,比如“{ cmd; } >/dev/null 2>&1 &”这样的形式。这是因为重定向只对单个简单命令或单个复合命令有效。
下面通过一些实际例子的情况帮助大家认识(/dev/null也可以是某个本地文件):

ssh target "for w in a b c; do ./test.sh >/dev/null 2>&1 0</dev/null; done"

ssh不返回,./test.sh一个一个启动

ssh target "for w in a b c; do ./test.sh >/dev/null 2>&1 0</dev/null; done &"

ssh 不返回,./test.sh一个一个启动。 第一个bash(由sshd启动的bash -c)是后台执行的,但是文件描述符1和2还与sshd有管道连接,所以不返回

ssh target "for w in a b c; do ./test.sh >/dev/null 2>&1 0</dev/null; done >/dev/null 2>&1 &"

ssh立即返回,./tesh.sh一个接一个启动

ssh target "for w in a b c; do ./test.sh ; done >/dev/null 2>&1 &"

ssh立即返回,./test.sh一个接一个启动;由于执行./test.sh的bash是由第一个bash启动的,而第一个bash执行了重定向,所以该bash也继承了这些重定向,换言之这条命令与上条命令的效果一样,即内层的./test.sh无需重定向了

ssh target "for w in a b c; do ./test.sh & done >/dev/null 2>&1 &"

ssh立即退出,./test.sh全部启动,第一个bash也退出了

ssh target "for w in a b c; do ./test.sh & done >/dev/null 2>&1"

ssh立即退出(为什么?因为第一个bash启动三个./tesh.sh的bash进程后,退出了;而执行./test.sh的bash进程因为继承了父bash的文件描述符,所以没有管道与sshd连接,因此ssh退出

ssh target "for w in a b c; do ./test.sh & done "

ssh不返回,因为执行./test.sh与sshd还有管道连接

ssh target "for w in a b c; do ./test.sh >/dev/null 2>&1 0</dev/null & done" 

ssh返回,因为第一个bash启动三个子bash之后结束,而子bash与sshd之间又没有管道上连接

ssh target "for w in a b c; do ./test.sh >/dev/null 2>&1 0</dev/null & done &"

同上,因为第一个bash启动所有子进程后会退出,此时将第一个bash作为后台进程已经意义不大

ssh target "./test.sh && ./test.sh >/dev/null 2>&1 &"

ssh不会返回

ssh target "{ ./test.sh && ./test.sh; } >/dev/null 2>&1 &" 

ssh会返回

版权声明:本文为作者原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原创文章,作者:老C,如若转载,请注明出处:https://www.code404.icu/955.html

发表评论

登录后才能评论