在服务器上干活久了,几乎每个人都会踩到同一个坑:一个跑了半小时的任务,因为网络抖了一下,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。