合上電腦,程式照跑 左邊一臺筆記本螢幕上顯示 Connection closed,下面是 Ctrl+a 再按 d 的按鍵;連線在中間斷開,右邊伺服器上的 tmux 視窗裡進度條還在往前走,幾個窗格和底部狀態列都還亮著。 $ ./migrate.sh Connection closed. Ctrl + a d

在伺服器上幹活久了,幾乎每個人都會踩到同一個坑:一個跑了半小時的任務,因為網路抖了一下,SSH 斷開,任務也跟著沒了。tmux 就是為解決這類問題而生的工具,但它能做的遠不止“斷線不丟程序”。

本文不按功能清單來介紹 tmux,而是按照我們在實際使用中依次遇到的問題來展開:每一步先看現象,再分析原因,最後給出解決辦法。讀到最後,你會得到一份完整的 ~/.tmux.conf,並且知道里面每一行是為了解決什麼問題。


一、程式的長時間執行

現象

我們登入到伺服器,執行一個耗時的資料遷移指令碼:

Terminal window
$ ssh dev@10.0.0.8
$ ./migrate.sh

指令碼跑到一半,筆記本合上了,或者 Wi-Fi 切換了一下。重新連上伺服器,ps 一查,migrate.sh 已經不在了。

原因

SSH 斷開時,sshd 會關閉這次登入對應的偽終端。核心隨即給這個終端上的 shell 傳送 SIGHUP(結束通話訊號),shell 退出前又會把 SIGHUP 轉發給它啟動的所有任務。大多數程式收到 SIGHUP 的預設行為就是退出,於是 migrate.sh 跟著一起沒了。

換句話說,程式的生命週期被綁在了這條 SSH 連線上。

第一次嘗試:nohup

最直接的辦法是讓程式忽略 SIGHUP:

Terminal window
$ nohup ./migrate.sh &

這樣確實能讓程式活下來,但很快就會發現新的問題:

  • 輸出被重定向到了 nohup.out,只能 tail -f 去看,沒法再和程式互動。如果指令碼中途要你輸入一個 y/n,就只能乾等著。
  • 像 vim、top、互動式的資料庫客戶端這類需要終端的程式,根本沒法用 nohup 放到後臺。

所以我們真正需要的,不是讓某一個程式“忽略結束通話”,而是讓整個終端不隨 SSH 斷開,下次連回來還能接著操作。

解決:tmux 的客戶端 / 服務端架構

tmux(Terminal Multiplexer,終端複用器)正是這麼做的。它把“終端”拆成了兩部分:

  • tmux 服務端:一個常駐後臺的程序,真正託管著 shell 和你執行的程式。它不屬於任何一次 SSH 登入,所以 SSH 斷開時它不會收到 SIGHUP。
  • tmux 客戶端:你在 SSH 裡看到的那個介面。它只負責把你的按鍵發給服務端、把服務端的畫面顯示出來,兩者透過一個 Unix socket(預設在 /tmp/tmux-<UID>/default)通訊。
tmux 的客戶端 / 服務端架構 本地終端 SSH 到遠端伺服器並執行 tmux a -t work;tmux 客戶端經 Unix socket 連到常駐的 tmux 服務端,服務端記錄著 work 和 ops 兩個會話,並託管 work 裡的 vim、npm、pytest 程序;客戶端隨 SSH 斷開退出,服務端和程序繼續執行。 本地電腦 遠端伺服器 · dev@10.0.0.8 ssh Unix socket 終端 iTerm2 $ ssh dev@10.0.0.8 $ tmux a -t work tmux 客戶端 pid 3412 tty /dev/pts/3 虛線:客戶端只活在這次 SSH 連線裡。 斷開或 detach 時退出的只有它, 服務端和右下的程序照常執行。 tmux 服務端 pid 2187 socket /tmp/tmux-1000/default session work · 2 windows attached session ops · 1 window detached client /dev/pts/3 → work vim main.go 2201 npm run dev 2214 pytest -f 2230 work 裡各面板的程序和 pid
圖 1 · 程式掛在服務端上,客戶端只是一扇隨時可以關上的窗

SSH 斷開時,退出的只有客戶端,服務端和它託管的程序照常執行。用起來就是這樣:

