如果同一个 shell 启动了多个连接,则后台运行的 SSH 连接不会退出

ter*_*don 9 shell ssh terminal stdin

显然,如果同一个 shell 启动到同一个服务器的多个 ssh 连接,它们在执行给定的命令后不会返回,但会永远挂起 ( Stopped (tty input))。为了显示:

#!/bin/bash
ssh localhost sleep 2
echo "$$ DONE!"
Run Code Online (Sandbox Code Playgroud)

如果我在后台多次运行上面的脚本,它永远不会退出:

$ for i in {1..3}; do foo.sh & done
[1] 28695
[2] 28696
[3] 28697
$                      ## Hit enter

[1]   Stopped                 foo.sh

[2]-  Stopped                 foo.sh

[3]+  Stopped                 foo.sh
$                      ## Hit enter again        
$ jobs -l
[1]  28695 Stopped (tty input)     foo.sh
[2]- 28696 Stopped (tty input)     foo.sh
[3]+ 28697 Stopped (tty input)     foo.sh
Run Code Online (Sandbox Code Playgroud)

细节

  • 我发现这个是因为我在 Perl 脚本中 ssh'ing 来运行命令。使用 Perlsystem()调用 launch时会发生相同的行为ssh。
  • 使用 Perl 模块而不是system(). 我试过了Net::SSH::Perl,Net:SSH2而且Net::OpenSSH。
  • 如果我从不同的 shell(打开多个终端)运行多个 ssh 命令,它们会按预期工作。
  • ssh 连接调试信息中没有明显有用的内容:

    OpenSSH_7.5p1, OpenSSL 1.1.0f  25 May 2017
    debug1: Reading configuration data /home/terdon/.ssh/config
    debug1: Reading configuration data /etc/ssh/ssh_config
    debug2: resolving "localhost" port 22
    debug2: ssh_connect_direct: needpriv 0
    debug1: Connecting to localhost [::1] port 22.
    debug1: Connection established.
    debug1: identity file /home/terdon/.ssh/id_rsa type 1
    debug1: key_load_public: No such file or directory
    debug1: identity file /home/terdon/.ssh/id_rsa-cert type -1
    debug1: key_load_public: No such file or directory
    debug1: identity file /home/terdon/.ssh/id_dsa type -1
    debug1: key_load_public: No such file or directory
    debug1: identity file /home/terdon/.ssh/id_dsa-cert type -1
    debug1: key_load_public: No such file or directory
    debug1: identity file /home/terdon/.ssh/id_ecdsa type -1
    debug1: key_load_public: No such file or directory
    debug1: identity file /home/terdon/.ssh/id_ecdsa-cert type -1
    debug1: key_load_public: No such file or directory
    debug1: identity file /home/terdon/.ssh/id_ed25519 type -1
    debug1: key_load_public: No such file or directory
    debug1: identity file /home/terdon/.ssh/id_ed25519-cert type -1
    debug1: Enabling compatibility mode for protocol 2.0
    debug1: Local version string SSH-2.0-OpenSSH_7.5
    debug1: Remote protocol version 2.0, remote software version OpenSSH_7.5
    debug1: match: OpenSSH_7.5 pat OpenSSH* compat 0x04000000
    debug2: fd 3 setting O_NONBLOCK
    debug1: Authenticating to localhost:22 as 'terdon'
    debug3: hostkeys_foreach: reading file "/home/terdon/.ssh/known_hosts"
    debug3: record_hostkey: found key type ECDSA in file /home/terdon/.ssh/known_hosts:47
    debug3: load_hostkeys: loaded 1 keys from localhost
    debug3: order_hostkeyalgs: prefer hostkeyalgs: ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521
    debug3: send packet: type 20
    debug1: SSH2_MSG_KEXINIT sent
    debug3: receive packet: type 20
    debug1: SSH2_MSG_KEXINIT received
    debug2: local client KEXINIT proposal
    debug2: KEX algorithms: curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha256,diffie-hellman-group14-sha1,ext-info-c
    debug2: host key algorithms: ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa
    debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,aes128-cbc,aes192-cbc,aes256-cbc
    debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,aes128-cbc,aes192-cbc,aes256-cbc
    debug2: MACs ctos: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1
    debug2: MACs stoc: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1
    debug2: compression ctos: none,zlib@openssh.com,zlib
    debug2: compression stoc: none,zlib@openssh.com,zlib
    debug2: languages ctos: 
    debug2: languages stoc: 
    debug2: first_kex_follows 0 
    debug2: reserved 0 
    debug2: peer server KEXINIT proposal
    debug2: KEX algorithms: curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,diffie-hellman-group14-sha1
    debug2: host key algorithms: ssh-rsa,rsa-sha2-512,rsa-sha2-256,ecdsa-sha2-nistp256,ssh-ed25519
    debug2: ciphers ctos: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com
    debug2: ciphers stoc: chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com
    debug2: MACs ctos: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1
    debug2: MACs stoc: umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1
    debug2: compression ctos: none,zlib@openssh.com
    debug2: compression stoc: none,zlib@openssh.com
    debug2: languages ctos: 
    debug2: languages stoc: 
    debug2: first_kex_follows 0 
    debug2: reserved 0 
    debug1: kex: algorithm: curve25519-sha256
    debug1: kex: host key algorithm: ecdsa-sha2-nistp256
    debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none
    debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none
    debug3: send packet: type 30
    debug1: expecting SSH2_MSG_KEX_ECDH_REPLY
    debug3: receive packet: type 31
    debug1: Server host key: ecdsa-sha2-nistp256 SHA256:uxhkh+gGPiCJQPaP024WXHth382h3BTs7QdGMokB9VM
    debug3: hostkeys_foreach: reading file "/home/terdon/.ssh/known_hosts"
    debug3: record_hostkey: found key type ECDSA in file /home/terdon/.ssh/known_hosts:47
    debug3: load_hostkeys: loaded 1 keys from localhost
    debug1: Host 'localhost' is known and matches the ECDSA host key.
    debug1: Found key in /home/terdon/.ssh/known_hosts:47
    debug3: send packet: type 21
    debug2: set_newkeys: mode 1
    debug1: rekey after 134217728 blocks
    debug1: SSH2_MSG_NEWKEYS sent
    debug1: expecting SSH2_MSG_NEWKEYS
    debug3: receive packet: type 21
    debug1: SSH2_MSG_NEWKEYS received
    debug2: set_newkeys: mode 0
    debug1: rekey after 134217728 blocks
    debug2: key: /home/terdon/.ssh/id_rsa (0x555a5e4b5060)
    debug2: key: /home/terdon/.ssh/id_dsa ((nil))
    debug2: key: /home/terdon/.ssh/id_ecdsa ((nil))
    debug2: key: /home/terdon/.ssh/id_ed25519 ((nil))
    debug3: send packet: type 5
    debug3: receive packet: type 7
    debug1: SSH2_MSG_EXT_INFO received
    debug1: kex_input_ext_info: server-sig-algs=<ssh-ed25519,ssh-rsa,rsa-sha2-256,rsa-sha2-512,ssh-dss,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521>
    debug3: receive packet: type 6
    debug2: service_accept: ssh-userauth
    debug1: SSH2_MSG_SERVICE_ACCEPT received
    debug3: send packet: type 50
    debug3: receive packet: type 51
    debug1: Authentications that can continue: publickey,password
    debug3: start over, passed a different list publickey,password
    debug3: preferred publickey,keyboard-interactive,password
    debug3: authmethod_lookup publickey
    debug3: remaining preferred: keyboard-interactive,password
    debug3: authmethod_is_enabled publickey
    debug1: Next authentication method: publickey
    debug1: Offering RSA public key: /home/terdon/.ssh/id_rsa
    debug3: send_pubkey_test
    debug3: send packet: type 50
    debug2: we sent a publickey packet, wait for reply
    debug3: receive packet: type 60
    debug1: Server accepts key: pkalg rsa-sha2-512 blen 279
    debug2: input_userauth_pk_ok: fp SHA256:OGvtyUIFJw426w/FK/RvIhsykeP8kIEAtAeZwYBIzok
    debug3: sign_and_send_pubkey: RSA SHA256:OGvtyUIFJw426w/FK/RvIhsykeP8kIEAtAeZwYBIzok
    debug3: send packet: type 50
    debug3: receive packet: type 52
    debug1: Authentication succeeded (publickey).
    Authenticated to localhost ([::1]:22).
    debug2: fd 6 setting O_NONBLOCK
    debug1: channel 0: new [client-session]
    debug3: ssh_session2_open: channel_new: 0
    debug2: channel 0: send open
    debug3: send packet: type 90
    debug1: Requesting no-more-sessions@openssh.com
    debug3: send packet: type 80
    debug1: Entering interactive session.
    debug1: pledge: network
    debug3: receive packet: type 80
    debug1: client_input_global_request: rtype hostkeys-00@openssh.com want_reply 0
    debug3: receive packet: type 91
    debug2: callback start
    debug2: fd 3 setting TCP_NODELAY
    debug3: ssh_packet_set_tos: set IPV6_TCLASS 0x08
    debug2: client_session2_setup: id 0
    debug1: Sending command: sleep 2
    debug2: channel 0: request exec confirm 1
    debug3: send packet: type 98
    debug2: callback done
    debug2: channel 0: open confirm rwindow 0 rmax 32768
    debug2: channel 0: rcvd adjust 2097152
    debug3: receive packet: type 99
    debug2: channel_input_status_confirm: type 99 id 0
    debug2: exec request accepted on channel 0
    
    Run Code Online (Sandbox Code Playgroud)
  • 这不取决于我的~/.ssh/config设置。重命名文件不会改变任何东西。

  • 这发生在多台机器上。我试过 4 或 5 台不同的机器运行更新的 Ubuntu 和 Arch 发行版。
  • 命令(sleep在虚拟示例中,但在现实生活中更复杂)成功退出并执行它应该做的事情。这不取决于您正在运行的命令,这是一个 ssh 问题。
  • 这是其中最糟糕的:不一致。时不时地,其中一个实例将退出并将控制权返回给父脚本。但并非总是如此,而且我无法辨别出任何模式。
  • 重命名~/.bashrc没有区别。另外,我已经在运行 Ubuntu(默认登录 shell dash)和 Arch(默认登录 shell bash,称为 as sh)的机器上运行了它。
  • 有趣的是,只有Enter在启动循环后但在第一个脚本退出之前我按下任何键(例如,但任何键似乎都有效)时才会出现问题。如果我单独离开终端,它们会按预期完成。

