☰
Linux后台运行不挂断:从nohup到systemd的完整指南
2026/10/22 14:09:20 网站建设 项目流程

经常有朋友跑过来问我:明明在服务器上用command &把任务放后台了,为什么一关 SSH 窗口,任务就跟着一起没了?还有人更冤枉,用了nohup command &,结果第二天一看进程还是没了。其实“Linux 后台运行不挂断”这件事,说大不大,说小不小,背后牵扯到信号、会话、进程组、作业控制还有 systemd 这一整套东西。搞懂了原理,你就算闭着眼也能选出合适的方案,而不是每次都靠复制粘贴碰运气。

这篇文章我用做运维和开发这么多年攒下的经验,把一个长任务在终端断开后继续运行的几种做法讲清楚:最基础的nohup + &、稍微进阶的setsid和disown、更符合日常习惯的tmux/screen,以及生产环境里最该用的systemd。不管你是刚摸 Linux 的新人,还是已经踩过坑的老手,都能从中找到对应你场景的那一招,而且看完还能明白“为什么它不挂断了”。

1. 先搞明白:进程到底为什么会被“挂断”

1.1 绕不开的信号:SIGHUP

你在终端里跑命令,终端窗口一关,命令就被杀,这在我们行话里叫“挂断”。背后其实是操作系统给那个进程发了一个信号,叫 SIGHUP,也就是 hang up 的缩写。这个信号很古老,历史可以追溯到电话拨号时代,意思是“电话线路断了,你别再等对方说话了”。

SIGHUP 的默认行为就是终止进程。你可以把它理解成:老板在工位上喊你去开会,你人没到,他就默认你离职了。多数进程没有为这个信号准备“会议室回应”,所以默认就被干掉。但 SIGHUP 是可以被忽略的,nohup干的事就是启动程序时告诉系统:我忽略这个挂断信号。这样即使终端断了,HUP 发过来,进程也会当没听见,继续干自己的活。

在 Linux 里可以用kill -l查信号列表,SIGHUP 排第一,编号是 1。常见被误用的还有 SIGTERM(编号 15)和 SIGKILL(编号 9),它们和 HUP 不是一回事。很多人以为加了nohup就刀枪不入,其实它只挡住了 HUP 这一种信号,别人拿 TERM 或者 KILL 来杀,它照样会死。

1.2 会话、进程组与控制终端的关系

要真正理解“不挂断”,光知道信号还不够,还得搞清楚会话(session)、进程组(process group)和控制终端(controlling terminal)这三者的关系。

你可以把一个登录会话想象成一场线上会议:你的 SSH 窗口是会议室里的白板,bash(或者别的 shell)是主持人,你在里面执行的任务是参会者。主持人走了,会议就散了;操作系统为了让“会议室”里的东西不要到处乱跑,在主持人退出时,会给所有还挂在白板相关流程里的参会者广播一个“散会”信号,也就是 HUP。进程组就是一拨一起干活的参会者,同一组里的人往往被一锅端。

具体到系统里,你每次通过 SSH 登录,系统都会创建一个会话。会话首进程通常就是你那个登录 shell。在这个 shell 里启动的命令,大部分会成为 shell 的子进程,大家共享同一个控制终端。控制终端一旦关闭,内核或 sshd 就会向会话首进程发送 HUP,shell 退出前还会把它作业表里的后台任务也一并关照一遍。

什么情况下会收到 HUP?最经典的就是 SSH 连接断开。你关掉笔记本电脑盖子、网络断开、或者ssh客户端被强制退出,远端 sshd 检测到连接断开后,会通知 shell 会话清理,于是 SIGHUP 就像多米诺骨牌一样传到你的任务进程上。

1.3 为什么“加上 & 还不够”

新手常犯的错是:在命令后面加一个&,以为这就万事大吉了。&确实把命令放到了后台,让你可以继续输入其他命令,但它只代表“这个任务不用等我完成,请放到后台作业表里”,并不能让进程忽略 HUP。

来做个最简单的验证。你 SSH 到一台机器上,执行:

sleep 300 &

然后直接关掉 SSH,重新登录后再看:

ps -ef | grep sleep

大概率什么都查不到。原因就是这个sleep还挂在 shell 的作业表里,终端断开时 shell 收到 HUP,它也会顺着把 HUP 传给这个后台任务。换句话说,&解决的是“前台占着终端”的问题,不解决“终端断开后被清场”的问题。要想让它不挂,必须组合别的工具,或者让任务彻底脱离这个会话体系。这就要进入正题了。

