1. 程式人生 > 實用技巧 >ssh遠端執行nohup命令不退出

ssh遠端執行nohup命令不退出

Linux系統下,使用預設使用者root。遠端target機器的主目錄下有個指令碼test.sh,可執行許可權,內容只有一條命令:sleep 10

在本地機器上執行ssh target "nohup ./test.sh &",結果ssh不立即退出,等test.sh執行完畢之後才退出。一般我們使用nohup命令是為了在斷開到某個伺服器的ssh連線之後,之前執行的命令仍然正常地在伺服器執行。但是前面的現象其實與nohup命令沒有什麼關係,只是ssh本身的問題;nohup其作用的前提是使用者使用ssh登入到伺服器上。至於跟nohup扯上關係,我猜是因為在大家的印象中上面這種nohup命令的執行方式應該是立即退出的,結果反差太大,所以當作了一個特別問題。相關的說明可以參見:

https://en.wikipedia.org/wiki/Nohup
http://www.snailbook.com/faq/background-jobs.auto.html

http://www.openssh.com/faq.html#3.10
https://bugzilla.mindrot.org/show_bug.cgi?id=52

解決的方法是,手動在命令裡面指定重定向,即上面的命令換成:ssh target "nohup ./test.sh >/dev/null 2>&1 &",然後就OK。下面的分析表明了nohup命令與“ssh host "cmd"”方式的ssh命令沒有任何關係(因為這種方式不會涉及SIGHUP),所以換成ssh target "./test.sh >/dev/null 2>&1 &" 就可以了。

分析:

一般處理ssh遠端執行某個命令的任務,在遠端目標機器上先建立一個sshd的子程序(父程序是最初始的sshd),然後由這個sshd程序啟動一個bash程序(如果使用bash程序)來執行傳遞過來的命令。 針對這次任務建立的sshd程序和bash程序在檔案描述符方面有一定關係:通常bash程序的0 1 2三個檔案描述符通過管道與sshd的相應檔案描述符聯絡起來。這可以通過查詢建立的sshd程序和bash程序在/proc檔案系統下的相應程序的fd目錄的詳細情況。ssh遠端執行命令這種建立ssh連線的方式在ps -ef 中顯示的sshd程序是有"sshd root@notty"標記。此sshd程序的命令可以通過命令“ps -ef | grep -v grep | grep 'sshd.*notty' | awk '{print $2}'”得到,而相關bash程序的PID可用$$獲取。遠端執行下面命令可以一步到位,得到比較結果:

ssh target "TMPSPID=\$(ps -ef | grep -v grep | grep -e 'sshd.*notty' | awk '{print \$2}');echo \$TMPSPID;ls -l /proc/\$TMPSPID/fd;echo \$\$;ls -l /proc/\$\$/fd"

如果遠端執行的命令是後臺執行,那麼可以發現新啟動的bash程序的父程序成了1,而輸入即描述符0重定向到了/dev/null。 nohup是防止程序被SIGHUP訊號中斷,正常使用的時候也會進行一些重定向操作,即當標準輸入/輸出/錯誤等是終端的時候,會對它們進行重定向。但是ssh遠端執行命令時,這些條件都不滿足,因為檔案描述符0,1,2(正常情況下)都被重定向到管道了。所以遠端執行nohup時不會進行相關重定向操作。而當遠端執行後臺命令的時候,雖然標準輸入被重定向到了/dev/null,但是標準輸出和錯誤還是管道, 所以針對這次任務啟動的sshd程序還不會結束。所以執行遠端命令時,還必須自己在命令列上重定向標準輸出和標準錯誤才行。

對於上面的test.sh指令碼,下面給出幾種命令執行執行方式:
ssh target "./test.sh" # 等待命令完成後退出;本地Ctrl+C中斷ssh會話,不會中斷test.sh的執行(bash父程序變為1)(與登入終端執行命令而終端連線斷開時的行為不一樣)
ssh target "./test.sh &" # 等待命令完成後退出;本地Ctrl+C中斷ssh會話,不會中斷test.sh的執行(bash父程序本來就為1)
ssh target "nohup ./test.sh &" # 等待命令完成後退出;本地Ctrl+C中斷ssh會話,不會中斷test.sh的執行(bash父程序本來就為1)
ssh target "nohup ./test.sh >/dev/null 2>&1 &" # 啟動test.sh執行後就會退出(bash父程序本來就為1)
ssh target "./test.sh >/dev/null 2>&1 &" # 啟動test.sh執行後就會退出(bash父程序本來就為1),這也表明ssh不退出與nohup命令本身沒有什麼關係

