合上电脑,程序照跑 左边一台笔记本屏幕上显示 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。


参考资料

Footnotes

  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 ↩