晚上 十点,你 启动 了 一个 沪深300 ETF(510300)的 5 分钟 均值回归 策略 回测,参数 扫了 三十 组,估计 要 跑 一整夜 加 半天。你 把 笔记本 一合 准备 回家,然后 突然 想起 一件 事:ssh 连接 一断,那个 Python 进程 就 死了。第二天 一早 你 还得 看 进度、还要 在 跑到 一半 时 杀掉 它 重启。这一课 教 你 怎么 用 tmux 把 长 任务 留下、用 ps / top / htop 看 它 在 干 什么、用 信号 干净 地 关 掉 它、用 rsync 把 数据 在 机器 之间 倒。
进程 模型
每个 在 跑 的 程序 是 一个 进程,有 唯一 的 进程 号(process id, PID)和 父进程 号(parent PID, PPID)。shell 自身 是 一个 进程,它 spawn 的 每个 子进程 继承 它 的 环境变量 与 工作目录。
ps aux 是 BSD 风格 的 「列出 所有 进程」;ps -ef 是 System V 风格 的 等价 形式。常 看 的 列:USER、PID、%CPU(瞬时 CPU 占用)、%MEM(物理 内存 占比)、VSZ(virtual size,虚拟 内存——因 mmap 很大 是 正常 现象,不要 慌)、RSS(resident set size,真正 占 的 物理 内存)、STAT(状态:R 运行、S 睡眠、D 不可中断 I/O、Z 僵尸)、START(启动 时刻)、TIME(累计 CPU 时间)、COMMAND(命令 行)。pstree -p 给 你 父 / 子 树形 视图;pgrep -f 'backtest.py' 按 完整 命令行 找 PID。
ps aux | grep -E 'backtest|python' | grep -v grep
pgrep -af backtest.py
top -p $(pgrep -d, -f backtest.py)
grep -v grep 把 grep 自身 这行 从 结果 里 排除,是 经典 习惯。pgrep -af 输出 「PID + 完整 命令行」;top -p 锁定 一组 PID 来 实时 看 数字。htop 是 top 的 美观、可 交互 版本——F9 发 信号、F6 改 排序——多 数 研究机 都 装 了。
信号:宇宙 通用 的 IPC
信号 是 内核 投递 给 进程 的 小 整数。日常 量化 开发 里 你 主要 用 这 几 个:
- SIGTERM (15) —
kill <pid>的 默认。语义 是 「请 收尾 退出」。好 写 的 程序 装 一个 handler(Python 里signal.signal(signal.SIGTERM, ...)或 try/finally),flush buffer、关 数据库 连接、再 退。 - SIGKILL (9) —
kill -9 <pid>。不可 捕获、不可 忽略——内核 直接 终结 进程。这是 最后 手段,因为 进程 没 机会 清 自己 留下的 临时 文件 / 数据库 行锁。 - SIGINT (2) — Ctrl-C。Python 里 翻 成
KeyboardInterrupt。 - SIGHUP (1) — 终端 关闭 时 投给 它 的 子进程。
nohup之所以 叫nohup,就是 让 进程 忽略 这一信号。 - SIGSTOP (19) / SIGCONT (18) — 暂停 / 继续。Ctrl-Z 实际 是 SIGTSTP,由 shell 接到 后 把 前台 任务 暂停。
kill 12345 # SIGTERM: please clean up and exit
sleep 10; ps -p 12345
kill -9 12345 # SIGKILL: cannot be caught, cannot be ignored
信号 选择 口诀:先 SIGTERM 给程序留清理时间;只有真死了才 SIGKILL。一上来 就 -9 是 业余 习惯——你 在 给 自己 制造 stale 临时 文件 与 半写 数据。
作业 控制 与 tmux
shell 自己 也是 一个 「作业(job)调度器」 的 缩水版。命令 末尾 加 & 把 它 投到 后台 启动:python -m backtest.run --date 2025-04-24 &,shell 立刻 返回 提示符,打印 [1] 12345(作业 号 1、PID 12345)。jobs 列 当前 shell 的 作业;fg %1 把 作业 1 拉 回 前台;bg %1 把 一个 已 暂停 的 作业 在 后台 继续。Ctrl-Z 把 前台 作业 暂停(shell 接 SIGTSTP)。disown %1 把 作业 从 shell 的 表 里 摘除——shell 关 闭 时 就 不会 给 它 发 SIGHUP。nohup cmd & 一步 到位:忽略 SIGHUP、把 stdout / stderr 重定向 到 nohup.out。
但 nohup 是 老一代 工具。现代 做法 是 tmux:
tmux new -s bt-mr5m # 开一个名为 bt-mr5m 的会话
python -m backtest.run --date 2025-04-24 2>&1 | tee run.log
# Ctrl-b d 分离会话(detach),进程继续跑
# 第二天从一台新 ssh 上来:
ssh quant@research-cn
tmux attach -t bt-mr5m # 把同一个会话拉回来
tail -F run.log # 跟踪日志,文件被轮替也能跟
A-股 量化团队 习惯 在 tmux 中 跑 多日 backtest——一个 会话 一个 实验,命名 规范 直接 写 在 会话 名 里(bt-510300-mr5m = 510300 上 的 mean-reversion 5min)。screen 是 老一代 的 等价 物,现在 基本 只在 老 服务器 上 见。
远程 shell 与 rsync
ssh user@host 是 交互式 登录。ssh user@host 'cmd' 在 远程 跑 一条 命令 然后 返回——经常 用来 检查 远程 文件 是否 存在、查询 远程 磁盘。~/.ssh/config 给 主机 起 别名:
Host research-cn
HostName 10.0.0.7
User quant
之后 ssh research-cn 就 是 上面 那 一坨 的 简称。
scp local remote: 拷 单 文件 / 小 目录。**rsync 是 量化数据 同步 的 日常 工具**:增量 同步、断 点 续传、可以 走 ssh。-avz 是 archive + verbose + compress(默认 套餐),--progress 显示 进度,--delete 让 目的 端 与 源 端 同步——这是 危险 操作。
rsync -avz --progress --dry-run --delete data-staging:/staging/cn/min1/20250424/ ./local-cache/ # --dry-run first, every time
rsync -avz --progress --delete data-staging:/staging/cn/min1/20250424/ ./local-cache/
三 条 rsync 纪律 一次 写 给 你:
--dry-run永远 走 在 真 跑 之前——尤其 当 命令 里 同时 出现--delete。- 带 不带
src/末尾 斜杠 决定 拷 的 是 「目录 内容」 还是 「目录 自身」——念 错 一次 就 多 走 一层 目录。 - 用
--progress看 进度、用-avz默认 套餐、跨 机 一律 走 ssh。
这 在 「--delete 把 一天 的 工作 删 没了」 之类 事故 之后 不用 再 学。
资源 控制:ulimit 与 /usr/bin/time -v
ulimit -a 列出 当前 shell 的 软 限制。最常 撞 的 一条 是 「too many open files」——Python 进程 在 大量 并发 HTTPS 请求 时 把 文件描述符 撑 爆,默认 是 1024。一次 性 拉 上来:
ulimit -n
ulimit -n 65536
/usr/bin/time -v python -m backtest.run --date 2025-04-24
/usr/bin/time -v(注意 是 完整 路径,不是 shell 内建 的 time)跑 完 后 打印 一 堆 计数。三 个 数 优先 看:
Elapsed (wall clock) time— 真实 流逝 时间。Maximum resident set size (kbytes)— 峰值 物理 内存。这 才 是 「这个 backtest 吃 了 多少 内存」 的 答案;不要 看 VSZ。Major (requiring I/O) page faults— 大于 0 通常 意味着 进程 在 swap 到 磁盘,研究机 上 一旦 看到 这个 数 上来 就 该 排查 内存 不够 用。
nice -n 10 cmd 起 一个 低 调度 优先级 的 进程(正 nice = 「我 让 着 别人」);ionice -c 3 cmd 降 I/O 优先级。研究机 是 多 人 共享 的 时 候 这两条 都 用 得 上。
工作 流:tmux + 监控 + 干净 收尾
把 上面 几 块 拼起来。把 一个 多日 510300 backtest 放进 一个 tmux 会话,在 里面 跑、log 到 文件、ssh 断 线 不 死;第二 天 重 连 attach 回来 看 tail,发现 一组 参数 不收敛 想 立刻 杀掉。SIGTERM 给 它 留 几秒 收尾,写 pnl.csv 落地 当 天 的 部分 结果(这 一段 由 第 4 课 的 trap 实现);如果 五 秒 后 还 没退,SIGKILL 它,再 启动 一组 新参数。这是 你 接下来 几年 的 日常。
一句 关于 cgroups:ulimit 限制 的 是 单 进程,cgroups 限制 的 是 一组 进程(可以 按 内存、CPU、I/O 限)。容器 内的 资源 限制 用的 就是 cgroups。再 深 的 跑 法 留给 3.6.5(构建、部署 与 容器)。
练习
Exercise
你 启动 了 一个 Python backtest:python -m backtest.run --date 2025-04-24 &。现在 想 (a) 用 pgrep -f backtest.run 找 它 的 PID,(b) 从 /proc/$PID/status 的 VmHWM 行 读 它 的 峰值 物理 内存,(c) 先 用 kill $PID 发 SIGTERM 并 等 5 秒,(d) 如果 还 活着(用 ps -p $PID 验证),用 kill -9 $PID 发 SIGKILL,(e) 在 一个 名为 bt-rerun 的 tmux 会话 中 重启 一次,让 ssh 断 线 也 不会 丢。把 每 一步 的 命令 写 出来。
提示
PID=$(pgrep -f backtest.run) 把 PID 抓到 变量 里。grep VmHWM /proc/$PID/status 直接 读 那一行。sleep 5 等 5 秒。ps -p $PID 还 在 输出 就 说明 进程 没 退。提示
tmux new -s bt-rerun,里面 跑 python -m backtest.run --date 2025-04-24 2>&1 | tee run.log;分离 用 Ctrl-b d;下次 上来 tmux attach -t bt-rerun。会话 名 就是 你 一周 后 的 路标。下一课预告
至此 你 已经 能 把 一个 长 任务 留在 研究机 上 跑、能 在 它 出 问题 时 干净 关 掉、能 把 数据 在 机器 之间 倒。但 这 一切 还是 「手 跑」 的——每 晚 你 还是 要 上 去 敲 一遍。下 一 课 教 你 把 它 写 进 一个 bash 脚本,加 上 set -euo pipefail、trap 清理、getopts 解析 参数,再 用 cron 或 systemd --user 定时 调度,整个 端到端 落地 流程 才算 真正 立住。
阅读清单
- 《鸟哥的 Linux 私房菜》第四版,第 16–17 章(进程管理 与 系统服务 SystemD)。
man 7 signal中文版(信号 标准 参考)。man ssh_config/man rsync中文(社区翻译)。- tmux 中文 用户指南(社区翻译)。
一条 额外 注释 提醒:A-股 量化团队 习惯 在 tmux 中 跑 多日 backtest,研究机 与 数据机 之间 通过 rsync 同步 落地数据;本地 笔记本 / 公司 网络 之间 走 VPN 或 跳板机。tmux + rsync + ssh 的 三件套 你 这几年 每天 都 会用。
参考卡
本课 出现 的 Fenced ```bash 块:进程 三 查(ps aux、pgrep -af、top -p)、信号 升级(SIGTERM 后 SIGKILL)、tmux 工作流(new / attach / tail -F)、rsync --dry-run 纪律、资源 计量(ulimit -n、/usr/bin/time -v)。