这是怎么回事?这是 ssh 中的错误吗?我需要设置一个选项吗?如何从同一个 shell 启动通过 ssh 运行命令的脚本的多个实例?

Gil*_*il' 7

前台进程和终端访问控制

要了解正在发生的事情,您需要对共享终端有所了解。当两个程序试图同时从同一个终端读取时会发生什么?每个输入字节随机进入其中一个程序。(不是在内核中使用 RNG 来决定随机,只是在实践中不可预测的随机。)当两个程序从管道或任何其他文件类型读取时,会发生同样的事情,这些文件类型是从一个地方移动的字节流到另一个(套接字,字符设备,...),而不是可以多次读取任何字节的字节数组(常规文件,块设备)。例如,在终端中运行一个 shell,找出终端的名称并运行cat.

$ tty
/dev/pts/18
$ cat
Run Code Online (Sandbox Code Playgroud)

然后从另一个终端运行cat /dev/pts/18. 现在在终端中输入,然后观察线路有时会转到一个cat进程,有时会转到另一个进程。当终端处于熟模式时,线路作为一个整体进行调度。如果您将终端置于原始模式,那么每个字节将被独立调度。

好乱啊 当然应该有一种机制来决定一个程序获得终端,而其他程序没有。嗯,有!它在典型情况下会触发,但不会在我上面设置的场景中触发。这种情况很不寻常,因为cat /dev/pts/18不是从/dev/pts/18. 从未在该终端内启动的程序访问终端是不寻常的。通常情况下,您在终端中运行 shell,然后从该 shell 运行程序。那么规则就是前台的程序拿到终端,后台的程序不拿到。这称为终端访问控制。它的工作方式是:

  • 每个进程都有一个控制终端(或者没有,通常是因为它没有任何打开的文件描述符作为终端)。
  • 当一个进程试图访问其控制终端时,如果该进程不在前台,则内核会阻止它。(条件适用。对其他终端的访问不受管制。)
  • shell 决定谁是前台进程。(实际上是前台进程组。)它调用tcsetpgrp来让内核知道谁应该在前台。

