Skip to content

第三章:持久会话

SSH 陷阱

你 SSH 到服务器,启动了训练,合上笔记本回家。

训练死了。

你没有 kill 它,没有按 Ctrl+C,也没有显存不足。SSH 连接断开了——你在那个 SSH 会话里启动的所有进程都收到了 SIGHUP 信号,然后终止了。你那个 14 小时的训练,跑了 47 分钟就死了,原因是你合上了笔记本、WiFi 断了、或者 VPN 抽风了。

这就是 SSH 陷阱,每个研究者至少会踩一次。它是人们在实验室熬夜的原因——坐在终端前面,训练在跑,连笔记本都不敢碰。它是人们在第二块屏幕上保持一个 SSH 窗口死活不关的原因。它是人们在周五晚上离开前启动训练,然后在手机上强迫症般地检查,直到手机上的 SSH 会话也断了的原因。

问题是根本性的:SSH 把你的进程绑定在你的连接上。连接断了,进程就死了。这是设计如此——Unix 会话就是这样工作的。你的训练脚本是 SSH shell 的子进程。杀掉父进程,子进程也跟着死。

你需要一种方法,把进程和 SSH 连接解耦。你需要训练在服务器上独立运行,不管你有没有连着,不管笔记本开没开,不管手机有没有信号,不管你醒没醒。

这个工具叫 tmux。


Tmux 的工作原理

Tmux(终端复用器)用一个简单的架构技巧解决了 SSH 陷阱:它把服务端客户端分开了。

当你在一台机器上启动 tmux 时,它会在后台创建一个服务端进程。在这个服务端里,你创建会话——每个会话就像一个虚拟终端。你实际的终端(通过 SSH 连上来的那个)只是一个客户端,用来显示会话里正在发生什么。

看起来是这样的:

你的 SSH 连接                     服务器
┌──────────────┐                ┌──────────────────────────┐
│              │   SSH 隧道     │                          │
│  终端        │◄──────────────►│  tmux 服务端(始终运行)   │
│ (客户端)    │                │    │                     │
│              │                │    ├─ 会话: train        │
│              │                │    │   └─ python train.py│
│              │                │    │                     │
│              │                │    └─ 会话: eval         │
│              │                │        └─ python eval.py │
└──────────────┘                └──────────────────────────┘

当 SSH 断开时:

┌──────────────┐                ┌──────────────────────────┐
│              │                │                          │
│  终端        │   ✗ 断了 ✗     │  tmux 服务端(还在跑!)   │
│ (死了)      │                │    │                     │
│              │                │    ├─ 会话: train        │
└──────────────┘                │    │   └─ python train.py│
                                │    │       (继续运行!)  │
                                │    └─ 会话: eval         │
                                │        └─ python eval.py │
                                │            (继续运行!)  │
                                └──────────────────────────┘

当你的 SSH 连接断开时,客户端消失了——但 tmux 服务端继续运行。每个 tmux 会话里的每个进程都像什么都没发生一样继续执行。你的训练脚本不知道也不在乎你断开了。它跟 tmux 服务端通信,不跟你的 SSH 客户端通信。

当你重新 SSH 进来时,你只需要把一个新的客户端连接到同一个 tmux 服务端。你从离开的地方完全接上。终端输出还在那里。进程还在跑。什么都没丢。

这就是为什么 tmux 改变了一切。你的进程不再活在 SSH 连接里,它们活在 tmux 里。SSH 只是你用来查看它们的窗口。


核心命令

你只需要六个命令,不多不少。

创建命名会话

bash
tmux new -s train

这会创建一个叫 train 的新 tmux 会话,并自动连入。你会注意到终端底部多了一条绿色状态栏——这是 tmux 的状态栏,确认你已经在会话里面了。

永远给会话命名。不带 -stmux new 会创建名为 012 的会话。当你同时跑三个训练时,你得知道哪个是哪个。给它们起名:trainevaldownloadexperiment01——什么有意义就用什么。

脱离会话

Ctrl+b  然后  d

这是一个两步按键序列。先按 Ctrl+b(按住 Ctrl,按 b,松开两个键),然后按 d。会话从你屏幕上消失了——但它还在服务器上运行。你回到了普通的 SSH 命令行。

脱离不是关闭。tmux 会话和里面的一切继续运行。这就是重点。

列出会话

bash
tmux ls

显示这台机器上所有运行中的 tmux 会话:

train: 1 windows (created Mon Mar 23 14:30:12 2026)
eval: 1 windows (created Mon Mar 23 15:45:03 2026)

两个会话,都在跑。你可以看到每个的创建时间。

重新连入会话

bash
tmux attach -t train

你回到了 train 会话里。一切如你离开时一样。你不在时产生的终端输出都在——往上翻就行。

终止会话

bash
tmux kill-session -t train

这会真正停止会话并杀死里面的所有进程。当你用完一个会话时用这个——实验跑完了、下载完成了。别让僵尸会话堆在服务器上。

就这些

六个命令:

命令功能
tmux new -s 名称创建命名会话
Ctrl+b d脱离(会话继续运行)
tmux ls列出所有会话
tmux attach -t 名称重新连入会话
tmux kill-session -t 名称终止会话及其进程

tmux 还能做的其他事情——分屏、面板、窗口、脚本——有用但不是现在必需的。之后可以慢慢学。这六个命令足以应对本指南的所有场景。


实测验证

证明它能用。这不是选做——你需要亲眼看到。

第 1 步:SSH 到服务器

bash
ssh lab-server

第 2 步:创建 tmux 会话

bash
tmux new -s test

你现在在一个叫 test 的 tmux 会话里。底部应该能看到绿色状态栏。

第 3 步:启动一个长时间运行的进程

