关闭MobaXterm窗口后Linux程序如何后台运行
2026/9/17 8:19:57 网站建设 项目流程

半夜两点挂上一个数据处理脚本,随手把 MobaXterm 的窗口叉掉,第二天早上打开一看,进程没了,日志停在 3%——这种经历,做过服务器运维或者跑过训练任务的人,基本都遇到过至少一次。很多人第一反应是"服务器是不是被重启了",登录上去uptime一看,机器好好地跑了几十天,压根没重启,问题就出在你在 MobaXterm 里敲下的那条命令,是挂在那个终端窗口上的。窗口一关,终端消失,内核顺手把你的进程也带走了。

这篇东西就是围绕"关闭 MobaXterm 等连接 Linux 服务器的软件窗口,程序依旧在后台跑"这件事,把背后的信号机制、几种主流方案的取舍、以及我在实际使用中踩过的坑,一次性讲透。关键词里提到的 MobaXterm、Linux、服务器、后台运行程序,全都是围绕这个核心问题展开的。无论你是刚学会ssh登录的新手,还是已经管着几台云服务器的运维,这里面的内容都能直接抄去用。特别是那些跑长时间任务——模型训练、批量转码、数据清洗、爬虫、压测——的人,看完之后应该不会再出现"一关窗口就前功尽弃"的情况。

1. 断开连接后进程为什么会死:终端、会话与 SIGHUP 的三方关系

要解决"关窗口程序就死",先得搞清楚它为什么会死。很多人以为是自己操作有问题,其实是 Linux 进程模型的正常行为,只是这个行为在图形化客户端里被隐藏得很好,让人产生了错觉。

1.1 一次典型的"任务凭空消失"现场

我在早期做数据清洗的时候有过一次很典型的翻车。当时在服务器上跑一个用 Python 写的日志解析脚本,处理大概 40GB 的文本,预计要跑三四个小时。我本地是用 MobaXterm 连上去的,脚本跑起来之后屏幕上刷刷地滚日志,看着挺爽。结果笔记本没电自动关机了,MobaXterm 的连接断开,等我换台电脑重新登录,ps -ef | grep python里已经找不到那个进程,输出文件大小停在 1.2GB 不再增长。

当时的疑惑点是:服务器明明一直在运行,为什么进程会跟着我的笔记本一起"死"?答案在于,进程并不是绑在服务器上的,而是绑在那个 SSH 会话所对应的伪终端上的。你的笔记本只是连接的一端,但真正确立"归属关系"的,是服务器上那个pts/x伪终端设备。连接断开,伪终端关闭,内核就按照既定规则向挂在它上面的进程发信号,默认行为就是终止。

1.2 SIGHUP 才是真正的"凶手"

Linux 里进程之间靠信号通信,kill -l能列出一大堆。这里面和"断开连接"直接相关的,是编号为 1 的SIGHUP,名字来自早期的串口调制解调器时代——电话线挂断(hang up)时给进程发这个信号。到了今天,它的触发场景变成了:控制终端关闭时,内核向该终端的前台进程组发送 SIGHUP

这条规则有几个关键点容易被忽略:

  • 发给的是"前台进程组",不是单个进程。所以你在前台跑的命令,以及它 fork 出来的子进程(如果还在同一个进程组里),会一起收到。
  • 进程对这个信号的默认动作是终止,而且是那种不留遗言的终止,不会有清理逻辑,临时文件、锁、半写状态的数据都会原样留在磁盘上。
  • 如果一个进程忽略或者捕获了 SIGHUP,那么它就不会死。这就是所有"后台常驻"方案的技术基础。

顺带说一句,bash自己也会收到 SIGHUP。收到之后,它会把当前 shell 里的作业(jobs)再补一刀,向它们转发 SIGHUP。所以有些情况下,即使你用了&把进程放到后台,窗口一关照样死——因为进程只是"后台运行",并没有"免疫 SIGHUP"。

1.3 前台进程组、后台进程组与作业控制

在终端里敲sleep 100 &,输出的[1] 12345里的1是作业号,12345是进程号。这个进程被放进了一个新的后台进程组,但它仍然属于当前会话,控制终端还是那个pts/x