Terminal window
$ tmux new -s work # 新建一个叫 work 的会话,并进入
$ ./migrate.sh # 在 tmux 里跑任务
# 按 Ctrl+b 再按 d,主动离开(detach);或者干脆断网
$ tmux ls # 重新登录后,看看服务端还托管着哪些会话
work: 1 windows (created Mon Oct 13 22:41:07 2026)
$ tmux attach -t work # 连回 work,脚本还在原地跑着

斷線不丟程序這個問題,到這裡就解決了。


二、多工的組織

現象

解決了斷線問題之後,我們開始把越來越多的工作放到伺服器上:一個標籤開 vim 改程式碼,一個標籤跑 npm run dev,再開一個看 pytest 的結果。每次斷線,三個標籤都要重新登入、重新 cd 到專案目錄、重新把程式拉起來。

就算每個標籤裡都開一個 tmux 會話,也只是從“重新啟動程式”變成了“重新 attach 三次”,並沒有輕鬆多少。

分析

問題在於,我們用“SSH 連線”這個粒度來組織工作,而它太粗了:一次登入只對應一個終端畫面。我們需要的是在一次連線裡同時擺開多個終端,並且按專案把它們歸攏在一起,斷線後一條命令就能整體恢復。

解決:Session、Window、Pane 三層結構

tmux 服務端裡的東西是分三層組織的,從外到內依次是:

  • Session(會話):一組相關工作的集合,通常一個專案一個,可以命名。attach 和 detach 都是以 Session 為單位的。
  • Window(視窗):相當於瀏覽器的標籤頁,一個 Session 裡可以有多個 Window,同一時刻螢幕上只顯示其中一個。
  • Pane(面板):一個 Window 可以橫向或縱向切成多個 Pane,每個 Pane 都是一個獨立的終端。
Session、Window、Pane 的巢狀關係 左邊是螢幕上看到的 work 會話的 0 號視窗 editor,被切成執行 vim、zsh、pytest 的三個面板 %0、%1、%2,底部狀態列標出當前視窗 0:editor;右邊是 tmux 記錄的結構:會話 work 包含視窗 0:editor 和不在螢幕上的視窗 1:server,後者只有一個執行 npm run dev 的面板 %3。 螢幕上看到的 tmux 記錄的結構 %0 %1 %2 package main func main() { serve(":8080") } ~ ~ ~ "main.go" 5L, 52B $ git status On branch main nothing to commit $ $ pytest -f 12 passed in 0.81s waiting for changes… [work] 0:editor* 1:server 23:00 13-Oct-26 Session work Window 0:editor %0 vim main.go %1 zsh %2 pytest -f Window 1:server %3 npm run dev 虛線:視窗存在,只是現在不在螢幕上
圖 2 · 螢幕只顯示會話裡的一個視窗,其餘視窗和麵板照樣在後臺執行

於是上面三個 SSH 標籤的場景,就變成了一個 work 會話:0:editor 視窗裡左邊 vim、右邊上下兩個 Pane 分別是 shell 和 pytest,1:server 視窗裡跑著 npm run dev。斷線後只需要一句 tmux attach -t work,所有視窗和麵板原樣回來。

在 tmux 裡操作視窗和麵板,都要先按字首鍵(預設 Ctrl+b),再按命令鍵。幾個最常用的預設快捷鍵:

操作預設快捷鍵
新建視窗Ctrl+b c
切換到下一個視窗Ctrl+b n
列出視窗並選擇Ctrl+b w
左右切分面板Ctrl+b %
上下切分面板Ctrl+b "
在面板間移動Ctrl+b 方向键
離開會話(detach)Ctrl+b d

會話本身則在 tmux 外面用命令管理:

Terminal window
tmux new -s work # 新建名为 work 的会话
tmux ls # 列出所有会话
tmux attach -t work # 重新连接 work 会话
tmux kill-session -t work # 删除 work 会话

三、快捷鍵的定製

現象

真正每天用起來,預設快捷鍵的問題就暴露出來了:

  • Ctrl+b 兩個鍵離得很遠,一隻手按起來很彆扭,而幾乎每個操作都要先按它。
  • 左右切分是 %,上下切分是 ",和“切分方向”沒有任何聯絡,而且都要再按 Shift。
  • 在面板間移動要把手挪到方向鍵上,對習慣 vim 的人來說很打斷節奏。