bash
python3 -c "import time; [print(i) or time.sleep(1) for i in range(999)]"

你会看到数字一秒一个往上数:0123……

至少看到数到 5。这就是你的"训练任务"。如果没有 tmux,你现在关掉 SSH 连接,这个进程立刻就死了。

第 4 步:脱离

Ctrl+b,然后按 d

计数消失了。你回到了普通 SSH 命令行。会话还在跑——你只是看不到了。

第 5 步:断开连接

关闭 SSH 连接:

bash
exit

然后彻底关闭你的终端。如果你在手机上,关掉 Termius。如果你在笔记本上,关掉终端应用。SSH 连接没了,彻底断了。

第 6 步:等一等

等 30 秒。去倒杯水。看看手机。干点别的。重点是:你跟服务器之间已经没有任何连接了。你的"训练"(计数器)独自在那里。

第 7 步:重新连接

重新打开终端,SSH 回去:

bash
ssh lab-server

第 8 步:重新连入

bash
tmux attach -t test

第 9 步:见证奇迹

计数器还在跑。它没停过。如果你脱离时数到了 5,等了 30 秒,现在它已经到了 35。你断开的整个过程中它一直在数。

这就是 tmux 的核心承诺:你的进程不会因为断开连接而终止。计数器不知道你离开了,也不知道你回来了。它在 tmux 服务端里一直跑着,完全不在乎你的 SSH 连接状态。

Ctrl+C 停掉计数器,然后终止测试会话:

bash
tmux kill-session -t test

手机友好的 Tmux

你会通过 Termius 在手机上使用 tmux。小屏幕、没有物理键盘、触屏输入。做几个调整会好用很多。

开启鼠标模式

默认情况下,tmux 对鼠标事件的拦截方式可能会让手机终端感到困惑。开启鼠标模式可以让滑动滚屏和选择文本变得自然。在服务器上,创建或编辑 ~/.tmux.conf

bash
echo 'set -g mouse on' >> ~/.tmux.conf

如果你已经有在运行的 tmux 会话,重新加载配置:

bash
tmux source-file ~/.tmux.conf

开启鼠标模式后,你可以在手机上通过滑动来滚屏浏览终端输出——比键盘操作方便多了。

无鼠标滚屏

如果鼠标滚屏在你的终端里不好使(有些手机客户端有兼容性问题),用 tmux 的复制模式:

  1. Ctrl+b,然后按 [ —— 进入复制模式。
  2. 用方向键(或滑动,取决于你的终端)向上翻看输出。
  3. q 退出复制模式,回到正常状态。

复制模式是你在手机上查看报错信息和训练日志的方式。现在就练一练,别等到凌晨两点出事的时候手忙脚乱。

会话名称要短

在手机屏幕上列出会话时,长名字会被截断。保持简短且有意义:

不好
trainmy-training-experiment-v2
exp01bert-finetune-lr3e5-bs32
dldownloading-imagenet-dataset

连入会话后你能看到完整上下文。名字只需要够你从 tmux ls 的列表里选对就行。


Tmux + 训练:每天的固定模式

这是你每天都会用到的模式。简单,好用。

1. SSH 到服务器

bash
ssh lab-server

2. 为实验创建命名会话

bash
tmux new -s experiment01

3. 启动训练

bash
cd /path/to/your/project
conda activate your-env
python train.py --config config.yaml

训练在跑了。你能看到 loss 在降,epoch 在增。一切正常。

4. 脱离

Ctrl+b,然后按 d

训练继续跑。你回到了 SSH 命令行。

5. 去过你的生活

关掉 SSH 连接。合上笔记本。回家。吃饭。睡觉。出去玩一个周末。都无所谓。你的训练在服务器的 tmux 里跑着,它不需要你。

6. 用手机随时查看

任何时候——在公交上、在床上、在餐厅——打开 Termius,SSH 进去,查看:

bash
tmux attach -t experiment01

你看到最新的训练输出。Loss 还在降。第 47 个 epoch,总共 100 个。一切正常。再脱离:

Ctrl+b,然后按 d

手机操作总用时:15 秒。

7. 跑完了再回来

第二天早上,或者你回到工位时:

bash
ssh lab-server
tmux attach -t experiment01

训练跑完了。结果保存好了。你检查结果,清理一下,终止会话:

bash
tmux kill-session -t experiment01

整个流程就是这样。创建会话、启动训练、脱离、去过生活、偶尔看看、跑完回来。不用盯着,不用焦虑,不用在实验室熬夜。


这解锁了什么

有 tmux 之前,问题是:"训练在跑,我能离开实验室吗?"

有 tmux 之后,这个问题不存在了。你启动训练然后离开。方便的时候用手机看一眼。还在跑,好。跑完了,好。崩了——好吧,目前你得用手机 SSH 进去,用大拇指修,很痛苦。但那是后面几章要解决的问题。Claude Code 会处理崩溃。Tmux 只负责保证你的进程不会因为你不在就死掉。

从这里开始的每一章都默认 tmux 已经是你工作流的一部分。Claude Code 会在 tmux 里运行。训练会在 tmux 里运行。下载、评估、监控——全部都在 tmux 里。它是让一切成为可能的隐形基础。


检查点

在 tmux 里启动一个长时间运行的进程:

bash
ssh lab-server
tmux new -s test
python3 -c "import time; [print(i) or time.sleep(1) for i in range(999)]"

Ctrl+b d 脱离。彻底关掉 Termius。等整整一分钟。重新打开 Termius,SSH 回去,用 tmux attach -t test 重新连入。

如果计数器在你不在的时候一直在数——你已经解锁了持久会话。从今以后,你的训练再也不会因为断开连接而终止了。

Released under the MIT License.