SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 已更新 2026-10-03

SSH 斷線後讓命令繼續執行的方法

SSH 斷線時核心會傳送 SIGHUP,命令可能隨即終止。比較 nohup、disown、tmux 與 systemd-run,依工作類型選擇合適方法。

SSH 中斷時命令為何會終止

若要讓命令在 SSH 中斷後繼續執行,命令最後必須位於不會收到 hangup signal 的環境中。以下每種方法都以不同方式達成這點,因此請先了解其運作機制。

您的登入工作階段會在 pty(pseudo-terminal,虛擬終端機)上執行。這是 sshd 在伺服器上為您的工作階段建立的虛擬終端機裝置。它是 shell 以及您從該 shell 啟動之所有命令的控制終端機。若要了解完整流程,請參閱 登入時 SSH 所建立的環境。TCP 連線中斷時,sshd 會關閉其端點,pty 也會被銷毀。核心會將此視為終端機中斷連線,因此向該終端機的前景程序群組及工作階段領導程序傳送 SIGHUP;後者就是您的 shell。SIGHUP 的預設動作是終止程序。您的命令位於前景程序群組中,因此也會終止。

背景工作也不安全。使用 & 啟動的工作會位於自己的程序群組中,因此核心不會直接向它傳送訊號。Bash 會處理這件事。互動式 bash 收到 SIGHUP 時,會在結束前向其工作表中的每個工作重新傳送 SIGHUP。從您的角度看,結果完全相同:工作消失,log 檔案也會在一行中途停止寫入。

這裡有一個容易造成混淆的不對稱性。輸入 exit 不會 中斷背景工作,因為 bash 只有在設定 huponexit 選項時才會這麼做,而此選項預設為關閉。連線中斷則會中斷這些工作。您正常關閉終端機後仍存活的工作,在 Wi-Fi 中斷時仍可能終止。

由此可得兩個結論,也是這個主題的核心。忽略 SIGHUP 的程序,或完全沒有控制終端機的程序,不會因 hangup 而終止。此外,若程序的標準輸出仍指向已銷毀的 pty,就沒有可寫入的目標:寫入會因 EIO(輸入/輸出錯誤)而失敗,而多數程式會在此時結束。這兩個問題都必須處理。許多作法只解決第一個問題,因此使用者才會回報「nohup 沒有作用」。

如果您的連線每天中斷數次,也應一併處理這個問題。ServerAliveInterval 60 位於 ~/.ssh/config 時,可避免閒置工作階段因路徑中某處的 NAT(network address translation)逾時而被捨棄。若工作階段根本無法建立,則是另一種故障,原因也不同;此時應了解 connection refused 與 connection timed out 的差異。

哪種方法能讓命令在 SSH 中斷連線後繼續執行?

以下提供 4 種方法,依工作的重要程度排列。

  • nohup 或 setsid:適合現在啟動、稍後查看日誌的一次性工作。輸出內容需要自行重新導向。
  • disown:適合已經啟動但忘記保護的工作。它可以挽救程序,但無法還原輸出內容。
  • tmux 或 screen:適合需要在數天內持續監看、中斷並返回處理的工作。
  • systemd-run 或正式的 unit 檔案:適合任何必須超過登入工作階段持續執行的工作,例如執行 6 小時的 rsync 或整夜執行的資料庫匯入。

請記住這項規則:如果忘記某項工作會造成問題,就應該使用 systemd,而不是 tmux。tmux 視窗需要由人員記得管理。unit 具備名稱、狀態、日誌與重新啟動原則,下一位處理人員無須他人說明,就能找到這些資訊。

nohup 與 setsid:啟動後離開

nohup ./import.sh > ~/import.log 2>&1 &
echo $! > ~/import.pid

nohup 將 SIGHUP 的處置方式設為忽略,然後執行您的命令。因此,核心送出 hangup 時不會產生作用。重新導向方式由您自行指定。如果標準輸出仍指向終端機,nohup 會自動將輸出重新導向至目前目錄中的 nohup.out;若無法使用,則改用 $HOME/nohup.out,並輸出:

nohup: ignoring input and appending output to 'nohup.out'

該檔案容易遺失,因此請自行指定檔名。$! 會保存上一個背景工作的 PID(process identifier)。儲存此 PID 後,重新登入即可檢查工作狀態。

setsid 從另一側處理相同問題。它會在沒有 controlling terminal 的新工作階段中執行命令,因此不存在可能讓程序中斷的終端機。

setsid --fork ./import.sh > ~/import.log 2>&1

請使用 --fork。若未使用,setsid 會在程序尚未成為 process group leader 時直接呼叫 setsid()。Shell script 中通常會發生這種情況,導致 script 持續遭到阻塞。使用 --fork 後,在 script 中和命令提示字元下的行為會一致。

