显然,如果同一个 shell 启动到同一服务器的多个 ssh 连接,它们在执行给定的命令后不会返回,而是会Stopped (tty input)
永远挂起 ( )。为了显示:
#!/bin/bash
ssh localhost sleep 2
echo "$$ DONE!"
如果我在后台多次运行上面的脚本,它永远不会退出:
$ 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
细节
- 我发现这个是因为我在 Perl 脚本中通过 ssh 来运行命令。使用 Perl
system()
调用 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: [email protected],[email protected],[email protected],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,[email protected],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: [email protected],[email protected],[email protected],ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,[email protected],[email protected],ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa debug2: ciphers ctos: [email protected],aes128-ctr,aes192-ctr,aes256-ctr,[email protected],[email protected],aes128-cbc,aes192-cbc,aes256-cbc debug2: ciphers stoc: [email protected],aes128-ctr,aes192-ctr,aes256-ctr,[email protected],[email protected],aes128-cbc,aes192-cbc,aes256-cbc debug2: MACs ctos: [email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],hmac-sha2-256,hmac-sha2-512,hmac-sha1 debug2: MACs stoc: [email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],hmac-sha2-256,hmac-sha2-512,hmac-sha1 debug2: compression ctos: none,[email protected],zlib debug2: compression stoc: none,[email protected],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,[email protected],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: [email protected],aes128-ctr,aes192-ctr,aes256-ctr,[email protected],[email protected] debug2: ciphers stoc: [email protected],aes128-ctr,aes192-ctr,aes256-ctr,[email protected],[email protected] debug2: MACs ctos: [email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],hmac-sha2-256,hmac-sha2-512,hmac-sha1 debug2: MACs stoc: [email protected],[email protected],[email protected],[email protected],[email protected],[email protected],[email protected],hmac-sha2-256,hmac-sha2-512,hmac-sha1 debug2: compression ctos: none,[email protected] debug2: compression stoc: none,[email protected] 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: [email protected] MAC: <implicit> compression: none debug1: kex: client->server cipher: [email protected] 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 [email protected] debug3: send packet: type 80 debug1: Entering interactive session. debug1: pledge: network debug3: receive packet: type 80 debug1: client_input_global_request: rtype [email protected] 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
这不取决于我的
~/.ssh/config
设置。重命名该文件不会改变任何内容。- 这种情况发生在多台机器上。我已经尝试过 4 或 5 台不同的机器运行更新的 Ubuntu 和 Arch 发行版。
- 该命令(
sleep
在虚拟示例中,但在现实生活中更复杂)成功退出并执行其应该执行的操作。这不取决于您正在运行的命令,这是一个 ssh 问题。 - 这是其中最糟糕的:它不一致。时不时地,其中一个实例将退出并将控制权返回给父脚本。但并非总是如此,而且我也无法辨别出任何模式。
- 重命名
~/.bashrc
没有什么区别。另外,我在运行 Ubuntu(默认登录 shelldash
)和 Arch(默认登录 shellbash
,称为 assh
)的机器上运行了这个。 - Enter有趣的是,只有当我在启动循环后但在第一个脚本退出之前按任意键(例如 ,但任何似乎都有效)时,才会出现此问题。如果我不理会终端,它们就会按预期完成。
这是怎么回事?这是 ssh 中的错误吗?我需要设置一个选项吗?如何从同一个 shell 启动通过 ssh 运行命令的脚本的多个实例?
答案1
前台进程和终端访问控制
要了解发生了什么,您需要了解一些有关共享终端的知识。当两个程序尝试同时从同一个终端读取时会发生什么?每个输入字节随机进入其中一个程序。 (不是随机的,如内核中使用 RNG 来决定,只是随机的,因为在实践中不可预测。)当两个程序从管道或任何其他文件类型(从一个位置移动字节流)读取时,会发生同样的情况到另一个(套接字、字符设备等),而不是任何字节都可以多次读取的字节数组(常规文件、块设备)。例如,在终端中运行 shell,找出终端的名称并运行cat
。
$ tty
/dev/pts/18
$ cat
然后从另一个终端运行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
$
SSH 的案例
现在让我们用 SSH 做同样的事情。
$ ssh localhost sleep 999999 &
$
$
$
[1] + Stopped (tty input) ssh localhost sleep 999999
$
按Enter有时会转到 shell(位于前台),有时会转到 SSH 进程(此时它会被 SIGTTIN 停止)。为什么?如果ssh
从终端读取,它应该立即收到 SIGTTIN,如果不是,那么为什么它会收到 SIGTTIN?
发生的情况是 SSH 进程调用select
系统调用以了解其感兴趣的任何文件的输入何时可用(或者输出文件是否准备好接收更多数据)。输入源至少包括终端和网络插座。与 不同的是read
,select
不禁止后台进程,并且ssh
在调用 时不会收到 SIGTTIN select
。目的select
是在不破坏任何内容的情况下查明数据是否可用。理想情况select
下根本不会改变系统状态,但事实上这并不完全正确。当select
告诉 SSH 进程输入在终端文件描述符上可用时,如果进程read
随后调用,内核必须承诺发送输入。 (如果没有,并且进程调用了read
,那么此时可能没有可用的输入,因此返回值select
将是一个谎言。)因此,如果内核决定将某些输入路由到 SSH 进程,它会由系统调用返回时决定select
。然后 SSH 调用read
,此时内核发现后台进程尝试从终端读取数据,并使用 SIGTTIN 暂停它。
请注意,您不需要启动与同一服务器的多个连接。一个就够了。多个连接只会增加出现问题的可能性。
解决方案:不要从终端读取
如果您需要 SSH 会话从终端读取数据,请在前台运行它。
如果您不需要 SSH 会话从终端读取数据,请确保其输入不是来自终端。有两种方法可以做到这一点:
您可以重定向输入:
ssh … </dev/null
-n
您可以使用或指示 SSH 不转发终端连接-f
。 (-n
相当于</dev/null
;-f
允许 SSH 本身从终端读取,例如读取密码,但命令本身不会打开终端。)ssh -n …
请注意,终端和 SSH 之间的断开必须发生在客户端。服务器上运行的进程sleep
永远不会从终端读取数据,但 SSH 无法知道这一点。如果客户端接收到标准输入上的输入,它必须将其转发到服务器,这将使数据在缓冲区中可用,以防应用程序决定读取它(并且如果应用程序调用select
,它将被告知数据是可用的)。
答案2
您可以在手册页中找到帮助:
-n Redirects stdin from /dev/null (actually, prevents reading from stdin). This must be used when ssh is run in the
background. A common trick is to use this to run X11 programs on a remote machine. For example, ssh -n
shadows.cs.hut.fi emacs & will start an emacs on shadows.cs.hut.fi, and the X11 connection will be automatically
forwarded over an encrypted channel. The ssh program will be put in the background. (This does not work if ssh
needs to ask for a password or passphrase; see also the -f option.)
如果这仍然没有帮助,我会尝试-T
(禁用伪 tty 分配),只是一时兴起。
答案3
显然,如果同一个 shell 启动到同一服务器的多个 ssh 连接,它们在执行给定的命令后不会返回,而是永远挂起(已停止(tty 输入))。
这是并发访问 TTY 的常见行为。整个进程已经在后台运行,当它尝试写入输出时,不允许访问 TTY 并收到信号 ( SIGTTOU
),该信号未被bash
进程捕获,因此执行默认操作 ( Stop
)。
该-n
选项的解释如下其他答案或者将 IO 重定向到某些文件会对您有所帮助。不确定是否还有更多内容需要描述,但如果有,请澄清。