在伺服器上幹活久了,幾乎每個人都會踩到同一個坑:一個跑了半小時的任務,因為網路抖了一下,SSH 斷開,任務也跟著沒了。tmux 就是為解決這類問題而生的工具,但它能做的遠不止“斷線不丟程序”。
本文不按功能清單來介紹 tmux,而是按照我們在實際使用中依次遇到的問題來展開:每一步先看現象,再分析原因,最後給出解決辦法。讀到最後,你會得到一份完整的 ~/.tmux.conf,並且知道里面每一行是為了解決什麼問題。
一、程式的長時間執行
現象
我們登入到伺服器,執行一個耗時的資料遷移指令碼:
$ 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:
$ 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)通訊。
SSH 斷開時,退出的只有客戶端,服務端和它託管的程序照常執行。用起來就是這樣:
$ 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 都是一個獨立的終端。
於是上面三個 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 外面用命令管理:
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:
unbind C-bset -g prefix C-a # 前缀键改为 Ctrl+a
bind '\' split-window -h # 左右切分(反斜杠要加引号)bind - split-window -v # 上下切分
bind h select-pane -Lbind j select-pane -Dbind k select-pane -Ubind l select-pane -R改完之後確實順手了,但用了沒多久,又冒出一個新問題:在 shell 裡按 Ctrl+a 跳回行首,沒反應了。
分析:字首鍵其實是在切換按鍵表
為什麼會這樣?要理解 tmux 是怎麼處理按鍵的。tmux 內部有多張按鍵表(key table),平時生效的是 root 表2。root 表裡最關鍵的繫結就是字首鍵,它的作用是“把下一個鍵交給 prefix 表去查”;其他所有鍵都不攔截,原樣交給當前 Pane 裡的程式。
所以,把字首鍵改成 Ctrl+a 之後,Ctrl+a 就在 root 表裡被 tmux 攔截了,再也到不了 bash。bash 的“行首跳轉”自然就失效了。
解決:send-prefix
既然 Ctrl+a 被 tmux 佔用了,那就在 prefix 表裡留一個出口,把它原樣發給程式:
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+a | prefix + 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,這份配置則可以直接載入。
# ── 前缀键 ──────────────────────────────────────────────unbind C-bunbind &unbind %unbind wunbind '"'
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 -Lbind j select-pane -Dbind k select-pane -Ubind l select-pane -R
# ── 面板大小调整 ─────────────────────────────────────────bind < resize-pane -L 5bind > resize-pane -R 5bind _ resize-pane -D 5bind + 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 0set -g status-interval 2set -g status-right "%Y/%m/%d %H:%M"set -g default-terminal "screen"
# ── 历史与滚动 ───────────────────────────────────────────set -g history-limit 10000set -g terminal-overrides 'xterm*:smcup@:rmcup@'setw -g mode-keys vi # 复制模式使用 vi 按键
# ── 其他 ─────────────────────────────────────────────────set -sg escape-time 0 # 消除 Esc 键延迟配置寫好後,在 tmux 裡執行以下命令立即生效:
tmux source ~/.tmux.conf小結
回頭看這一路,每一步都是被一個具體的問題推著走的:
- 程式的長時間執行:程式的生命週期被綁在了 SSH 連線上。tmux 用客戶端 / 服務端架構把程式掛到常駐的服務端上,斷開的只是客戶端。
- 多工的組織:以 SSH 連線為單位組織工作,粒度太粗。Session、Window、Pane 三層結構讓一次連線就能擺開整套工作環境。
- 快捷鍵的定製:改用
Ctrl+a作字首,又與 bash 的行首跳轉衝突。理解了按鍵表,就知道用send-prefix留一個出口。 - 使用細節的調優:Esc 延遲、歷史行數不足,分別交給
escape-time和history-limit等選項。
把上面的配置儲存到 ~/.tmux.conf,下次再遇到斷線,只需要一句 tmux attach。