这里的区别很微妙,我一般用下面这张表来跟新人解释:

状态控制终端归属收到 SIGHUP 吗关窗口后
前台运行cmd当前 pts
后台运行cmd &当前 pts是(默认)
nohup cmd &脱离否(被忽略)
screen/tmux里的cmd虚拟终端否(在另一会话里)
systemd 管理的服务无终端活,且能自启

这张表基本概括了所有场景。看明白这张表,后面几节的内容你就能自己推导出七八成。

1.4 怎么判断一个程序"怕不怕断线"

不是所有程序都怕断线。有几种情况其实是可以放心关窗口的:

  • systemctlservice启动的系统服务,它们本来就不依赖终端,关窗口毫无影响。
  • crontab定时起的任务,跑的时候没有控制终端,也不受影响。
  • 已经用daemon()或者双重 fork 把自己变成守护进程的程序,比如 nginx、mysqld,天生就不绑终端。

反过来,只要你是在 SSH 命令行里手工敲上去的交互式命令,不管它是pythonjava -jarffmpeg还是rsync,默认都是怕断线的。判断方法很简单,跑起来之后执行:

ps -o pid,ppid,pgid,sid,tty,cmd -p <PID>

TTY这一列。如果是pts/0之类,说明它还挂在某个伪终端上,关掉那个终端它就有危险;如果是?,说明它已经没有控制终端了,随便关窗口都不影响。

2. nohup 加 & 到底做了什么:一次把参数讲清楚

nohup是解决这个问题最轻量的手段,几乎每台 Linux 上都自带,不需要额外装东西。但它的行为比大多数人想象的要"朴素"——它并不负责把进程变成守护进程,只做了有限的几件事。

2.1 nohup 实际只做了两件事

nohup的源码逻辑非常短,核心就两个动作:

  1. SIGHUP的处理方式设置为忽略(SIG_IGN)。这样终端关闭时内核发来的 SIGHUP 就被丢掉了。
  2. 检查标准输出是不是终端,如果是,就把 stdout 重定向到当前目录的nohup.out(如果当前目录不可写,就落到$HOME/nohup.out)。

注意第二个动作的关键词——"如果是终端"。也就是说,如果你自己已经用>重定向过 stdout,nohup就不会再插手。这也解释了一个常见困惑:为什么有时候会莫名其妙多出一个nohup.out文件,有时候又不会。

没有做这些事:没有 fork 出新会话、没有setsid、没有脱离控制终端、没有改变工作目录。所以严格来说,nohup之后的进程仍然属于原来的会话,只是它"脸皮厚",收到 SIGHUP 也不理而已。

2.2 输出重定向:必须自己接管 stdout 和 stderr

这是最容易出问题的一环。很多人写nohup python app.py &就完事了,运行一段时间后发现两件事:一是目录里冒出一个巨大的nohup.out,二是程序莫名其妙卡住不动。

卡住的原因在于:进程的 stdout/stderr 文件描述符仍然指向那个伪终端。终端关闭后,这个 fd 变成"写不出去"的状态。当程序的输出量大到把缓冲区填满时,写操作就会阻塞,进程就卡住了。表现就是"进程还在,但死活不动,CPU 占用为零"。

所以正确写法是把两个流都显式接管:

nohup python train.py > /data/logs/train.log 2>&1 &

拆解一下:

  • > /data/logs/train.log把 stdout 指向文件。
  • 2>&1把 stderr 也指向 stdout 当前所在的位置,也就是同一个文件。顺序不能反,写2>&1 > file是错的,那样 stderr 还是指向终端。
  • 结尾的&让命令进入后台,shell 立刻把提示符还给你。

更稳妥的做法是连 stdin 也一起处理掉,防止程序在某个地方等待输入:

nohup python train.py < /dev/null > /data/logs/train.log 2>&1 &

我习惯再加一步,把 PID 记下来,方便后续管理:

mkdir -p /data/run nohup python train.py < /dev/null > /data/logs/train.log 2>&1 & echo $! > /data/run/train.pid

$!是 shell 的一个特殊变量,表示"最后一个进入后台的进程的 PID"。有了这个 PID,后面要停任务就kill $(cat /data/run/train.pid),不用再去ps里翻。

