tmux 实用要点:会话、窗格与脚本化用法
会话独立于终端连接而存在,这是远程运维里最核心的价值;以及 send-keys / capture-pane 让交互式程序可被脚本驱动。最后说明何时该改用 systemd 而不是「挂在 tmux 里」。
为什么需要它
SSH 会话断开会杀掉该会话的前台进程(收到 SIGHUP)。对一条要跑二十分钟的升级、备份或构建来说,这意味着一次网络抖动就前功尽弃,更糟的情况是留下半完成状态。
tmux 的会话独立于终端连接而存在:连接断了,会话里的进程继续跑,随时可以重新 attach 回去。这是它在运维场景下唯一真正不可替代的价值,其余都是便利性。
会话
tmux new -s deploy # 新建并命名后进入
tmux ls # 列出全部会话
tmux attach -t deploy # 重新接入
tmux kill-session -t deploy
在会话内用 Ctrl-b d(detach)离开但不关闭——这是最常用也最容易误解的一对概念:
| 操作 | 进程 | 之后能回来吗 |
|---|---|---|
Ctrl-b d detach |
继续运行 | 能,attach 回去 |
| 直接关终端窗口 | 继续运行 | 能 |
exit / 会话内最后一个程序退出 |
结束 | 不能 |
kill-session |
结束 | 不能 |
给会话起名是有意义的。 不命名的话回来时 tmux ls 只看到一串编号(0:、1:),在多个会话并存时完全无法分辨哪个是干什么的。用 deploy、db-migrate 这种名字,几周后再看也知道是什么。
窗口与窗格
两者是不同的层级,容易混:
- 窗口(window):同一会话内的并列标签页
- 窗格(pane):同一窗口内的分屏
常用键(默认前缀 Ctrl-b):
| 键 | 作用 |
|---|---|
c |
新建窗口 |
n / p |
下一个 / 上一个窗口 |
0-9 |
跳到第 N 个窗口 |
% |
纵向分屏(左右) |
" |
横向分屏(上下) |
| 方向键 | 在窗格间切换 |
z |
当前窗格全屏 / 还原(toggle) |
x |
关闭当前窗格 |
, |
重命名当前窗口 |
z 值得单独记:在一个窄窗格里看长输出很痛苦,Ctrl-b z 把它临时放大到整屏,再按一次还原,布局不受影响。
滚屏与复制模式
在 tmux 里鼠标滚轮或 PageUp 的行为与普通终端不同,因为终端的历史缓冲被 tmux 接管了。要翻看输出需要进入复制模式:
Ctrl-b [ 进入复制模式
↑ / ↓ / PgUp 滚动
Ctrl-b (在 vi 模式下)开始选择
Enter 复制选中内容
q 退出复制模式
脚本化用法(最值得记住的部分)
tmux 可以被脚本驱动,这让它成为控制交互式程序的实用工具——那些没有批处理模式的程序(某些安装器、REPL、调试器)只能通过终端交互,而 send-keys + capture-pane 恰好能替代人工输入输出。
# 后台新建会话并运行命令(-d = detached,不接管当前终端)
tmux new -d -s build 'make -j4 2>&1 | tee /tmp/build.log'
# 向会话注入输入(模拟敲键盘,Enter 表示回车)
tmux send-keys -t build 'make clean' Enter
# 读取当前屏内容(用于脚本检查交互式程序的输出)
tmux capture-pane -p -t build
# 带历史行数读取
tmux capture-pane -p -S -200 -t build
一个完整的"后台跑完再取回结果"的例子:
tmux new -d -s upgrade
tmux send-keys -t upgrade 'apt-get -y upgrade; echo "EXIT=$?"' Enter
# 之后轮询检查是否结束
tmux capture-pane -p -t upgrade | tail -5
# 确认结束后取回完整输出并清理
tmux capture-pane -p -S - -t upgrade > /tmp/upgrade.out
tmux kill-session -t upgrade
-S - 表示从历史缓冲的最开头取,-S -200 表示往上取 200 行。默认只取当前可见屏——这在"输出很长、重要的部分已经滚过去了"时会造成误判,因此取完整输出时要用 -S -。
注意 send-keys 的转义:内容里含引号或 $ 时要当心外层 shell 与 tmux 的两次解析。稳妥做法是写成脚本文件再 send-keys -t <会话> 'bash /tmp/doit.sh' Enter,避免嵌套引号。
与 systemd 的取舍
这两者的分界很清楚,混淆会带来麻烦:
| 需求 | 用什么 |
|---|---|
| 一次性长任务(升级、备份、构建、数据迁移) | tmux(或 nohup) |
| 需要开机自启、崩溃重试、日志归集的服务 | systemd |
把服务挂在 tmux 里是常见的错误做法,它缺三样东西:
- 重启后不会恢复——tmux 会话本身不是开机自启的(即使配置了,也没有崩溃重试)
- 没有失败重试——进程挂了就一直挂着,没人拉起
- 日志不归集——输出散在某个窗格里,
journalctl查不到,出事后无法回溯
一个长期运行的服务应该是:
[Unit]
Description=My service
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/myservice
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Type=simple + Restart=on-failure 就提供了 tmux 给不了的自愈能力,而日志自动进 journal。用 tmux 跑服务,本质上是在手工重造一个更差的 systemd。
反过来,给一次性任务写 systemd 单元也是过度设计——你并不需要它开机自启,只需要它这次跑完。这时候 tmux 是更轻的选择。
判断要点
- 任务超过几分钟、且断开连接会造成损失 → 放进 tmux,并给它起名
- 需要在断开后回来查看进度 →
attach,看完Ctrl-b d离开 - 想让脚本与交互式程序对话 →
send-keys输入、capture-pane -S -取完整输出 - 需要开机自启或崩溃自愈 → 这不是 tmux 的职责,写 systemd 单元