第一次嘗試:把字首鍵改成 Ctrl+a

Ctrl+a 兩個鍵挨在一起,按起來順手得多(這也是 GNU screen 的預設字首)。切分快捷鍵則換成形狀上更直觀的 \ 和 -,面板移動換成 vim 風格的 h/j/k/l1:

Terminal window
unbind C-b
set -g prefix C-a # 前缀键改为 Ctrl+a
bind '\' split-window -h # 左右切分(反斜杠要加引号)
bind - split-window -v # 上下切分
bind h select-pane -L
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R

改完之後確實順手了,但用了沒多久,又冒出一個新問題:在 shell 裡按 Ctrl+a 跳回行首,沒反應了。

分析:字首鍵其實是在切換按鍵表

為什麼會這樣?要理解 tmux 是怎麼處理按鍵的。tmux 內部有多張按鍵表(key table),平時生效的是 root 表2。root 表裡最關鍵的繫結就是字首鍵,它的作用是“把下一個鍵交給 prefix 表去查”;其他所有鍵都不攔截,原樣交給當前 Pane 裡的程式。

字首鍵是怎麼工作的 tmux 用兩張按鍵表解釋按鍵:root 表裡只有 C-a 被繫結為切換到 prefix 表,其他鍵原樣交給當前面板;prefix 表裡 \ 左右切分、- 上下切分、a 把 Ctrl+a 原樣發給程式、d 斷開客戶端。圖中三組按鍵 l s 回車、C-a \、C-a a 分別得到 zsh 收到 ls、左右切出新面板、zsh 收到 Ctrl+a。 你按下的鍵 tmux 的按鍵表 結果 下一個鍵查這張表 l s ⏎ C-a \ C-a a C-a 自己不做事,只把 下一個鍵交給 prefix 表 key table root C-a switch-client -T prefix 其他鍵 不攔截,交給當前 Pane key table prefix \ split-window -h - split-window -v a send-prefix d detach-client zsh 收到 ls⏎ 左右切出新 Pane zsh 收到 Ctrl+a
圖 3 · 字首鍵只是切換按鍵表:C-a 之後的那個鍵才決定 tmux 做什麼

所以,把字首鍵改成 Ctrl+a 之後,Ctrl+a 就在 root 表裡被 tmux 攔截了,再也到不了 bash。bash 的“行首跳轉”自然就失效了。

解決:send-prefix

既然 Ctrl+a 被 tmux 佔用了,那就在 prefix 表裡留一個出口,把它原樣發給程式:

Terminal window
bind a send-prefix # Ctrl+a a:把 Ctrl+a 发给当前 Pane

這樣,需要行首跳轉的時候,連按 Ctrl+a a 就行了。這個代價換來的是每天成百上千次更順手的字首鍵,還是值得的。

按這個思路整理下來,本文配置下的常用快捷鍵是:

操作快捷鍵
新建視窗prefix + c
切換到下一個視窗prefix + n
列出視窗並選擇prefix + "
左右切分面板prefix + \
上下切分面板prefix + -
在面板間移動prefix + h/j/k/l
調整面板大小prefix + < / > / _ / +
關閉當前視窗(會先確認)prefix + K
離開會話(detach)prefix + d
給程式傳送 Ctrl+aprefix + a

四、使用細節的調優

Esc 鍵延遲

現象:在 tmux 裡用 vim,按 Esc 退出插入模式,要頓一下才生效。

原因:終端裡 Alt 組合鍵是以 Esc 開頭的一串字元發出來的。tmux 收到 Esc 後,會等一小段時間看後面還有沒有字元,以區分“單獨的 Esc”和“Alt+某鍵”。較老的版本預設要等 500 毫秒。

解決:set -sg escape-time 0,不再等待。

歷史輸出的回看

現象:程式打了一大屏日誌,想往回翻,很快就到頭了。

原因:tmux 每個 Pane 預設只保留 2000 行歷史。另外,tmux 預設會讓外層終端進入“備用螢幕”,所以外層終端(比如 iTerm2)自帶的捲軸也翻不到 tmux 裡的歷史。