確認實際取得的結果:

ps -o pid,ppid,sid,tty,stat,cmd -p "$(cat ~/import.pid)"

TTY 欄位中的 ? 表示該程序沒有 controlling terminal,因此沒有終端機可以讓它中斷。在仍保持連線時,nohup 下的 TTY 欄位仍會顯示類似 pts/0 的內容;pty 被銷毀後,則會變成 ?。這兩種結果都表示狀態正常。工作仍在執行。

disown:救回已啟動的工作

您已在前景執行一項需要 2 小時的工作,之後才想起這個問題。不要終止工作再重新開始。

# press Ctrl-Z to suspend the job first
bg
jobs -l
disown -h %1

Ctrl-Z會暫停工作,bg會在背景繼續執行,jobs -l會在 PID 旁列印工作編號。disown -h %1會標記該工作,讓 bash 不會將 SIGHUP 傳送給它。單獨使用 disown %1會將工作從 bash 的工作表中完全移除。這對 hangup 有相同效果,但之後 jobs 將不再列出該工作。

disown無法做的是移動輸出。程序仍將 pty 保留為標準輸出;pty 消失後,下一次寫入會回傳 EIO。因此,disown能可靠地救回安靜的工作,例如將輸出寫入檔案的編譯工作,但對輸出頻繁的工作通常無法保護。工作可能在沒有輸出目的地的情況下繼續執行,也可能在下一行輸出時終止。

有一項工具可處理檔案描述元。reptyr會將執行中的程序移到目前的終端機:使用 sudo apt install -y reptyr 安裝,然後在 tmux 視窗中執行 reptyr <pid>。它透過 ptrace 運作;Ubuntu 提供 kernel.yama.ptrace_scope = 1,只允許追蹤您自己的子程序,因此繼承而來的程序需要 sudo reptyr <pid>。請將它視為緊急工具,不要以此建立例行程序。

tmux:需要監看並稍後返回的工作

tmux(終端機多工器)會在不同層面解決這個問題。它不讓程序免受 pty 影響,而是提供一個不屬於 SSH 工作階段的 pty。tmux server 在該工作階段之外執行,並管理其中所有內容所使用的終端機。SSH 連線只是附加至該 server 的檢視器。中斷連線後,server 不會察覺。

sudo apt update && sudo apt install -y tmux
tmux new -s import

在該視窗中啟動工作,然後按下 Ctrl-b,再按 d 以脫離工作階段。稍後重新登入並接回工作:

tmux ls
tmux attach -t import

tmux ls 應輸出一行以 import: 1 windows 開頭的文字。若輸出 no server running on /tmp/tmux-1000/default,表示沒有可附加的工作階段,原因可能是工作階段從未建立,或某個程序終止了 server。

screen 也能完成相同工作,但使用不同的按鍵組合。screen -S import 會建立工作階段,接著按 Ctrl-a 再按 d 即可脫離。screen -ls 會列出現有的工作階段,screen -r import 會重新接回其中一個。這兩個工具都適用。人們最常忘記的是脫離工作階段的按鍵。

多工器也是互動式工作的適當環境,能讓工作在連線中斷後繼續執行。因此,在 VPS 的 tmux 中執行 Claude Code 是標準設定;在每隔幾分鐘就重新連線的行動網路上,這也是透過手機操作 server 工作階段仍可使用的原因。

systemd-run:將工作交給 PID 1

對於完全不應依賴目前工作階段的工作,請將它交給 init system。

sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/

這會建立名為 bigsync.service 的暫時服務單元。它會使用自己的 cgroup,不連接控制終端,也不與您的登入工作階段建立關聯。命令會立即返回,並輸出 Running as unit: bigsync.service。您可以使用下列任一方式監看:

systemctl status bigsync
journalctl -u bigsync -f

--collect 會告訴 systemd 在單元完成後移除它,即使單元執行失敗也一樣。若未使用此選項,失敗的暫時單元會保持載入狀態,其名稱也會持續被占用,因此下一次執行會失敗,並顯示單元已存在的訊息。輸出會寫入 journal,且每一行都會包含時間戳記。只有在存在 /var/log/journal 時,journal 項目才會保留到重新開機之後;若需要此功能,請執行 sudo mkdir -p /var/log/journal,再重新啟動 systemd-journald。

以一般使用者身分呼叫 systemd-run 且未使用 sudo 時,會要求 polkit 授權,並輸出 ==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===。針對 system units,請使用 sudo。

您也可以在自己的 user manager 下執行工作:

systemd-run --user --unit=bigsync --collect /usr/bin/rsync -aH /srv/data/ /mnt/backup/

