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

通过 ssh 在主机上面执行命令时,遇到 ssh 连接不会自动断开,在程序结束时才断开的情况。如何才能让 ssh 执行完命令后自动退出?以及这种问题该如何分析?背后的原理又是什么?现象一般情况下,执行简单的命令时,会在命令执行完成后,立刻返回ssh ucloud ps当执行的程序是堵塞的时nohup

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

现象

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

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

效果如下:

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

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

正确的姿势应该是

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

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

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)来执行传递过来的命令。

$ 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 的相应文件描述符联系起来。可以通过如下的脚本来查看上述对应关系:

$ 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。

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

参考:

Read more

容器镜像(4):镜像的常用工具箱

容器镜像(4):镜像的常用工具箱

前几篇在讲多架构镜像时已经用过 skopeo 和 crane 做镜像复制,这篇系统整理这两个工具的完整能力,同时介绍几个日常操作镜像时同样好用的工具。 一、skopeo:不依赖 Daemon 的镜像瑞士军刀 skopeo 的核心价值是绕过 Docker daemon,直接与 Registry API 交互。上一篇用它做镜像复制和离线传输,但它的能力远不止于此。 1.1 安装 # Ubuntu / Debian sudo apt install -y skopeo skopeo --version # skopeo version 1.15.1 1.2 inspect:免拉取检查镜像元数据 docker inspect 需要先把镜像拉到本地,skopeo inspect 直接向 Registry

容器镜像(3):多架构镜像构建

容器镜像(3):多架构镜像构建

一、什么是多架构镜像 1.1 OCI Image Index 上一篇介绍了单平台镜像的结构:一个 Manifest 指向 Config 和若干 Layer blob。多架构镜像在此之上多了一层——OCI Image Index(也叫 Manifest List),是一个轻量的索引文件,把多个单平台 Manifest 组织在一起: $ docker manifest inspect golang:1.22-alpine { "schemaVersion": 2, "mediaType": "application/vnd.oci.image.index.v1+json", "manifests&

容器镜像(2):containerd 视角下的镜像

容器镜像(2):containerd 视角下的镜像

一、为什么需要了解 containerd 如果你只用 docker run 跑容器,从来不关心底层,那可以不了解 containerd。但如果你在用 Kubernetes,或者想真正理解"容器运行时"是什么,containerd 是绕不开的。 事实上,当你执行 docker run 的时候,containerd 早就在后台悄悄工作了——Docker 从 1.11 版本开始,就把核心运行时剥离出来交给 containerd 负责。 1.1 Docker 的架构演变 早期的 Docker(1.10 及之前)是一个"大一统"的单体程序:一个 dockerd