解決:set -g history-limit 10000 把歷史調大;terminal-overrides 'xterm*:smcup@:rmcup@' 關掉備用螢幕,讓外層終端的捲軸也能用。想在 tmux 裡翻歷史、搜尋、複製,則用 prefix + [ 進入複製模式,配合 setw -g mode-keys vi,就能用 vim 的按鍵來移動和搜尋。


完整配置檔案(tmux 2.9+)

把前文各節的改動合在一起,就是下面這份配置。樣式相關的配置全部用 -style 選項來寫:tmux 2.9 刪掉了舊的 -fg、-bg、-attr 選項(它們早在 1.9 就被 -style 取代)3,網上不少老配置在新版本上會報 invalid option,這份配置則可以直接載入。

~/.tmux.conf
# ── 前缀键 ──────────────────────────────────────────────
unbind C-b
unbind &
unbind %
unbind w
unbind '"'
set -g prefix C-a # 将前缀键改为 Ctrl+a,更顺手
bind a send-prefix # 解决与 bash Ctrl+a(行首跳转)的冲突
# ── 窗口与面板切分 ───────────────────────────────────────
bind '\' split-window -h # 左右切分
bind - split-window -v # 上下切分
bind K confirm-before -p "kill-window #W? (y/n)" kill-window # 关闭窗口前确认
bind '"' choose-window # 窗口选择列表
# ── 面板导航(vim 风格)──────────────────────────────────
bind h select-pane -L
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R
# ── 面板大小调整 ─────────────────────────────────────────
bind < resize-pane -L 5
bind > resize-pane -R 5
bind _ resize-pane -D 5
bind + resize-pane -U 5
# ── 窗口样式(-style 写法)─────────────────────────────
setw -g window-status-current-style 'bg=red,fg=white,bold'
setw -g window-status-style 'bright'
setw -g window-status-format '#[fg=cyan,dim]#I#[fg=blue] #[default]#W#[fg=grey,dim]'
setw -g window-status-current-format '#[fg=cyan,dim]#I#[fg=blue] #[default]#W#[fg=grey,dim]'
# ── 状态栏(-style 写法)───────────────────────────────
set -g status-style 'bg=blue,fg=white'
set -g status-left ""
set -g status-left-length 0
set -g status-interval 2
set -g status-right "%Y/%m/%d %H:%M"
set -g default-terminal "screen"
# ── 历史与滚动 ───────────────────────────────────────────
set -g history-limit 10000
set -g terminal-overrides 'xterm*:smcup@:rmcup@'
setw -g mode-keys vi # 复制模式使用 vi 按键
# ── 其他 ─────────────────────────────────────────────────
set -sg escape-time 0 # 消除 Esc 键延迟

配置寫好後,在 tmux 裡執行以下命令立即生效:

Terminal window
tmux source ~/.tmux.conf

小結

回頭看這一路,每一步都是被一個具體的問題推著走的:

  1. 程式的長時間執行:程式的生命週期被綁在了 SSH 連線上。tmux 用客戶端 / 服務端架構把程式掛到常駐的服務端上,斷開的只是客戶端。
  2. 多工的組織:以 SSH 連線為單位組織工作,粒度太粗。Session、Window、Pane 三層結構讓一次連線就能擺開整套工作環境。
  3. 快捷鍵的定製:改用 Ctrl+a 作字首,又與 bash 的行首跳轉衝突。理解了按鍵表,就知道用 send-prefix 留一個出口。
  4. 使用細節的調優:Esc 延遲、歷史行數不足,分別交給 escape-time 和 history-limit 等選項。

把上面的配置儲存到 ~/.tmux.conf,下次再遇到斷線,只需要一句 tmux attach。


參考資料

參考資料

  1. tmux 終端複用配置opstool.com收藏opstool.com配置Tmux 一節https://www.opstool.com/#blog/article/253 ↩

  2. tmux(1) 手冊收藏man7.orgKEY BINDINGS 一節https://man7.org/linux/man-pages/man1/tmux.1.html ↩

  3. tmux CHANGES收藏github.comCHANGES FROM 2.8 TO 2.9https://github.com/tmux/tmux/blob/master/CHANGES ↩