2. 入门方案:nohup + & 的标准用法与常见误区

2.1 标准写法为什么长这样

最广为人知的命令组合是nohup 命令 &。但很多人只写到这一步,结果日志满天飞,进程也不好管。规范一点的写法是这样:

nohup ./your-script.sh > /var/log/your-app.log 2>&1 &

拆开逐个看:nohup让进程忽略 SIGHUP;&把任务放到后台;> /var/log/your-app.log把标准输出重定向到指定文件;2>&1把标准错误也合并到同一个输出通道。后两个加起来的作用是让程序的正常日志和报错信息都落到一个可控的文件里,方便事后排查。

如果省略重定向,nohup默认会在当前目录生成一个nohup.out,所有输出都往里丢。这个文件其实挺坑的,一是文件位置取决于你启动命令时的目录,二是一旦任务疯狂打日志,这个文件可能膨胀到几个 G,直接把磁盘塞满。所以只要你不是临时跑一条一次性命令,我都建议老老实实写清重定向路径。

启动后还有个容易被忽略的动作:把进程 PID 记下来。

echo $!

$!在 bash 里代表最近一次放到后台的进程 ID。把它写进文件,后面不管是用kill还是排查问题,都有据可查。不然你ps -ef半天,可能都找不出哪个进程是你刚启动的那个,特别是当你跑了多个同类脚本时。

2.2 日志和进程管理的一些实操经验

日志管理是所有长驻任务的必修课。我的习惯是统一建一个目录放后台任务日志,比如/var/log/apps/,每个任务一个单独文件,文件名包含任务名和日期。这样排查问题的时候不可能搞混,而且能很方便地写 logrotate 规则,避免磁盘被日志塞满。

logrotate 的配置不算复杂,一段示例就够用:

/var/log/apps/your-app.log { daily rotate 14 compress missingok notifempty copytruncate }

放到/etc/logrotate.d/下即可。copytruncate很关键,它先复制一份日志再清空原文件,不需要重启进程,适合那些自己都没做日志轮转的普通脚本。

判断进程是否活着,不要靠肉眼ps扫,可以直接用:

kill -0 <PID>

这个命令不真发信号,只探测进程是否存在。返回 0 说明进程还在,返回非 0 就是已经不在了。配合一个简单的 shell 判断,就能写一段监控脚本,实现进程死了自动发提醒,这也是后面讲 systemd 之前的一个轻量临时方案。

2.3 注意!nohup 不是万能保命符

先说最常见的误解。nohup只管 SIGHUP,对 SIGTERM、SIGKILL 完全无能为力。你如果执行kill <PID>(默认发 TERM),或者服务器内存不够触发 OOM killer 直接发 KILL,进程照样死。它也不是守护进程,不负责崩溃后的重启。任务自己写崩了、异常退出了,nohup不会把它拉起来。

更坑的是,它没法“补丁”到已经运行的任务上。假设你用python run.py在前台跑了一个任务,跑到一半发现要跑不完了,但你现在必须关终端走人。这时候你想“那我套个 nohup 吧”,是行不通的。nohup只能在进程启动那一刻影响它,对已经在运行的进程没法逆向生效。这个场景下面有别的解法,用disown可以兜底。

还有一点容易被忽略:某些程序需要从标准输入读内容,或者检测到没有终端会直接拒绝启动。你把它放后台 + nohup,程序反而会立刻报错或者卡住。典型例子是会交互式确认的安装脚本,比如sh install.sh时不时问一下 yes/no,放进后台后没人回答,它就一直卡在读写上。这种交互式任务不适合用 nohup 硬挂。

3. 进阶玩法:setsid、disown 与真正的“脱离终端”

3.1 setsid:换个新会话,彻底切断关系

如果nohup是“忽略挂断信号”,那setsid就是“换个新会议室”。setsid会调用setsid(2)系统调用,让目标程序开启一个全新的会话。新的会话没有控制终端,跟你原来的 SSH 窗口、原来的 shell 不再是一家人。这时候不管原来的终端怎么断开,HUP 信号根本找不到它头上,因为它已经不属于那个会话了。

用法简单粗暴:

setsid ./long-run.sh > /var/log/long-run.log 2>&1 < /dev/null