2.3 已经跑起来了才想起要后台运行怎么办

现实里更常见的情况是:程序已经在前台跑着了,日志刷得飞快,你突然意识到待会儿可能要断线,这时候不想重启它。有两个补救手段。

第一个是暂停 + 转后台。用Ctrl+Z把前台进程挂起(注意不是终止),然后:

bg %1 disown -h %1

bg让挂起的作业在后台继续跑;disown -h把它从 shell 的作业表里摘掉,并且标记为不接收 SIGHUP。这样 shell 自己收到 SIGHUP 转发作业的时候,就会跳过它。不加-h的话是直接从作业表删除,但那样你就不能用%1这类作业号了,两者按需选。

第二个是事后重新接管。如果进程的 stdout 还指着终端,可以试试用reptyr把它"搬"到一个新的 tmux 会话里:

reptyr <PID>

这个工具在大多数发行版的仓库里都有,但需要内核ptrace权限配合,不少系统默认限制了yama/ptrace_scope,可能要临时调整。这东西属于应急手段,能用但不保证百分百成功,别把它当常规方案。

2.4 setsid:彻底脱离控制终端的另一种选择

如果你不想要nohup.out这种副作用,也不想被 shell 的作业控制牵连,可以用setsid

setsid python train.py < /dev/null > /data/logs/train.log 2>&1 &

setsid会调用setsid()系统调用,让目标进程成为一个新会话的首进程,同时脱离原有的控制终端。既然没有控制终端了,自然也就不会收到终端关闭引发的 SIGHUP。它和nohup的区别是:nohup是"收到信号也不理",setsid是"根本收不到信号"。两条路都走得通,setsid更干净一点,代价是有些老系统上的setsid行为略有差异,比如不加-f时可能不会 fork,导致 shell 的&行为和预期不一致。

我个人的习惯是:临时性、跑几个小时的任务用nohup加显式重定向;需要绝对干净、跑几天的用setsid;长期存在的用下面的 systemd 方案。

3. screen 与 tmux:把会话留在服务器上而不是客户端上

nohup解决了"进程不死",但没解决另一个需求:我想看到程序的实时输出,还想跟它交互。一个跑三个小时的交互式程序,你总不能一直tail -f日志,或者写个依赖input()的脚本然后发现它断了就卡住。这时候需要的是终端复用器。

3.1 思路的差别:脱离 vs 托管

nohup的思路是"让进程脱离终端",而screen/tmux的思路是"在服务器上造一个虚拟终端,把你的程序放在里面"。你从 MobaXterm 连过去,只是"看到"了这个虚拟终端的画面;断开连接,虚拟终端和里面的程序都还在服务器内存里活着,下次连上来再"接回去",看到的还是原来的画面,光标停在原来的位置。

这个差别很重要。它意味着用screen/tmux你不仅能保住进程,还能保住交互现场——比如一个正在运行的top、一个半交互的 REPL、一个等着你按回车确认的脚本。

3.2 screen 的最小可用命令集

screen出现得早,几乎所有发行版都有,甚至有些精简系统只带 screen 不带 tmux。它的常用操作就下面这几条:

screen -S work # 新建一个叫 work 的会话 # 在里面正常跑你的程序 # Ctrl+A 然后按 D -> detach,回到普通 shell screen -ls # 列出所有会话 screen -r work # 重新接入 work 会话 screen -d -r work # 如果它显示 Attached,先踢掉之前的连接再接入 screen -S work -X quit # 彻底杀掉这个会话(里面的程序也会被结束)

有个细节要注意:screen -r如果提示There is a screen on ... Attached,说明上一次的连接状态没被正确清理(比如网络断了但 TCP 没超时)。这时候用-d -r组合,先 detach 再 attach,一般都能解决。如果还是不行,screen -ls会给出 PID,可以kill掉那个 screen 进程,但那样里面的程序就没了,慎用。

还有一个新手常踩的坑:Ctrl+A是 screen 的转义键,在 screen 里想跳到行首是不能用Ctrl+A的,得按两次。这个习惯冲突让不少人第一次用 screen 时很抓狂,可以用-e参数换一个不常用的转义键,或者干脆上 tmux。

3.3 tmux 的会话、窗口、面板三层结构