實際上如先ssh登入target,執行./test.sh &,然後正常退出ssh(即exit命令),那麼./test.sh這個指令碼也不會終止,而且會將父程序換成1;如果不正常退出,而是直接關閉連線,那麼會導致./test.sh任務終止。

補充:

感覺上面的分析還不是很到位,因為簡單命令還不能夠顯示出真實情況,比如執行

ssh target "./test.sh"

在遠端機器上執行“ps -ef | grep 'test\|notty'”命令,結果如下

root 35929 3306 0 19:20 ? 00:00:00 sshd: root@notty
root 35931 35929 0 19:20 ? 00:00:00 /bin/bash ./test.sh”

好像執行./test.sh的bash程序直接由顯示的sshd程序建立,其實情況應該不是這樣的。先執行一個稍複雜的命令:

ssh target "for w in a b c; do ./test.sh; done"

同樣使用上面的檢視命令可以看到如下結果:

root 36219 3306 0 19:29 ? 00:00:00 sshd: root@notty
root 36221 36219 0 19:29 ? 00:00:00 bash -c for w in a b c; do ./test.sh; done
root 36228 36221 0 19:29 ? 00:00:00 /bin/bash ./test.sh

這就表明了其實有兩層程序關係,sshd ---- bash -c ----- bash,即sshd 先建立一個bash以bash -c的方式執行傳遞過來的作為命令的字串,然後再由這個bash建立執行./test.sh指令碼的子bash程序(這個可以建立多個)。而本地執行ssh host "cmd"形式命令要能迅速返回,必須滿足的條件是:該命令物件的sshd程序(一般是sshd: root@notty),沒有子程序需要等待結束(靠將第一個bash搞成後臺程序,或者第一個bash會立即執行完命令自然退出——即它啟動一些後臺子程序), 而且沒有其他程序與它有管道連線關係(靠重定向解決,在第一個bash處或者所有第二層bash處都可以)。簡而言之,要ssh host "cmd"形式命令立即返回,在整個命令最後面新增“>/dev/null 2>&1 &”,是有保證的。注意,對於組合的命令, 可能需要放到{}中才行,比如“{ cmd; } >/dev/null 2>&1 &”這樣的形式。這是因為重定向只對單個簡單命令或單個複合命令有效。

下面通過一些實際例子的情況幫助大家認識(/dev/null也可以是某個本地檔案):

ssh target "for w in a b c; do ./test.sh >/dev/null 2>&1 0</dev/null; done"

ssh不返回,./test.sh一個一個啟動

ssh target "for w in a b c; do ./test.sh >/dev/null 2>&1 0</dev/null; done &"

ssh 不返回,./test.sh一個一個啟動。 第一個bash(由sshd啟動的bash -c)是後臺執行的,但是檔案描述符1和2還與sshd有管道連線,所以不返回

ssh target "for w in a b c; do ./test.sh >/dev/null 2>&1 0</dev/null; done >/dev/null 2>&1 &"

ssh立即返回,./tesh.sh一個接一個啟動

ssh target "for w in a b c; do ./test.sh ; done >/dev/null 2>&1 &"

ssh立即返回,./test.sh一個接一個啟動;由於執行./test.sh的bash是由第一個bash啟動的,而第一個bash執行了重定向,所以該bash也繼承了這些重定向,換言之這條命令與上條命令的效果一樣,即內層的./test.sh無需重定向了

ssh target "for w in a b c; do ./test.sh & done >/dev/null 2>&1 &"

ssh立即退出,./test.sh全部啟動,第一個bash也退出了

ssh target "for w in a b c; do ./test.sh & done >/dev/null 2>&1"

ssh立即退出(為什麼?因為第一個bash啟動三個./tesh.sh的bash程序後,退出了;而執行./test.sh的bash程序因為繼承了父bash的檔案描述符,所以沒有管道與sshd連線,因此ssh退出

ssh target "for w in a b c; do ./test.sh & done "

ssh不返回,因為執行./test.sh與sshd還有管道連線

ssh target "for w in a b c; do ./test.sh >/dev/null 2>&1 0</dev/null & done" 

ssh返回,因為第一個bash啟動三個子bash之後結束,而子bash與sshd之間又沒有管道上連線

ssh target "for w in a b c; do ./test.sh >/dev/null 2>&1 0</dev/null & done &"

同上,因為第一個bash啟動所有子程序後會退出,此時將第一個bash作為後臺程序已經意義不大

ssh target "./test.sh && ./test.sh >/dev/null 2>&1 &"

ssh不會返回

ssh target "{ ./test.sh && ./test.sh; } >/dev/null 2>&1 &" 

ssh會返回