这在典型情况下有效。在 shell 中运行一个程序,该程序将成为前台进程。在后台运行程序(使用&),该程序不会在前台运行。当 shell 显示提示时,shell 会将自己置于前台。当您使用 恢复暂停的作业时fg,该作业将位于前台。随着bg,事实并非如此。

如果后台进程试图从终端读取,内核会向它发送一个 SIGTTIN 信号。信号的默认操作是挂起进程(如 SIGSTOP)。该过程可以通过调用知道这个的父waitpid与WSTOPPED标志; 当子进程接收到一个暂停它的信号时,waitpid父进程中的调用返回并让父进程知道信号是什么。这就是 shell 知道打印“Stopped (tty input)”的方式。它告诉您的是,由于 SIGTTIN,此作业已暂停。

由于进程被挂起,在它被恢复或终止之前不会发生任何事情(带有进程没有捕获的信号,因为如果进程设置了信号处理程序,它不会因为进程被挂起而运行)。您可以通过向它发送一个 SIGCONT 来恢复该进程,但是如果该进程正在从终端读取,则不会有任何效果,它会立即收到另一个 SIGTTIN。如果您使用 恢复进程fg,它会转到前台,因此读取成功。

现在您了解cat在后台运行时会发生什么:

$ cat &
$ 
[1] + Stopped (tty input)        cat
$ 
Run Code Online (Sandbox Code Playgroud)