這裡有一個陷阱。您的 per-user manager user@1000.service 通常會在最後一個工作階段結束時停止,並一併停止所有 user units。請啟用 lingering 一次:

loginctl enable-linger "$USER"
loginctl show-user "$USER" --property=Linger

第二個命令應輸出 Linger=yes。啟用 lingering 後,您的 user manager 會在開機時啟動,無論您是否已登入都會持續執行。未啟用時,systemd-run --user 相較於 nohup 不會帶來任何好處。

systemd-run --scope 是另一回事。它會在前景執行命令,並連接到您的終端機,因此無法解決這裡的需求。

對於會重複執行的工作,請將單元寫入檔案,而不要每次都輸入暫時單元。

會再次執行之工作的永久單元
[Unit]
Description=Nightly data sync
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
User=deploy
WorkingDirectory=/srv/data
ExecStart=/usr/local/bin/nightly-sync.sh

將內容儲存為 /etc/systemd/system/nightly-sync.service,執行 sudo systemctl daemon-reload,接著使用 sudo systemctl start nightly-sync 啟動,並使用 journalctl -u nightly-sync 讀取。若應依排程執行而非手動啟動,請加入相符的 .timer 檔案。

撰寫 systemd 服務單元及其計時器完整說明檔案格式與排程語法。

輸出會寫入哪裡,以及為何會消失

重新導向的順序很重要。> file 2>&1 會先將標準輸出指向檔案,再將標準錯誤指向同一處。2>&1 > file 的順序相反:標準錯誤仍會輸出到終端機,而終端機正是即將消失的對象。Bash 也接受 &> file,一次重新導向兩個串流。

另一個容易誤判的原因是緩衝。標準輸出連接終端機時,C library 會逐行清空緩衝區。標準輸出連接檔案時,則會改用數 KB 的區塊緩衝,因此 tail -f ~/import.log 可能數分鐘都沒有輸出,讓工作看起來像是停止。使用 stdbuf -oL ./import.sh > ~/import.log 2>&1 強制採用逐行緩衝,或使用程式本身的選項,例如 python3 -u 或 grep --line-buffered。

請避免使用以下模式:

nohup ./import.sh 2>&1 | tee ~/import.log &

nohup 只會保護 import.sh,不會保護其他內容。tee 是同一個 pipeline 中的獨立程序,仍會在 hangup 時終止。接著 import.sh 會寫入沒有讀取端的 pipe,因此會收到 SIGPIPE 並停止。請將整個 pipeline 放入 setsid bash -c '...' 中,或直接寫入檔案,重新連線時再對該檔案執行 tail -f。

rsync 還有一個特別需要注意的細節。--info=progress2 會寫出一連串 carriage return;在終端機上看起來正常,但在 log 檔案或 journal 中會變成一行極長的內容。若要執行無人值守的工作,請停用它,改用 --stats。

為何在 shell 中可正常執行的工作,在 systemd 或 cron 下卻失敗

互動式 shell 會讀取 /etc/profile、~/.profile 和 ~/.bashrc,因此具備你的 PATH、版本管理器 shim 及已匯出的變數。systemd unit 不會讀取這些內容。cron 也同樣不會讀取:在 Debian 和 Ubuntu 上,cron 會以 SHELL=/bin/sh 和 PATH=/usr/bin:/bin 執行工作。

在 systemd 下,常見症狀是 systemctl status 回報 (code=exited, status=203/EXEC)。這表示 systemd 完全無法執行該檔案,原因可能是路徑錯誤,或檔案未標記為可執行。在 cron 下,通常是 command not found,由本機郵件系統傳送;若未安裝郵件系統,則可能完全不會收到任何通知。

在花費 1 小時猜測之前,先確認實際使用的環境:

sudo systemd-run --collect --wait --unit=envtest /usr/bin/env
journalctl -u envtest --no-pager

這會列出工作實際執行時使用的完整環境。接著修正差異。對於你自行管理的任何項目,都應使用絕對路徑,因為 systemd 會依固定的系統路徑清單解析不含路徑的 rsync,不會使用 shell 的 PATH。在命令列使用 -p Environment="KEY=value" 傳入所需變數,或在 unit 檔案中使用 EnvironmentFile=/etc/default/myjob。如果工作確實需要你的登入環境,請以 /bin/bash -lc 'my-command' 執行,但要接受該工作現在會依賴你的 dotfiles。

