为什么SSH执行命令不会退出

通过 ssh 在主机上面执行命令时,遇到 ssh 连接不会自动断开,在程序结束时才断开的情况。如何才能让 ssh 执行完命令后自动退出?以及这种问题该如何分析?背后的原理又是什么?

现象

一般情况下,执行简单的命令,会在命令执行完成后,立刻返回;若执行的程序堵塞,ssh 会一直不返回,直到堵塞的程序自己结束,才会断开 ssh 连接。

1
2
3
ssh devcloud pwd
ssh devcloud sleep 3s
ssh devcloud java -jar balabala.jar

效果如下:

同时还可以发现平时常用的 nohup + & 没有作用:

1
2
ssh devcloud "nohup sleep 3s &"
ssh devcloud "nohup java -jar balabala.jar &"

正确的姿势应该是

1
2
ssh devcloud "sleep 3s >/dev/null &"
ssh devcloud "java -jar balabala.jar >/dev/null 2>&1 &"

且调换 >/dev/null2>&1 的顺序,会导致 ssh 命令不会退出。

1
2
ssh devcloud "sleep 3s 2>&1 >/dev/null &"
ssh devcloud "java -jar balabala.jar 2>&1 >/dev/null &"

ssh 执行远程命令的过程

一般通过 ssh 远程执行某个命令时,会在远程机器上先建立一个 sshd 的子进程(父进程是 sshd daemon),然后由这个 sshd 进程启动一个 bash 进程(取决于该用户所设置的默认 shell)来执行传递过来的命令。

1
2
3
4
5
6
7
8
9
10
$ ssh devcloud "ps -ef | grep -v grep | grep sshd"
root 9425 1 0 2020 ? 00:00:07 /usr/sbin/sshd -D
root 10130 9425 0 13:49 ? 00:00:00 sshd: root@notty
root 10633 9425 0 13:51 ? 00:00:00 sshd: root@notty
$ ssh devcloud "ps -ef | grep -v grep | grep sleep"
root 10132 10130 0 13:49 ? 00:00:00 sleep 1000s
root 10559 8630 0 13:51 ? 00:00:00 sleep 60
$ ssh devcloud "pstree -p 9425"
sshd(9425)-+-sshd(10130)---sleep(10132)
`-sshd(10642)---pstree(10645)

针对这次任务建立的 sshd 进程和 bash 进程在文件描述符方面有一定关系:bash 进程的 012 三个文件描述符通过管道与 sshd 的相应文件描述符联系起来。可以通过如下的脚本来查看上述对应关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ ssh devcloud "TMPSPID=\$(ps -ef | grep -v grep | grep -e 'sshd.*notty' | awk '{print \$2}');echo SSHD子进程:\$TMPSPID;ps -ef | grep -v grep | grep sshd;echo ;ls -l /proc/\$TMPSPID/fd;echo ;echo SHELL 进程:\$\$;ps -ef | grep \$\$;echo ;ls -l /proc/\$\$/fd"
SSHD子进程:688
root 688 9425 0 15:49 ? 00:00:00 sshd: root@notty
root 9425 1 0 2020 ? 00:00:07 /usr/sbin/sshd -D

总用量 0
lrwx------ 1 root root 64 4月 29 15:49 0 -> /dev/null
lrwx------ 1 root root 64 4月 29 15:49 1 -> /dev/null
l-wx------ 1 root root 64 4月 29 15:49 11 -> pipe:[2517193862]
lr-x------ 1 root root 64 4月 29 15:49 12 -> pipe:[2517193863]
lr-x------ 1 root root 64 4月 29 15:49 14 -> pipe:[2517193864]
lrwx------ 1 root root 64 4月 29 15:49 2 -> /dev/null
lrwx------ 1 root root 64 4月 29 15:49 3 -> socket:[2517182996]
lrwx------ 1 root root 64 4月 29 15:49 4 -> socket:[2517189863]
lr-x------ 1 root root 64 4月 29 15:49 5 -> pipe:[2517189866]
l-wx------ 1 root root 64 4月 29 15:49 6 -> /run/systemd/sessions/360922.ref
l-wx------ 1 root root 64 4月 29 15:49 7 -> pipe:[2517189866]

SHELL进程:690
root 690 688 0 15:49 ? 00:00:00 zsh -c TMPSPID=$(ps -ef | grep -v grep | grep -e 'sshd.*notty' | awk '{print $2}');echo SSHD子?程:$TMPSPID;ps -ef | grep -v grep | grep sshd;echo ;ls -l /proc/$TMPSPID/fd;echo ;echo SHELL?程:$$;ps -ef | grep $$;echo ;ls -l /proc/$$/fd
root 699 690 0 15:49 ? 00:00:00 ps -ef
root 700 690 0 15:49 ? 00:00:00 grep 690
root 26903 1 0 2020 ? 01:18:33 /usr/local/sa/agent/plugins/sap1015

总用量 0
lr-x------ 1 root root 64 4月 29 15:49 0 -> pipe:[2517193862]
l-wx------ 1 root root 64 4月 29 15:49 1 -> pipe:[2517193863]
l-wx------ 1 root root 64 4月 29 15:49 2 -> pipe:[2517193864]
lr-x------ 1 root root 64 4月 29 15:49 3 -> /proc/690/fd

可以看出 shell 进程的 012 文件描述符均来自 sshd 进程的管道。

如果远程执行的命令是后台执行,那么新启动的命令的父进程成了 1,而输入即描述符 0 重定向到了 /dev/null

nohup 的误解

可以从 man nohup 中看到这个命令说明。其中它的作用是将命令运行成不受 SIGHUP 信号的模式;它主要做的事为如下 3 点:

  • 若标准输入是终端,将它重定向到一个不可达的文件(/dev/null)
  • 若标准输出是终端,将它重定向到 nohup.out 文件。若当前目录的 nohup.out 不可用,则使用 ${HOME}/nohup.out 文件。
  • 若标准错误是终端,将它重定向到标准输出。

也就是说,如果标准输入不是终端的话,那么 nohup 将不会发挥出作用。因此,在上述上下文里面,nohup 是可以不要的。

SIGHUP 信号是当终端关闭时,发送给该终端所控制的进程。当进程收到这个信号后,默认操作为终止进程;但是也可以对此信号做捕捉,比如 wget 能捕获SIGHUP 信号,并忽略它,这样就算退出了 Linux 登录,wget 也 能继续下载。

那 ssh 执行命令,能获取到终端吗?

先来一个简单的表述:

  • TTY。TeleTypeWriter,通过 console 登录。
  • pts。pseudo terminal slave,通过 ssh 登录。
  • notty。通过 SFTP 或其他不需要终端的方式登录。

通过 ssh 执行命令获取到的是 notty。

终端和我们想象中的终端可能有一些差别,内容也非常长,可以参考下面的文章进行详细的了解:

参考:

为什么SSH执行命令不会退出

https://eucham.me/2021/04/22/689e71ecdbb3.html

作者

遇寻

发布于

2021-04-22

更新于

2022-04-21

许可协议

评论