SSH的案例

现在让我们用 SSH 做同样的事情。

$ ssh localhost sleep 999999 &
$ 
$ 
$ 
[1] + Stopped (tty input)        ssh localhost sleep 999999
$ 
Run Code Online (Sandbox Code Playgroud)

按Enter有时会进入 shell(在前台),有时会进入 SSH 进程(此时它会被 SIGTTIN 停止)。为什么?如果ssh是从终端读取,它应该立即收到 SIGTTIN,如果不是,那么为什么它会收到 SIGTTIN?

发生的事情是 SSH 进程调用select系统调用以了解何时可以在它感兴趣的任何文件上使用输入(或者输出文件是否已准备好接收更多数据)。输入源至少包括终端和网络套接字。与read,select不禁止后台进程,并且ssh在调用 时不会收到 SIGTTIN select。目的select是找出数据是否可用,而不会中断任何事情。理想情况下select根本不会改变系统状态,但实际上这并不完全正确。当select告诉 SSH 进程输入在终端文件描述符上可用时,内核必须承诺在进程调用时发送输入read然后。(如果没有,并且进程调用了read,那么此时可能没有可用的输入,因此 from 的返回值select本来就是谎言。)因此,如果内核决定将某些输入路由到 SSH 进程,它会在select系统调用返回时做出决定。然后 SSH 调用read,此时内核看到后台进程试图从终端读取并使用 SIGTTIN 挂起它。

请注意,您不需要启动到同一服务器的多个连接。一个就够了。多个连接只会增加出现问题的可能性。

解决方案:不要从终端读取

如果您需要从终端读取 SSH 会话,请在前台运行它。

如果您不需要从终端读取 SSH 会话,请确保其输入不是来自终端。有两种方法可以做到这一点:

请注意,终端和 SSH 之间的断开连接必须发生在客户端。sleep服务器上运行的进程永远不会从终端读取,但 SSH 无法知道这一点。如果客户端接收到标准输入的输入,它必须将其转发到服务器,这将使数据在缓冲区中可用,以防应用程序决定读取它(如果应用程序调用select,它会被告知数据是可用的)。