仍會終止分離工作階段的因素

  • 重新開機。tmux 不會跨越重新開機保留任何狀態,因為伺服器是一般程序,而工作階段是其記憶體內狀態。核心更新代表需要重新開機,因此無法輕易重新啟動的工作,應放入可由 systemctl enable 管理的單元中。
  • 記憶體不足終止程式。dmesg -T | grep -i 'killed process' 會顯示相關資訊,包括它選定的程序名稱。在小型 VPS 上執行大型匯入工作時,經常會遇到這種情況。
  • logind 清理。如果 /etc/systemd/logind.conf 設定 KillUserProcesses=yes,最後一個工作階段結束時,系統會終止剩餘的程序,其中也包括 tmux 伺服器。使用 loginctl show --property=KillUserProcesses 檢查目前設定,並使用 loginctl enable-linger "$USER" 將您的使用者排除在外。
  • 磁碟已滿。工作停止的原因是重新導向的日誌填滿檔案系統,而不是因為您離開了工作階段。先執行 df -h,再判斷是否為訊號造成。

透過 SSH 啟動工作且不維持連線

ssh vps 'sudo systemd-run --unit=import --collect /usr/local/bin/import.sh'

systemd-run 會在單元啟動後立即返回,因此 ssh 命令也會返回,且該工作不會與啟動它的工作階段建立連線。這是較乾淨的做法。

nohup 版本需要更謹慎:

ssh vps 'nohup ./import.sh > ~/import.log 2>&1 < /dev/null &'

如果沒有這些重新導向,看起來就像當機。只要仍有程序持有遠端命令的標準輸出或標準錯誤,sshd 就會維持通道開啟,而背景工作會繼承這兩者。單獨使用 nohup 無法解決問題,因為 nohup 只有在輸出目標是終端機時才會重新導向輸出;此處的輸出是透過管道傳回用戶端。加入 < /dev/null 也會關閉輸入端。ssh -n 在用戶端端執行相同的工作。

FAQ

為什麼 SSH 連線中斷後,我的命令會停止?

工作階段使用的 pty(虛擬終端機)會被銷毀,核心也會對該終端機的前景處理程序群組傳送 SIGHUP。SIGHUP 的預設動作是終止處理程序。背景工作也會停止,因為 bash 結束前會將 SIGHUP 重新傳送給其工作表中的每個工作。忽略 SIGHUP 的命令(例如以 nohup 啟動的命令),或從未共用該工作階段的服務(例如 systemd unit),則不受影響。

執行六小時的 rsync 時,tmux 和 systemd-run 哪個較好?

systemd-run。tmux 工作階段依賴你啟動的伺服器程序,因此下次重新開機時就會結束;不知道要執行 tmux ls 的人也看不到該工作階段。執行 sudo systemd-run --unit=bigsync --collect /usr/bin/rsync -aH --stats /srv/data/ /mnt/backup/ 會提供 systemctl status bigsync 來保存狀態,以及 journalctl -u bigsync 來保存輸出。下一位管理員不必詢問你,就能找到這兩者。若工作期間需要查看畫面並輸入內容,請使用 tmux。

如何查看忘記重新導向的工作的輸出?

通常無法查看,因為那些輸出已寫入不再存在的終端機。處理程序仍在執行時,可以使用 sudo ls -l /proc/<pid>/fd 檢查其開啟的檔案,或使用 sudo strace -p <pid> 監看其系統呼叫,但已寫出的文字已經遺失。reptyr <pid> 可以將處理程序移到新的終端機;Ubuntu 的 kernel.yama.ptrace_scope = 1 表示,對於不是自己子處理程序的處理程序,需要 sudo。避免所有這些問題的方法,是一開始就將輸出重新導向至檔案,並 tail -f 該檔案。

分離的 tmux 工作階段會在重新開機後保留嗎?

不會。tmux server 是一般處理程序,而工作階段是儲存在記憶體中的狀態,因此重新開機會結束兩者。當 /etc/systemd/logind.conf 設定 KillUserProcesses=yes,且你登出最後一個工作階段時,tmux server 也會結束;loginctl enable-linger "$USER" 可以避免這種情況。若工作必須在重新開機後自動恢復,請撰寫 systemd unit 並 systemctl enable 它。

為什麼我的指令碼在 shell 中能執行,作為 systemd unit 卻失敗?

unit 不會讀取 /etc/profile 或 ~/.bashrc,因此沒有你的 PATH 設定,也沒有你匯出的變數。systemctl status 顯示 (code=exited, status=203/EXEC),表示 systemd 根本無法執行該檔案,因此請使用絕對路徑並檢查可執行位元。執行 sudo systemd-run --collect --wait --unit=envtest /usr/bin/env,再使用 journalctl -u envtest 讀回內容,即可取得工作實際使用的完整環境。使用 Environment= 或 EnvironmentFile= 提供缺少的項目。

#ssh#tmux#nohup#systemd#long-running-jobs