tmux 的模型比 screen 清晰,分三层:

  • session(会话):最外层,一个 session 就是一个独立的环境,对应一次"项目"。
  • window(窗口):session 里的标签页,一个 session 可以有多个 window。
  • pane(面板):window 里的分屏,一个 window 可以切成多个 pane。

基本操作:

tmux new -s work # 新建会话 # Ctrl+B 然后 D # detach tmux ls # 列出会话 tmux attach -t work # 接入 tmux kill-session -t work # 干掉整个会话

窗口和面板的操作前缀都是Ctrl+B

快捷键作用
Ctrl+BC新建窗口
Ctrl+BN/P下一个 / 上一个窗口
Ctrl+B%左右分屏
Ctrl+B"上下分屏
Ctrl+B方向键在面板间切换
Ctrl+BDdetach 整个会话
Ctrl+B[进入复制模式,可上下翻页看历史输出

最后那个复制模式非常实用。跑长任务的输出动不动几千行,直接看屏幕只能看到最后几十行,进复制模式就能翻回去,q退出。我通常会把历史缓冲区调大,写进~/.tmux.conf

set -g history-limit 50000 set -g mouse on setw -g mode-keys vi

mouse on之后可以用鼠标滚轮翻页、拖动调整面板大小,体验会好很多。mode-keys vi让复制模式支持 vi 风格的移动键。改完配置文件执行tmux source-file ~/.tmux.conf生效,或者下次启动自动加载。

3.4 断线重连后的现场恢复

用 tmux 的一个典型流程是这样的:早上连上去tmux new -s etl,在里面跑一个数据同步脚本,看着它开始刷日志;中午要出门,按Ctrl+B D或者直接把 MobaXterm 关掉;下午换个网络环境重新连上服务器,tmux attach -t etl,画面立刻回到上午断开时的样子,脚本该跑到哪儿还跑到哪儿。

这里有个容易忽略的点:tmux 会话是跟着服务器走的,不是跟着用户走的。如果服务器重启,所有 tmux 会话都会消失。所以 tmux 保的是"断线不死",不保"重启不死"。要保重启,得看下一节的 systemd。

另外,多个用户同时 attach 同一个 tmux 会话是可以的,会看到同一块屏幕,这在结对排查问题时挺好用,但要注意两个人同时输入会互相干扰。

3.5 三种方案怎么选

我把这几节的方案整理成一张对照表,方便直接决策:

方案断线不死能交互服务器重启后自动拉起适合场景
nohup/setsid跑几小时的批处理、脚本
screen/tmux需要看输出、需要交互、调试期
systemd service长期常驻的服务、生产任务
crontab定时触发周期性任务

实际工作中经常是组合使用:开发调试阶段用 tmux,验证没问题之后固化成 systemd 服务。

4. 交给 systemd:真正让服务"活"下来的方式

如果你跑的东西不是"跑一次就完"的脚本,而是要长期存在、挂了要自动重启、最好开机能自动起来的服务,那nohup和 tmux 都不合适。它们要么没有重启能力,要么需要人工维护一个会话。这时候应该用 systemd。

4.1 为什么不推荐用 nohup 跑长期服务

nohup跑长期服务有几个绕不过去的问题:

  • 没有自动重启。进程 OOM 被内核干掉、段错误崩了,就真的没了,没人拉它起来。
  • 没有依赖管理。它需要在数据库起来之后再启动,nohup做不到,只能靠手写sleep
  • 资源限制不好设。想限制内存、CPU 亲和性,得靠ulimittaskset东拼西凑。
  • 状态不可观测ps能看到进程在不在,但看不到它启动了多久、重启过几次、上次为什么退出。

systemd 把这些问题一次性解决。它本来就是现代 Linux 的初始化系统,systemctl命令人人都会用,学习成本主要在于写单元文件,而单元文件的格式其实很简单。

4.2 一个可以直接抄的 service 单元

假设你有一个 Python 服务放在/opt/myapp,启动命令是/opt/myapp/venv/bin/python /opt/myapp/main.py,用哪个用户跑、工作目录在哪、环境变量怎么设,都可以在单元文件里写清楚。新建文件/etc/systemd/system/myapp.service

[Unit] Description=My background data service After=network-online.target Wants=network-online.target [Service] Type=simple User=appuser Group=appuser WorkingDirectory=/opt/myapp Environment=PYTHONUNBUFFERED=1 Environment=ENV=prod ExecStart=/opt/myapp/venv/bin/python /opt/myapp/main.py Restart=on-failure RestartSec=5 StandardOutput=journal StandardError=journal LimitNOFILE=65535 [Install] WantedBy=multi-user.target

几个字段值得单独说一下:

  • After=network-online.target配合Wants=表示等网络真正就绪之后再启动。只写After不写Wants是不够的,Wants才会把那个目标拉进来。
  • WorkingDirectory一定要显式写。systemd 启动服务时的工作目录默认是/,很多程序会在这个目录下找相对路径的配置文件,找不到就报错退出,这是最常见的"手动跑没问题,systemd 起不来"的原因。
  • Restart=on-failure表示非正常退出(退出码非零或被信号杀死)才重启;正常退出(退出码 0)不重启。如果是那种必须一直活着的服务,可以改成always
  • RestartSec=5防止崩溃循环把日志刷爆,加个间隔很必要。
  • Environment=PYTHONUNBUFFERED=1是 Python 特有的,让日志立即写出去而不是攒在缓冲区里,这个后面还会讲。

写完文件之后:

sudo systemctl daemon-reload # 让 systemd 重新读取单元文件 sudo systemctl enable myapp # 设置开机自启 sudo systemctl start myapp # 启动 sudo systemctl status myapp # 看状态

daemon-reload这一步千万别忘。改了单元文件不 reload,systemd 用的还是旧配置,你会怀疑人生。

4.3 日志走 journald 之后怎么看

单元文件里如果写了StandardOutput=journal,程序的输出会被 systemd 收走,交给 journald 管理。这时候nohup.out之类的文件就不需要了,查看用:

journalctl -u myapp -f # 实时跟踪 journalctl -u myapp --since today # 今天的所有日志 journalctl -u myapp -n 200 # 最近 200 行 journalctl -u myapp -p err # 只看 error 级别以上

journald 默认会做日志轮转,不会像nohup.out那样无限膨胀把磁盘塞满。不过要注意它的总量上限,默认一般是磁盘的 10%,可以在/etc/systemd/journald.conf里用SystemMaxUse调整。

如果你更习惯把日志写到文件,那就把StandardOutputStandardError改成append:/var/log/myapp.log,效果等同于自己重定向。两种方式各有好处:走 journald 方便按时间过滤和统一管理,走文件方便直接用tailgrepawk处理。

4.4 普通用户也能用 systemd

不是所有人都有 root。好消息是 systemd 支持用户级服务,systemctl --user就能操作,单元文件放在~/.config/systemd/user/下。用法和系统级几乎一样:

mkdir -p ~/.config/systemd/user # 写好 myapp.service 放进去 systemctl --user daemon-reload systemctl --user enable --now myapp systemctl --user status myapp journalctl --user -u myapp -f

这里有个坑:用户级服务默认在该用户的所有会话都退出后会被停掉。也就是说你 SSH 登出,服务就跟着停了。要让它一直活着,需要开 lingering:

loginctl enable-linger $USER

这条命令执行一次即可,之后即使你不登录,用户级服务也会保持运行,开机也会自动拉起。这是很多人在共享服务器上跑自己服务时的标准做法,避免去动系统目录。

5. 输出、日志和"我以为它在跑"的翻车现场

进程活着不等于任务在正常推进。这一节讲几个我在实际使用中遇到过、并且看到别人反复遇到的坑,基本都是围绕"输出"和"验证"这两件事。

5.1 日志被缓冲吃掉:明明在跑,日志却一片空白

最经典的场景:nohup python app.py > app.log 2>&1 &跑起来,过了半小时tail app.log,发现是空的,但进程确实在,CPU 也在跑。这时候的怀疑方向通常是"程序卡住了",其实大概率是输出缓冲

标准输出重定向到文件时,C 标准库默认使用全缓冲(通常是 4KB 或 8KB),攒够一块才写一次磁盘。程序如果只在开头打印了几行"启动中",那点量根本填不满缓冲区,你自然什么都看不到。

解决办法分语言:

# 通用:用 stdbuf 强制按行缓冲 stdbuf -oL -eL python app.py > app.log 2>&1 & # Python 专用:-u 参数关闭缓冲 python -u app.py > app.log 2>&1 & # Python 里也可以主动 flush print("start", flush=True)

stdbuf -oL对 stdout 设行缓冲,-eL对 stderr 设行缓冲,两个都要写。不过stdbuf依赖LD_PRELOAD机制,对静态链接的程序或者某些语言运行时可能无效,所以更保险的是程序自身的开关,比如 Java 可以用-Dfile.encoding之类配合日志框架配置,Node 一般默认就是行缓冲。

我在实际项目里养成的习惯是:不管有没有用,Python 脚本一律加-u。多打几个字符,省掉一次排查。systemd 那边对应的就是Environment=PYTHONUNBUFFERED=1

5.2 怎么确认进程真的在干活

ps看进程在不在只是第一层。要确认它在"干活",有几个更靠谱的手段:

# 看进程的 CPU 时间是否在增长,隔几秒看两次 ps -o pid,etime,time,pcpu,pmem,stat,cmd -p <PID> # 用 top 只看某个进程 top -p <PID> # 看它打开了哪些文件,日志写到哪去了 ls -l /proc/<PID>/fd # 看它的工作目录和环境变量 ls -l /proc/<PID>/cwd tr '\0' '\n' < /proc/<PID>/environ

ps输出里的TIME那一列是累计 CPU 时间,隔十几秒看两次,如果数值在涨,说明它在消耗 CPU;如果纹丝不动,可能是卡在等 IO 或者死锁了。STAT列如果是D,表示不可中断睡眠,通常卡在磁盘 IO;如果是S,是正常等待;如果是R,是在跑;如果是Z,那就是僵尸进程,需要查它的父进程。

/proc/<PID>/fd这个目录特别有用。我遇到过好几次"日志不知道写到哪去了"的情况,ls -l /proc/12345/fd/1一看,指向的是某个早就删掉的文件,程序还在往里写,磁盘空间被一个"看不见的文件"占着,df显示满但du找不到。这种情况需要重启进程才能释放。

5.3 磁盘被日志撑爆:一个凌晨三点的告警

nohup跑长期任务,最容易忽略的就是日志积累。一个日志刷得快的程序,一晚上写几十 GB 很正常。磁盘写满之后,连锁反应是:程序写日志失败可能直接崩溃,数据库写不进去,SSH 登录都可能因为无法写wtmp而失败,你就彻底登不上去了,只能走控制台。

这类问题有两层防护:

第一层是控制日志本身。如果程序自己能设日志级别和轮转就最好,不行的话就在日志文件层面做。用 logrotate 是标准做法,写一个/etc/logrotate.d/myapp

/var/log/myapp.log { daily rotate 7 compress delaycompress missingok notifempty copytruncate }

这里copytruncate是关键。默认的 logrotate 是"改名 + 通知程序重新打开文件",但你的程序不一定支持重开,用了copytruncate就是复制一份再清空原文件,对程序透明。代价是在复制的瞬间可能丢一点点日志,对于大多数场景可以接受。

第二层是监控。用df -h定期看一眼,或者写个简单的判断脚本挂到 crontab 里,超过 85% 就发个通知给自己。别等到登不上服务器才发现问题。

5.4 MobaXterm 侧的几个实用设置

回到客户端这一侧,MobaXterm 本身有几个设置和这个主题相关,顺手提一下:

  • 保存日志:在 session 设置里可以开启"保存终端输出到文件",这样即使你忘了重定向,客户端侧也留了一份记录。但要注意这只是客户端的记录,进程本身的 stdout 还是指向服务器上的终端的,不能替代服务端的重定向。
  • 保持连接:在 SSH 设置里有个 keepalive / 发送空包的选项,可以防止长时间无操作被网络设备断开。这只是减少掉线概率,不是让程序不死,两件事别混。
  • 断线自动重连:有些版本支持重连,重连之后你会看到一个新的 shell,之前的前台命令是回不来的,别指望它。

真正稳的做法仍然是:在服务器侧用前面说的方案把程序安顿好,客户端断不断都无所谓。

6. 几个真实场景下的组合拳与经验教训

前面讲的是单个技术点,这一节把几个真实场景串起来,顺带说几个容易想岔的地方。

6.1 关掉窗口不等于关掉服务器

这是新手最常见的误解。关掉 MobaXterm 窗口,关掉的只是一个远端的登录会话,服务器本身好好地在机房里(或者云上)跑着,其他用户的进程、系统服务、你之前用 systemd 起的服务,全都不受影响。真正会受影响的只有"挂在那个会话控制终端上的前台/后台进程组"。

理解这一点之后,很多行为就顺了:为什么同事的进程没被你的断线影响,为什么systemctl起的服务关窗口没事,为什么crontab里的任务从来不怕断线。它们共同的特点是没有把你的客户端终端作为控制终端

6.2 跳板机和多层 SSH 的坑

如果中间隔了跳板机(ssh -J或者手工两层登录),要注意一点:进程要安顿在最终执行它的那台机器上。很多人犯的错是在跳板机上敲了nohup,然后登录到目标机器执行命令,退出的时候只处理了跳板机那一层,结果目标机器上的进程还是挂在目标机器的 pts 上,照样会死。

多层场景下的排查方法是,在最终那台机器上确认进程的TTY列是不是?,或者直接tmux ls看会话在不在。别图省事在跳板机上操作。

还有一种情况是用了ssh -t强制分配终端,或者某些自动化工具默认分配了 pty,这时候nohup的效果可能被抵消,需要额外加-n让 ssh 不读 stdin。

6.3 定时任务和后台运行是两回事

crontabat这两个工具经常和后台运行混在一起谈,其实它们解决的是不同问题。crontab解决的是"什么时候执行",at解决的是"延迟到某个时间执行一次",它们共同的特点是执行时没有控制终端,所以天然不受断线影响。

但要注意 cron 的执行环境非常简陋:PATH可能只有/usr/bin:/bin,没有你~/.bashrc里设的环境变量,当前目录是用户家目录。所以 cron 里跑的脚本经常出现"手动跑得好好的,定时跑就报 command not found",原因就在这里。习惯做法是在脚本开头显式设置PATH和需要的环境变量,或者干脆用source加载一份自己的环境文件。

另外,cron 任务的输出默认会以邮件形式发给用户,如果系统没配邮件,这些输出就丢了。所以 cron 任务里最好也自己重定向输出,比如>> /var/log/myjob.log 2>&1

6.4 我平时的选择习惯

做了这么多年,我现在基本形成了一套固定的判断逻辑,说给你参考:

跑几十分钟到几个小时的脚本,直接用nohup ... > log 2>&1 &,把 PID 记到文件里,简单直接,出问题也好查。需要看实时输出就tail -f log

需要交互式操作、或者调试阶段、或者需要多个窗口并行观察的,开一个 tmux 会话,tmux new -s <项目名>,在里面干活。命名有讲究,用项目名而不是12,一周之后回来还能知道哪个是哪个。

确定要长期跑的服务,直接写 systemd 单元文件,enablestart,之后就当它是个系统服务来管。日志走 journald,需要持久化和过滤的时候再考虑写文件。

有一点我想特别强调:不要在 tmux 里跑你认为要长期存在的东西。tmux 会话会因为你误敲exit、误执行kill-session而消失,服务器重启也会全部清空。它适合"阶段性任务",不适合"常驻服务"。分清楚这个边界,能省掉很多次半夜爬起来重新拉服务的麻烦。

最后分享一个我用了很久的小习惯:给每个长任务单独建一个目录,比如/data/jobs/20250612-etl/,里面放run.pidrun.log、必要时放一份run.sh记录启动命令。三个月后回头查"当时那个任务是怎么起的",看目录就知道,比翻 shell 历史记录靠谱得多。

JOB=/data/jobs/20250612-etl mkdir -p "$JOB" cd /opt/myapp nohup ./venv/bin/python main.py < /dev/null > "$JOB/run.log" 2>&1 & echo $! > "$JOB/run.pid"

四条命令,一个习惯,之后所有"这个任务是谁起的、什么时候起的、日志在哪"的问题都自动有了答案。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询