ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SSH断开任务就死?nohup与tmux让后台任务永不掉线

SSH断开任务就死?nohup与tmux让后台任务永不掉线 很多刚接触 Linux 服务器的人应该都遇到过这个场景本地电脑连上服务器跑一个数据同步脚本估摸着要十分钟你正想合上笔记本去倒杯水结果屏幕一锁、Wi-Fi 一断回来一看终端里全是报错任务没跑完进程也没了。又或者你白天在办公室连着公司服务器跑训练任务下班回家一断开 SSH任务就跟着“陪葬”了第二天上班发现白跑一晚。这里面的核心问题不在于任务本身多复杂而是一个很底层的机制SSH 会话一旦断开系统会向这个会话里的前台进程发送挂断信号进程默认收到信号就退出。搞清楚这个机制之后解决思路其实就几条要么让进程忽略挂断信号要么让进程彻底脱离这个会话要么干脆让进程在一个独立的“会话房间”里跑。这篇文章就把这几条路都捋一遍从原理到命令到坑一次性聊透。我写这篇文章不是为了给你背一堆命令而是希望你看完之后能自己判断什么样的任务适合用什么方案出了问题怎么排查。下面直接进入正题。1. 为什么SSH一断开程序就跟着“断气”想解决问题先得知道问题是怎么来的。这一节把 SSH 会话和进程之间的关系拆开讲透后面你用命令的时候心里才有底。1.1 前台进程和SSH会话的“生死绑定”SSH 连接不是你敲完命令就结束的关系它本质上是一个“会话通道”。你通过 SSH 登录到服务器服务器会给你分配一个会话这个会话里跑着你的 Shell通常是 bash 或者 zsh。你在 Shell 里启动的进程只要是前台运行的就都属于这个会话的一部分。可以这么理解SSH 会话是一间“房间”Shell 是房间里的“管家”你启动的程序是房间里的“住客”。正常情况下管家和住客都在房间里正常工作。可一旦 SSH 连接断开比如网线被拔了、电脑休眠了、网络超时了服务器那边的“房间”就会判定主人已经走了这间房要拆了。房间拆掉里面的住客自然也得跟着离开。问题在于这个“离开”不是客客气气地让程序做完收尾工作再走而是直接来一刀会话终止的时候系统会向会话里所有的前台进程发送 SIGHUP 信号。你的程序如果不特殊处理这个信号默认动作就是退出。所以很多脚本、训练任务、同步进程都是在一瞬间被“砍”掉的连报错都来不及写完整。1.2 真正的“杀人凶手”SIGHUP信号SIGHUP 这个名字是有来历的“HUP”是“hang up”的缩写最早是电话挂断的意思。在 Unix/Linux 世界里它被用来表示“控制终端挂断了”。你想想看一个进程原本在终端里运行终端没了它连输出都不知道往哪里写输入也没人给这种情况下让它继续运行已经没什么意义了所以系统默认把它终止掉。信号机制的优先级很高程序很难在收到 SIGHUP 之后还能“假装没听到”继续跑除非它显式地忽略这个信号。而很多命令行工具和脚本压根不会去处理 SIGHUP所以后果就是SSH 断开任务夭折。这里还有个很容易被忽略的细节不是只有 SSH 断开才会触发 SIGHUP。你本地开一个终端窗口直接关掉窗口同样会给里面的前台进程发 SIGHUP终端模拟器卡死之后你强制关掉进程也是一样的效果。明白了这一点你就知道为什么这类问题在本地终端里也会出现只不过在 SSH 场景下更常见、更让人恼火因为你人都已经离开办公室了任务死在服务器上也浑然不知。1.3 先学会“看现场”确认进程是怎么没的很多读者可能会问我怎么知道程序是收到 SIGHUP 死的还是自己崩溃的其实排查起来也不难几个命令就能看出端倪。第一个是看日志。大多数程序自己会记录日志你翻一下程序最后几行日志如果日志戛然而止、没有正常的结束标记很可能就是被信号干掉的。第二个方法是看进程的退出信息如果你当时在终端前台运行断开连接的时候你根本看不到输出但如果用的是带记录的终端工具可能能看到部分残留。第三种方式是看系统日志不过大多数普通任务不会往系统日志里写那么细主要还是靠程序日志判断。我个人更推荐的做法是别赌自己记不记得直接用实验验证一次。你开一个 SSH 连接跑一个简单的sleep 30然后另开一个终端用ps -ef | grep sleep看它的父进程是谁接着你把第一个 SSH 连接断掉再去看那个sleep进程还在不在。实验过一次你对 SIGHUP 的印象就深刻了后面看任何“SSH 断开程序消失”的问题都会秒懂。2. 最朴素也最管用的三招nohup、disown、setsid原理搞清楚了直接上解决方案。这一节先说三个最经典的命令它们都是围绕“让进程不收到挂断信号”或者“让进程脱离会话”来设计的。这三个命令覆盖面已经很广日常绝大多数场景都够用了。2.1 nohup让进程“忽略”挂断信号nohup可能是你听过最多的一个命令全称是 “no hang up”意思就是“不要挂断”。用法非常简单在命令前面加上nohup然后在末尾加一个让它在后台运行nohup python train.py train.log 21 我来拆解一下这条命令的每个部分nohup告诉系统启动这个进程的时候把 SIGHUP 信号屏蔽掉就算终端挂了也别影响它。python train.py是你原本要执行的命令。 train.log把标准输出重定向到文件。21把标准错误也重定向到同一个文件这样报错和正常输出都在里面。末尾的让进程放到后台运行命令敲完会立刻回到 Shell 提示符不会一直卡着你的终端。注意nohup有一个行为习惯如果你不重定向输出它默认会把输出写到当前目录下的nohup.out文件里。这算是个兜底设计但实际用的时候我建议总是手动指定日志文件因为nohup.out默认文件会越攒越大而且你在不同目录下跑多个任务时容易搞混。运行之后会输出一行提示大概是[1] 12345什么的这个数字就是进程的 PID。想确认进程活着就用ps -ef | grep train.py查看。想停掉它用kill 12345即可。nohup最大的优点是简单直观不需要任何额外工具几乎每个 Linux 发行版都有。缺点也很明显它只是屏蔽了 SIGHUP并没有让进程脱离会话。如果 SSH 断开之外还有其他因素导致会话被清理进程依然可能受影响。而且用nohup启动的进程你很难再“接回来”操作它属于一次性放出去就不管了。2.2 disown任务已经跑起来了再“解除关系”nohup要求在启动命令前就规划好但实际中我们经常遇到的情况是任务已经在前台跑起来了或者已经在后台用启动了跑了半天才想起来“糟了我要是断开 SSH 它不就死了吗”。这时候nohup帮不上忙因为它只能在启动时介入。而disown正好解决这个问题它可以“事后补救”。disown是 Shell 内置命令作用是把指定的任务从当前 Shell 的任务表中“移除”让它不再受会话退出时信号清理的影响。使用方式分两步# 第一步任务已经在后台运行比如你用 启动的 python train.py train.log 21 # 第二步查看任务列表找到任务编号 jobs -l # 第三步把任务 disown 掉 disown %1jobs -l会输出类似[1] 12345 Running python train.py ...的信息%1就是任务编号。执行disown %1之后这个任务就跟当前 Shell 的会话“解绑”了你断掉 SSH它也会继续跑。还有一种写法是disown -a把当前 Shell 里所有后台任务全部 disown 掉适合任务特别多的情况。disown -h则表示只标记任务不接收 SIGHUP但保留在任务列表里这个相对少见了解一下即可。实战中的建议是如果你已经进了终端发现任务在前台卡着先按CtrlZ把任务挂起再执行bg让它到后台运行接着用jobs -l查到编号然后disown %1一套操作下来任务就保住命了。这套连招值得背下来因为关键时刻真能救命尤其是你半夜想起来自己白天启动了个长任务还没做保护处理的时候。2.3 setsid让进程彻底“脱离会话”nohup是屏蔽信号disown是解除任务表关系而setsid的思路更彻底直接让新进程创建一个全新的会话跟当前终端完全脱离关系。setsid python train.py /dev/null train.log 21注意这里我没有加因为setsid会自己把进程放到新会话中命令执行后会直接返回 Shell 提示符不需要再加。当然你加了也无妨只是容易产生一个多余的空进程。 /dev/null这句容易被忽略但很关键。进程脱离了会话之后标准输入如果还指向原来的终端一旦终端关闭进程尝试读输入可能会触发异常。所以把标准输入重定向到/dev/null是一个标准的“脱机”姿势意思是这个进程不再从任何终端读取输入。和nohup相比setsid的保护力度更大因为它不仅让进程忽略 SIGHUP还让进程彻底脱离了原来的会话、进程组和控制终端等于是一个全新的“组织”。实际使用中我的习惯是能不用nohup的场景用nohup就行但如果这个进程需要长期运行、又特别重要就上setsid多一层保障。尤其是跑数据同步、长时间编译这类任务信号干扰本来就多setsid更稳妥。2.4 三个命令怎么选一个顺手参考很多读者看到三个命令可能有点晕我直接给一个简化版的选择逻辑方案核心原理适用场景限制nohup启动时忽略 SIGHUP 信号启动前就知道要长跑的任务、临时任务快速保护无法事后补救进程仍属于原会话disown启动后从 Shell 任务表移除任务已经跑起来需要紧急保命只对当前 Shell 的后台任务有效Shell 退出前操作setsid创建新会话彻底脱离终端长期任务、关键任务希望不受会话影响无法用fg或jobs找回只能靠日志观察这几个方案有个共同点它们都是“进程启动后基本告别交互”的模式。假如你跑的任务需要跑到一半观察一下结果、改一改参数、甚至随时进去看看训练曲线这三个方案就不太够用了。下一节讲的tmux才是这种需求下的正解。3. 正菜来了用 tmux 给任务建一个“永不断线的房间”如果说nohup和setsid是“任务放出去就不管”那tmux就是“任务在一个独立房间里跑你想进去看随时进去想出来随时出来就算门锁坏了SSH 断开房间里的东西依然在。” 这也是我目前最推荐的方式没有之一。3.1 为什么 tmux 比 nohup 更值得依赖tmux是一个终端复用器简单说就是你在服务器上开一个“虚拟终端房间”这个房间不受 SSH 连接存亡的影响。你可以随时随地重新“进入”这个房间看到任务还在跑输出还在继续甚至可以同时打开好几个窗口、分屏操作。和nohup比tmux最大的优势是可恢复性。nohup启动的进程你想看输出只能去翻日志文件tmux里跑的任务你重新连回 SSH、重新tmux attach就能看到终端上正在滚动的实时输出就像你从来没有离开过一样。而且tmux里跑多个任务时每个任务可以开一个窗口互不干扰、一目了然。还有一点特别实用tmux可以“假死”和“复活”。你在公司跑任务下班前把整个tmux会话 detach 掉回家 SSH 一连接上再 attach 回来状态完整保留。断网也一样你重新连上之后 attach 回来一切照旧。这种体验是nohup完全给不了的。如果你系统里没有tmux安装也很简单CentOS/RedHat 系用yum install tmuxUbuntu/Debian 系用apt install tmuxmacOS 上brew install tmux。装好之后不用额外配置默认设置已经够日常使用了。3.2 tmux 三步走新建、分离、恢复我把最常用的操作流程写成一条龙照着敲就能跑通# 第一步新建一个 tmux 会话并给会话起个名字比如 train tmux new -s train # 第二步在里面正常跑你的命令比如 python train.py train.log 21 # 或者干脆直接写 python train.py输出直接在 tmux 窗口里滚动 # 第三步让会话在后台“挂着”按快捷键Ctrl b然后松手按 d # 这个操作是 detach分离不是退出会话分离之后你会回到正常的 Shell prompt。这时候你尽管断开 SSH 走人。下次回来重新连接服务器然后用# 查看当前有哪些 tmux 会话 tmux ls # 恢复之前的会话 tmux attach -t traintmux attach -t train之后你看到的就是之前那个界面任务还在跑输出条还在滚动就像中间什么事情都没发生过一样。想停掉任务在 tmux 窗口里按CtrlC就行。想彻底关闭这个 tmux 会话在窗口里输入exit即可。除了这些基本操作还有几个高频快捷键值得记一下在 tmux 会话里Ctrlb是前缀键按下前缀之后再按c新建一个窗口按n或p切换下一个/上一个窗口按%左右分屏按上下分屏。窗口一多你还能按Ctrlb再按窗口编号快速跳转跑多个任务时真心方便。我个人习惯是给每个重要任务单独开一个 tmux 会话并用任务名命名。比如跑训练脚本就叫train跑抓取任务就叫spider这样长久使用下来不会混乱tmux ls一目了然。3.3 连接中断、窗口假死时的恢复策略tmux也不是万能的有一种情况很多人遇到过SSH 断开前 tmux 会话还在但你重新连上 SSH 之后输入tmux attach却报错或者卡住进不去。这种问题大多不是 tmux 本身的问题而是之前的 tmux 进程还“误以为”自己连接着旧的终端状态没有完全释放。遇到这种情况不要慌先执行tmux ls看看会话是不是还挂在列表里。如果会话还在但 attach 卡住可以先执行tmux detach -a把其他所有客户端的连接都强制断开再重新 attach。如果会话列表里什么都没了你的任务大概率是跟着旧的 tmux 进程一起跑了只能去日志文件里找线索。另一个常见问题是 SSH 断网后 tmux 进程还在但显示乱码或者界面错乱。先tmux detach再重新attach一次通常能恢复。实在不行tmux kill-session杀掉这个会话重新在同一个窗口里运行任务——不过这个操作会杀掉里面的任务所以不到万不得已不要轻易执行。更稳妥的做法是进入 tmux 后在 Shell 里重新运行一次任务养成“一切重要任务都在 tmux 里跑”的习惯偶尔出问题也有心理准备。这里还要补一句screen是 tmux 的老前辈功能定位相似。有些人习惯用screen -S name新建会话、screen -r name恢复会话原理和tmux几乎一样。如果你所在的老服务器上没有tmux用screen顶上完全没有问题。我个人更推荐tmux因为分屏操作、会话管理、窗口命名这些细节体验确实更好但两者选哪个看个人习惯都能解决同样的问题。4. 日志、跟踪与多任务管理把后台任务玩明白很多场景下任务跑起来只是第一步怎么持续观察它、怎么管理多个任务、怎么处理日志才是真正决定体验的部分。这一节集中讲几个高频细节。4.1 输出重定向的坑别让日志文件失控用nohup或setsid跑任务时日志文件是你唯一的观察窗口所以输出重定向不能马虎。之前提过 train.log 21这个写法把标准输出和标准错误都写进同一个文件简单高效。但有几个细节值得注意。第一是覆盖写是追加写。如果任务能跑多次建议用否则第二次启动的时候之前的历史日志就被清了排查问题的时候你会很痛苦。第二很多程序有输出缓冲机制。比如 Python 的print默认往文件里写时会走缓冲区如果程序突然崩溃或者被信号干掉最近几行输出可能还在缓冲区里没落盘导致日志文件“缺尾巴”。解决方法是给 Python 加上-u参数强制无缓冲运行或者设置环境变量PYTHONUNBUFFERED1。其他语言也各有对应的办法这是一个非常容易踩的细节。第三日志文件不要放根目录或者家目录底下随手一丢。我的习惯是在用户目录下建一个类似~/logs的目录按任务名命名比如~/logs/train/。时间一长你就知道这个习惯有多重要找日志、做归档、删旧文件全都方便。还有一点关于日志的查看方式任务在跑的时候想跟tail一样实时看最新输出不一定要tmux进会话直接在 Shell 里tail -f train.log-f是“跟随”模式有新内容会实时刷出来。想看最后 50 行加一个-n 50即可。搭配CtrlC退出查看不影响任务本身非常安全。4.2 任务真的还在跑吗常用检查手段任务切到后台之后第一件事就是要确认它确实活着。最常用的检查命令是ps -ef | grep train.py这个命令会列出所有包含train.py字样的进程。注意grep结果里通常有一行是grep train.py本身那个是无关的看有没有真正的 Python 进程即可。还有一种检查方式是用端口、PID 等更精确的方式。如果你的任务监听了某个端口比如 Web 服务那就直接netstat -tlnp | grep 8080。如果任务已经启动了一段时间你还想看看它占了多少内存、CPU 使用率那就用top -p 1234512345换成实际 PID。CPU 跑满和内存涨得快一眼就看出来。任务管理方面想根据 PID 杀掉任务用kill 12345还不听话就用kill -9 12345强制杀。注意kill -9是最后手段它会直接干掉进程不给清理资源的机会有些程序的数据可能写一半就坏掉能不用就不用。4.3 多个任务同时跑怎么管理不混乱当你同时跑好几个任务的时候光靠ps和grep就显得粗糙了。我自己管理多任务时的习惯是一个tmux会话一个任务会话名就是任务名。这比在同一个 shell 里狂开后台进程清晰得多。你随时tmux ls看有哪些会话对应哪些任务一目了然。想进去看哪个就tmux attach -t进哪个互不干扰。如果你不想用tmux只用纯后台进程也可以给自己定个准则每个任务写日志日志文件名和任务名一致启动时记录 PID 到一个小文本文件里。这样出问题的时候心里有谱。不过说实话这么操作过一阵子之后你会由衷体会到tmux的好最后还是回到tmux来。还有一个更“重”但更可靠的方向如果你管理的是一组常年跑的服务不是一次性脚本那最适合的工具其实是systemd直接把这些服务做成 systemd service开机自启、崩溃自动重启、日志走journalctl属于服务器运维的基本功。这里不展开讲但提一句tmux适合人肉管理临时任务systemd适合机器自动管理长期服务两者定位不同可别搞混。4.4 前台任务想转后台一套安全连招有时候你已经在终端前台跑起了任务跑着跑着才意识到要断开 SSH这时候直接CtrlC杀掉重来太浪费。有一种连招可以不中断任务把它“转”到后台# 1. 按 CtrlZ 挂起当前任务 # 2. 把任务切到后台运行 bg # 3. 查看任务编号 jobs -l # 4. 解除任务与 Shell 的绑定 disown %1这一套操作做完任务就安全了可以放心断 SSH。这个连招在nohup、tmux这些工具没来得及用的时候特别管用。我记得有一次跑一个数据迁移任务跑了一多半才意识到晚上要断网就是靠这个连招保住命的不然第二天全部重来。当然手忙脚乱容易出错更稳妥的做法是从一开始就用tmux不让这种紧急情况发生。5. 常见问题与排查技巧实录写着写着感觉还是得把实际中真正遇到过、或者读者容易踩的坑集中汇总一下。这一节不走教科书路线直接列问题、给判断、说解法。5.1 高频问题速查表现象原因分析处理办法SSH 断开后任务消失前台进程收到 SIGHUP 被终止使用tmux、nohup、setsid启动任务用 nohup 启动后没有日志没重定向输出写入了 nohup.out检查 nohup.out或启动时手动指定 log 21nohup 启动的进程还爱死进程没脱离会话受其他信号影响改用tmux或setsidtmux attach 卡住旧终端状态没释放tmux detach -a断开其他客户端再 attach日志里有输出但文件不刷新程序输出缓冲未落盘用-u或无缓冲模式启动或设置环境变量任务还在跑但 ps 找不到进程名被改了或使用虚拟进程名用ps -ef看完整命令行或按 PID 查询断网后 tmux 会话正常但窗口乱码终端宽度变化引起显示问题重新 attach 或先 detach 再 attach这张表覆盖了我遇到过的大部分场景。建议你收藏起来遇到类似问题先翻一眼能少走不少弯路。5.2 几个难啃的实操心得说几个比较细、但实际有用的心得都是文档里不常提的。第一个是关于日志文件权限。你用某个用户启动任务日志也是那个用户创建的。如果你平时有习惯用另一个用户或者 root 去tail -f日志可能会因为权限不足看不到内容。反过来你用 root 启动任务日志写到普通用户目录下也会遇到目录权限问题。解决很简单日志目录统一建在一个约定好的位置比如/var/log/myapp/或者~/logs/权限按需放开。别小看这个细节日志看不了的时候真是干着急。第二个是关于 SSH 连接断开的判定。有时候你以为的“程序被杀”其实是误会。比如 SSH 连接确实断了但服务器上任务还在跑只是你无法看到输出。这种场景在tmux下尤其常见你 attach 不上去以为任务没了实际上任务活得好好的只是 tmux 服务端和客户端之间的缓存出了问题。所以判断任务是否存活一定要以ps输出为准不能以自己的终端显示为准。第三个是关于nohup命令的一个行为。有些发行版里直接敲nohup 命令 会提示输出被重定向到nohup.out这个提示不影响使用但会误导一些人以为命令没启动成功。实际上进程已经正常启动了你只需要去查ps确认即可。第四个也是我个人总结得最深的一点不管用什么方案日志永远是最可靠的“证人”。任务跑得好好的时候大家都懒得多写日志一出事就后悔。如果你要跑一个长任务启动前想清楚日志记录哪些关键节点运行过程中出现异常该怎么判断任务结束后有没有结束标记。这些看似琐碎遇到问题时能让你少折腾一晚上。5.3 再给一个容易被忽略的细节SSH工具本身的设置有一部分场景下SSH 断开的“锅”不在机制而在 SSH 客户端或服务器的心跳超时设置。比如你的网络环境不稳定或者经过某种代理中转SSH 连接可能因为长时间没动静被中途掐断。这种情况下任务本身没问题但你的体验就是“一切断就死”容易误判。解决方向可以从两端入手。SSH 服务端服务器上可以调整ClientAliveInterval和ClientAliveCountMax作用是定期探测客户端是否存活减少因网络静默导致的误杀。SSH 客户端上可以启用 ServerAliveInterval 机制定期发送心跳包维持连接保活。这两处配置属于运维基本功但不少初学者根本不知道导致明明不是信号终止的问题也背了锅。判断标准也很简单如果你断开 SSH 之后很快重连发现进程还在跑之前的“死”就根本不是 SIGHUP 杀的而是连接本身出问题了。最后再分享一点个人的经验这篇文章从原理到方案再到排查基本把“SSH 断开之后程序如何继续执行”这个问题讲透了。我第一次遇到这个坑的时候还不知道什么叫 SIGHUP只知道断网之后训练进度全没了整个人是崩溃的。后来搞明白了原理学会了nohup再后来彻底转向tmux这个问题的困扰已经基本消除了。如果让我给你一个最简单的行动建议从现在开始凡是跑超过几分钟的任务一律先tmux new -s 任务名然后在里面跑。不用想太多这一个习惯就够你用很久了。等你对tmux越来越熟你会发现它不仅解决“SSH 断开”这一个问题还让你的多任务管理、远程协作、断点续跑全都顺畅起来。至于nohup和setsid了解、会用即可关键时刻能救命。但论日常使用的舒适度和安全感我还是无条件站在tmux这边。希望这篇文章能帮你少踩几个坑从此告别“一断开 SSH 任务就死”的悲剧。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进