如果同一 shell 啟動了多個連接,則在後台執行的 SSH 連接不會退出
顯然,如果同一個 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: 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
- 這不取決於我的
~/.ssh/config
設置。重命名文件不會改變任何東西。- 這發生在多台機器上。我試過 4 或 5 台不同的機器執行更新的 Ubuntu 和 Arch 發行版。
- 該命令(
sleep
在虛擬範例中,但在現實生活中要復雜得多)成功退出並執行它應該執行的操作。這不取決於您正在執行的命令,這是一個 ssh 問題。- 這是其中最糟糕的:不一致。時不時地,其中一個實例將退出並將控制權返回給父腳本。但並非總是如此,而且我無法辨別出任何模式。
- 重命名
~/.bashrc
沒有區別。另外,我已經在執行 Ubuntu(預設登錄 shelldash
)和 Arch(預設登錄 shellbash
,稱為 assh
)的機器上執行它。- 有趣的是,僅當我在啟動循環之後但在第一個腳本退出之前按下任何鍵(例如
Enter
,但任何鍵似乎都有效)時才會出現此問題。如果我不理會終端,它們會按預期完成。這是怎麼回事?這是 ssh 中的錯誤嗎?我需要設置一個選項嗎?如何從同一個 shell 啟動通過 ssh 執行命令的腳本的多個實例?
前台程序和終端訪問控制
要了解發生了什麼,您需要對共享終端有所了解。當兩個程序試圖同時從同一個終端讀取時會發生什麼?每個輸入字節隨機進入其中一個程序。(不是隨機的,因為核心使用 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 知道列印“已停止(tty 輸入)”的方式。它告訴你的是,這項工作由於 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
在呼叫時不會收到 SIGTTINselect
。的目的select
是在不破壞任何內容的情況下找出數據是否可用。理想情況下select
根本不會改變系統狀態,但實際上這並不完全正確。當select
告訴 SSH 程序終端文件描述符上有輸入可用時,如果程序呼叫,核心必須承諾發送輸入read
然後。(如果沒有,並且程序呼叫了read
,那麼此時可能沒有可用的輸入,因此 from 的返回值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
,它將被告知數據是可用的)。