注意我把&都省了,因为setsid启动的新会话进程本来就不跟当前终端抢终端,加不加&影响不大。如果你加了&,注意它返回的 PID 和你ps里看到的进程 PID 可能不一致,因为setsid先 fork 了一次,然后子进程变成新会话首进程,父进程退出。实战里建议用pgrep -f long-run.sh来定位进程。

有一个真实案例。之前某项目跑一个内网穿透客户端,我最初用nohup ... &启动,每次网络抖动一次,SSH 断开重连,这个穿透进程就会收到 HUP,导致内网服务中断。后来改成setsid,彻底切断与控制终端的关联,从此再没因为这个原因掉过线。那种只需要安静在后台干活、不需要人再回去操作的任务,setsid特别合适。

3.2 disown:把任务踢出作业表

disown是 bash 内建命令,它的使用场景正好补上nohup的短板:任务已经在跑了,而且在前台,没法重新用 nohup 启动,但你现在必须断开会话。这是应急场景里的好帮手。

具体操作分三步。假设你正在前台跑python run.py,突然要断开:

  1. 按Ctrl+Z暂停任务。这会向进程发送 SIGTSTP,进程挂起,像按了暂停键。
  2. 执行bg,把暂停的任务放到后台继续跑。
  3. 执行disown,把它从当前 shell 的作业表里踢出去。这样 shell 退出时就不会再“惦记”它,也就不会向它发 HUP。

如果你不想把它完全踢出作业表,只想让它忽略 HUP,可以用disown -h %1,%1是作业号,先用jobs查看。

举个例子:

sleep 1000 & jobs -l disown %1

之后不管你怎么退 shell,这个sleep都跟你无关了。我常用disown的场景是:某次临时写的同步脚本已经跑了一半,正好有急事要关电脑,可它还要再跑十分钟。这种按“临时计划”启动的任务,没必要为此上 tmux 或者 systemd,一个disown就干净利落地解决了。

3.3 多次 fork 与孤儿进程收养

看进程状态的时候,你可能会注意到一个奇怪现象:用 nohup 启动的进程,父进程 PID 会变成 1。这个 1 号进程就是系统里的 init 或 systemd,专门负责收养那些“爹没了”的孤儿进程。

父进程(你原来的 bash 或 SSH 会话)退出后,子进程并不会自动变成孤儿,只有父进程先退出,子进程才会被系统重新挂到 1 号进程下面。这个机制保证了即使原终端的整个进程树都解散了,那个后台任务还能作为孤儿进程继续活下去。

传统编写守护进程的程序,套路就是“第一次 fork + setsid + 第二次 fork”。第一次 fork 是为了让 setsid 成功(因为只有当调用者不是进程组组长时,才能创建新会话),setsid 让进程成为新会话首进程并脱离控制终端;第二次 fork 是再制造一层隔离,防止进程之后重新获取控制终端。这个双 fork 的渊源,你现在再回头看就能理解为什么很多人坚持“正式服务不能用 nohup 凑合”,因为守护进程的标准做法从一开始就要求彻底脱离会话体系,而不是靠信号忽略。

4. 实用主义:screen / tmux 比 nohup 更舒服的长驻方案

4.1 为什么我最终习惯用了 tmux

nohup 和 setsid 解决的问题是“跑完就算”,但有一类长任务的痛点不是“挂不住”,而是“我还想回去看看现场”。比如你在跑一个需要手动干预的数据清洗脚本,中间要看进度、改参数;或者你在服务器上远程编译某个项目,一边等一边刷别的终端;再或者你临时开了一个交互式服务,想给别人一个能接入的空间。这些场景里,nohup感觉很别扭,因为你只能靠日志判断进度,不能回到原来的交互界面。

tmux 是一个终端复用器,它能维护一个独立于 SSH 会话的“后台房间”。你在这个房间里跑任何命令,哪怕 SSH 断开、重新登录,房间和里面的进程都还在。回头再连上,tmux attach一下就回到了那个房间,屏幕内容、会话状态一样不少。它最大的意义不是“不挂断”,而是“可恢复”。从使用体验上讲,tmux 是真正解决日常百分之八十长任务问题最舒服的方案。

4.2 最小上手手册

如果服务器是新环境,先安装:

# Debian/Ubuntu 系 apt install tmux # RHEL/CentOS 系 yum install tmux

然后创建一个会话:

tmux new -s work

这个命令会进入 tmux 的界面,看起来就是多了一个终端窗口。你在这个窗口里正常执行你的长任务,比如:

python train.py

