Shell

如果同一 shell 啟動了多個連接,則在後台執行的 SSH 連接不會退出

  • August 3, 2017

顯然,如果同一個 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 來執行命令。使用 Perlsystem()呼叫 launch時也會發生同樣的行為ssh
  • 使用 Perl 模組而不是system(). 我試過了Net::SSH::PerlNet:SSH2Net::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(預設登錄 shell dash)和 Arch(預設登錄 shell bash,稱為 as sh)的機器上執行它。
  • 有趣的是,僅當我在啟動循環之後但在第一個腳本退出之前按下任何鍵(例如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系統呼叫以了解何時在它感興趣的任何文件上輸入可用(或者輸出文件是否準備好接收更多數據)。輸入源至少包括終端和網路插座。不像readselect不禁止後台程序,並且ssh在呼叫時不會收到 SIGTTIN select。的目的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,它將被告知數據是可用的)。

引用自:https://unix.stackexchange.com/questions/383501