之后需要临时离开,按Ctrl+b,然后松开再按d,就能从 tmux 会话里分离出来,回到普通 shell。SSH 连接断开也没事。

重新登录服务器后,用tmux ls查看有哪些会话,然后:

tmux attach -t work

就回到了刚才的窗口。你会看到python train.py的输出还挂在眼前,甚至中途输出的几百行内容都还在。这个体验是用 nohup 完全给不了的。

我再补充几个高频操作,都很短:Ctrl+b c新建窗口;Ctrl+b n/Ctrl+b p切换窗口;Ctrl+b %左右分屏;Ctrl+b(双引号)上下分屏。这些足够日常用了,不需要一上来背一整套手册。

4.3 screen 用户迁移要点

很多老运维习惯用 screen,因为它是更早出现的终端复用器,CentOS 老版本里默认就有,tmux 当时还得额外装。如果你接手的老机器上只有 screen,起码要知道这几个命令:

screen -S name # 创建会话 Ctrl+a d # 分离(注意不是 Ctrl+b) screen -r name # 重连 screen -ls # 列出会话

从 screen 迁到 tmux 最需要适应的就是前缀键,screen 是Ctrl+a,tmux 是Ctrl+b。手指肌肉记忆改起来需要一两天,其他概念大同小异。如今新部署的环境我都优先装 tmux,因为它的分屏能力、快捷键设计和脚本化都比 screen 舒服不少。机器上实在没权限装 tmux,才退而使用系统自带的 screen。

5. 生产环境正解:用 systemd 管理长驻任务

5.1 一个 service unit 长什么样

严格来说,nohup和 tmux 都不是生产环境里跑服务的正规军。正规军是 systemd,因为任何需要开机自启、崩溃自动拉起、统一日志管理的服务,都应该写成一个 systemd service。systemd 是进程 1,你的服务挂在它下面,跟登录终端彻底无关,自然不存在“SSH 断了服务就没了”的问题。

先看一个最基础的 unit 文件,放在/etc/systemd/system/my-task.service:

[Unit] Description=My long running task After=network.target [Service] Type=simple User=app WorkingDirectory=/opt/app Environment=PATH=/usr/local/bin:/usr/bin:/bin Environment=APP_ENV=prod ExecStart=/usr/bin/python3 /opt/app/run.py Restart=always RestartSec=5 StandardOutput=journal StandardError=journal SyslogIdentifier=my-task [Install] WantedBy=multi-user.target

关键点逐个说。Type=simple意思是 ExecStart 启动的进程本身就是要守护的服务进程,systemd 会直接跟踪它。如果你的程序是自己先 fork 成守护进程再退出(比如某些老派服务),就需要用Type=forking,还得配PIDFile=告诉 systemd 去哪儿找主进程 PID,这个容易踩坑,新写的服务建议统一用Type=simple。

Restart=always表示进程无论因为什么原因退出,systemd 都会把它重新拉起来。RestartSec=5是重启间隔,防止进程秒退导致 systemd 疯狂重启,那会变成一种变相拒绝服务。日志配置成journal后,统一收进 journald,比各写各的 log 文件好管得多。

写好配置后执行:

systemctl daemon-reload systemctl start my-task systemctl status my-task

status输出里能看到进程状态、主进程 PID、最近日志,排错信息直接就在里面。

5.2 开机自启、崩溃重启与日志

要让服务开机自动启动,一条命令:

systemctl enable --now my-task

enable是注册开机自启,--now是立即启动。这样系统一启动,服务就自己跑起来了,不需要你手动 SSH 上去敲命令。这是 nohup 永远做不到的。

崩溃重启这块我再强调一下。Restart=always和Restart=on-failure是有区别的。前者只要进程退出就重启,哪怕你是自己stop让它正常退出的,systemd 也会尝试拉起来(所以想停服务必须systemctl stop);后者只在非正常退出(比如崩溃、被杀)时才重启,手动 stop 不会拉起来。生产脚本我们通常用on-failure,配合RestartSec防止疯转。

查日志也别再去翻文件了:

journalctl -u my-task -f

-u指定服务名,-f是跟随最新输出。也能查时间范围:

journalctl -u my-task --since "10 minutes ago"

这比打开一个几个 G 的 nohup.out 省心太多。

5.3 哪些场景用哪个方案

我把自己的选型经验总结成下面这张表,方便你对号入座:

场景推荐方案理由
临时跑一条长命令,跑了就完事nohup + &简单直接,看日志用重定向
跑到中途才发现要先断一下,之后再回来Ctrl+Z+bg+disown不中断任务,摆脱 shell 控制
任务需要持续交互、随时看进度、断线重连tmux可恢复会话,体验最接近“保留现场”
纯后台静默任务,不需要再交互setsid彻底脱离会话,不想看到任何终端关联
生产服务、开机自启、崩溃自动拉起systemd生命周期管理、日志归集、资源统计都是正规军配置
老机器上没 tmux、只能简单挂任务screen老牌工具,兼容性好

这套选型梯度,基本能覆盖我从日常开发到生产运维遇到的所有“后台运行不挂断”需求。

6. 常见问题与排查实录

6.1 明明用了 nohup,进程还是没了

这个问法几乎每周都会遇到。先别急着怀疑 nohup 没用,按下面顺序查一圈:

第一,看看日志。如果是任务自己抛异常退出的,nohup 管不着。举例来说,某个脚本依赖网络,断网后程序直接退出,此时你再怎么 nohup 都没用。nohup 只保证“终端断开时进程不被信号杀死”,不保证“程序本身不退出”。

第二,看看是不是其他信号杀的。你或者服务器的监控脚本可能给它发了 SIGTERM,或者系统内存不足触发了 OOM killer。OOM 可以从 dmesg 看到线索:

dmesg | grep -i "killed process"

第三,确认 nohup 是否真的生效。可以这样看进程对哪些信号做了忽略:

cat /proc/<PID>/status | grep -i sigign

SigIgn 那一行是一串十六进制数。如果 SIGHUP 对应的 bit(bit 1)是 1,就说明进程确实忽略来自终端的挂断信号。想更直观一点,也可以直接看进程和终端的关系:

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

正常情况下,长时间后台任务的 TTY 会变成?,表示它已经不属于任何终端;SSID 也会和原来的 shell 不同。如果 TTY 还是pts/0之类的,说明它还赖在原来会话里,那终端一断,它不死才怪。

6.2 后台进程在但日志没更新的怪事

这是另一类很迷惑的现象:进程明明还活着,ps能看到,但日志文件就是半天不更新。你觉得程序卡住了,其实它可能一直在干活,只是输出被缓冲住了。

默认情况下,程序如果检测到自己输出到的是文件(而不是终端),C 运行库会启用块缓冲,攒够 4K 或者 8K 才写一次。比如 Python 里频繁print,重定向到日志文件后,可能积累很久才 flush 一次。

解决的办法有几个,都很实用。Python 脚本可以在运行时直接关掉输出缓冲:

python3 -u run.py > run.log 2>&1 &

或者用stdbuf强制行缓冲:

stdbuf -oL -eL ./run.sh > run.log 2>&1 &

-oL表示标准输出按行缓冲,看到一行写一行;-eL对标准错误同理。如果你写的是自己的代码,更推荐在代码里显式flush,这样整个生命周期都可控。这个“进程活着但日志没更新”的现象特别容易让人误判成 nohup 没用,实际上是输出缓冲扛的锅。

6.3 我的排查清单

最后把我每次排查“后台任务为什么又没了”时的动作整理成清单,照着做基本能定位九成问题:

  1. 先看systemctl status或ps -ef | grep 任务名,确认进程到底死没死,还是说只是日志不更新。
  2. 看进程的 PID、PPID、TTY,判断它是不是真的脱离了会话。TTY 不是?就说明还没断干净。
  3. 看日志文件和journalctl,区分是程序自身异常退出,还是被信号杀死。
  4. 查dmesg里有没有 OOM 记录,排除内存被系统回收的情况。
  5. 检查磁盘空间,df -h看一眼。日志写满磁盘导致程序写不了而退出,是超级常见又超级隐蔽的挂掉原因。
  6. 别急着kill -9。先kill -TERM,给程序清理机会,实在没反应再放大招。

我自己这些年踩坑总结下来一条原则:能用 systemd 管的就绝不用裸 nohup,需要重连操作的就上 tmux,临时任务才用 nohup/setsid 去土法炼钢。按这个思路选,后台任务“不挂断”就不再是玄学,而是一套清清楚楚的方案组合。最后再分享一个小技巧,写脚本时我总会在开头加一句set -u,很多后台任务跑着跑着消失,其实就栽在某个未定义变量上——这种问题,再好的不挂断工具都救不回来。

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

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

立即咨询