1. 这个问题是怎么逼疯我的我印象很深那年在一台云服务器上跑一个数据同步脚本日志一页一页往上翻眼看就要跑完了。我看时间差不多了顺手把终端窗口一关回办公室泡了杯茶。结果回来一看脚本才执行到一半就停了那批数据要重新跑一遍白白浪费了几个小时。从那天起我就把“Linux 后台运行不挂断”这件事当成必修课来对待。后来不管是部署服务、跑爬虫、执行批量任务还是给同事搭环境我都会先确认这个进程到底能不能脱离终端活下去。因为在实际环境中SSH 断开、终端异常退出、网络闪断这些都是家常便饭而你的程序不会自动拥有“离开终端继续运行”的能力。这篇文章我会把这些年用过的方案完整梳理一遍从最基础的nohup、到setsid、disown再到tmux、screen这类终端复用工具最后是生产环境最常用的systemd服务。每种方法适合什么场景、有什么坑、怎么排查我都会用自己的实际经验讲清楚。1.1 先搞清楚为什么终端一关进程就跟着死这个问题看着简单但很多人从没想过底层原因。你在终端里敲下一条命令比如python app.py这个进程其实和终端建立了一套“会话”关系。终端关闭时系统会向这个会话里的进程发送挂断信号也就是SIGHUP。大部分程序默认收到这个信号就直接退出来不及保存任何状态。我用个生活化的类比来解释终端就像是遥控器进程就像是电视。遥控器一关电视就跟着断电。即便很多程序本身不愿意死但它们没有处理SIGHUP的能力或者根本没有这个意识结果就是终端窗口一消失进程也随之终结。这里有一个很关键的概念叫会话首进程。你在终端启动的第一个进程会被当作这个会话的首进程而整个终端会话内的进程组都归它管。终端关闭时内核会通知会话首进程会话里的进程组也会收到对应的信号。这也是为什么有些你以为“在后台跑了”的任务照样会在关终端时被中断——你只用了但进程并没有脱离会话。除了挂断信号终端关闭还会带来另一个问题进程的输出无处可去。你的程序一直在往终端上打印日志但终端已经不存在了如果你没有把输出重定向到文件程序在写入时就会报错或者直接被系统强制终止。这也是为什么单纯加一个并不够你还得同时处理输出重定向。理解了这层机制后面所有方案的核心思路就都清楚了无非就是做两件事让进程不再依赖终端的会话以及给进程的输出寻找一个稳定的去处。2. nohup 加 最经典的基础组合2.1 标准写法和参数详解nohup几乎成了“后台运行不挂断”的代名词它做的事情非常简单忽略SIGHUP信号。也就是说当终端关闭时你的进程并不会收到挂断通知自然就继续跑下去了。不过在实际使用中我强烈建议你带上输出重定向而不是直接敲nohup command /dev/null 21 。先看一个我常用的完整写法nohup python app.py /tmp/app.log 21 拆解一下这条命令的每个部分nohup忽略挂断信号进程不随终端退出而终止。python app.py你要运行的命令可以是脚本、服务、同步任务没有任何限制。 /tmp/app.log把标准输出重定向到日志文件。21把标准错误重定向到同一个文件。这个必须写否则程序报错时你不会看到任何信息。让命令在后台执行终端马上回到可输入状态。很多人会问到底起什么作用其实本身和“关闭终端能不能活下来”没有直接关系。它只是让你把进程放到后台终端不被命令占着。真正保命的是nohup。我见过不少新手只用了然后关掉终端回来发现进程死了一脸疑惑这就是没搞清两者的分工。还有一点容易踩坑如果你不写重定向nohup默认会把输出写入当前目录下的nohup.out文件。听起来很方便对吧但如果程序运行时间长、日志量大这个文件会无限增长直到把磁盘写满。我之前排查过一台服务器磁盘告警最后发现罪魁祸首就是几个月前的nohup.out已经膨胀到了几十个 GB。所以我的习惯是永远不用默认的nohup.out而是显式指定日志文件并且按日期命名方便后续清理。2.2 nohup 的隐藏坑位nohup虽然基础但也不是银弹。第一个问题就是它不提供“交互能力”。你想看看程序内当前进度只能去读日志文件你想中途给程序发个指令比如暂停或者切换参数nohup做不到。这其实意味着它适合“跑完就算完”的任务不适合需要持续管理、经常查看状态的任务。第二个问题是nohup不解决“孤儿孤儿”问题。所谓孤儿进程指父进程已经退出子进程变成被系统收养的进程。nohup本身不会刻意处理这一层它只关心信号和输出。如果你想做的更干净一些就得用setsid这个我后面会专门讲。第三个问题更隐蔽当你关闭终端时你的 shell 环境变量会随之消失。如果你的程序依赖某些自定义变量比如export MY_CONFIG_ENVdev而你又是在登录时手动 export 的那么nohup跑的进程启动时复制的是当时的环境一旦终端关闭它不会自动重新加载那些配置。我曾经遇到过一个部署脚本在终端里手动 export 了数据库连接串然后用nohup启动服务一切正常。但第二天重启服务时报错说找不到配置变量就是因为没有把环境变量固化到脚本或配置文件里。所以用nohup的黄金法则是把程序需要的一切做成显式参数或配置文件不要依赖终端里手动设置的环境变量。2.3 怎么确认和停止 nohup 进程启动之后怎么知道进程还活着我最常用的检查方式ps aux | grep app.py或者用更精确的方式用pgreppgrep -af python看输出里有没有你的命令路径、启动时间等就能判断进程是否还在运行。还有一种是查看日志文件的更新时间如果日志一直在更新说明程序在正常跑。要停止一个后台进程先通过ps找到它的进程 ID就是 PID然后kill PID如果程序不响应普通kill信号可以用kill -9 PID-9是强杀信号程序没有机会做清理工作可能留下临时文件或者锁所以一般先试普通kill确实杀不死再上-9。这里我多说一句很多程序会捕获SIGTERM做优雅退出比如保存状态、关闭连接。你直接kill其实是在给它机会体面收尾动辄用-9反而容易引入数据不一致的问题。3. 比 nohup 更干净的方案setsid 与 disown3.1 setsid彻底脱离会话nohup是在“管住挂断信号”这个层面解决问题但进程本身还没有完全脱离会话的管辖范围。如果我们想让进程彻底独立不再和任何终端会话扯上关系就要用到setsid。这个名字直译过来就是“设置会话 ID”它让新进程成为一个新会话的首进程彻底脱离原来的控制终端。setsid的用法特别简单setsid python app.py /tmp/app.log 21 /dev/null 注意我加了一个 /dev/null。为什么要这样做因为进程虽然脱离了会话但它的标准输入还挂着原来的终端。如果程序在运行中尝试从标准输入读取内容比如某些交互式脚本就会读到已经失效的终端描述符引发不可预知的错误。把标准输入重定向到/dev/null相当于告诉程序这里没有任何输入通道。setsid和nohup的区别其实很明显。nohup是“忽略信号”进程依然被记录在会话里只是对挂断信号免疫了。setsid是“重新开一家新公司”进程直接自立门户不再依赖原来的终端会话。如果你手头有比较重要的长任务我建议用setsid它的隔离更彻底。3.2 disown从 shell 的作业表里除名如果你已经启动了一个前台任务中途意识到它要在终端关闭后继续跑这时候怎么办重新用nohup跑一遍不一定非要这样。在 bash 里有一个命令叫disown它能把任务从 shell 的作业表里移除。移除之后shell 在退出时就不会给这个任务发送挂断信号了。具体操作是这样的先正常启动任务比如python app.py然后按Ctrl Z把任务挂起接着用jobs查看作业编号jobs -l你会看到类似[1] 12345 Running python app.py这样的输出然后bg %1 disown %1bg让任务在后台继续运行disown把它从作业表移除。经此操作即便你直接关掉终端任务也不会收到挂断信号。这个方法适合那种“忘了提前加 nohup”的紧急补救场景形式上比重新启动一个进程更优雅。有一点要注意disown只对当前 shell 的后台任务有效。如果你用的是zsh这些命令也基本兼容但如果你用的是sh或者dash可能没有disown这个内建功能需要setsid来实现类似效果。我把这三种方式放在一起对比场景差别一下就看出来了方法核心原理适合场景局限性nohup忽略 SIGHUP 信号跑完即走、无需交互的短任务输出需手动重定向无法交互setsid创建新会话脱离原终端需要彻底隔离的长任务命令较冷门新手不熟悉disown从 shell 作业表移除补救已启动的任务只针对当前 shell 有效4. tmux 与 screen能查看、能接管、能多窗口4.1 为什么需要终端复用工具有了nohup和setsid进程可以在后台跑但如果我想实时看看进度、批量跑不同任务、或者过几天再回来接着看之前的工作现场单靠日志文件和kill是没法搞定的。这时候就需要终端复用器它像一个常驻的“虚拟屏幕”你的进程跑在虚拟终端里和实际 SSH 会话完全解绑。即使你的 SSH 断开虚拟终端还在后台运行你只要重新连接回去发现进程还在还能看到之前输出的内容在一个虚拟终端里随意切换窗口。我自己的习惯是凡是运行时间超过几小时、需要观察进度、甚至可能中途要人工介入的任务统统丢进终端复用器。写代码时启动的开发服务器、数据处理任务、需要多步骤执行的操作流程用终端复用器管理起来非常顺手。4.2 tmux 核心操作三板斧在tmux和screen之间我强烈推荐tmux。它虽然上手门槛稍微高一点但功能丰富度和维护活跃度都比screen好得多。先看如何安装通常是apt install tmux # Debian/Ubuntu 系 yum install tmux # CentOS/RHEL 系启动一个会话并给它起个名字tmux new -s myproject在这个会话里正常跑你的程序比如python app.py然后关键来了按Ctrl b再按d退出会话但会话本身还在后台运行。这个操作叫 “detach”中文叫脱离。你的 SSH 断掉、终端关掉都不影响这个会话。等你回来重新连接服务器用tmux attach -t myproject就重新进入了那个虚拟终端一切都在日志、程序输出、命令行历史全都原封不动。如果开了多个窗口可以在会话里按Ctrl b后按c新建窗口按Ctrl b后按数字键切换窗口比如Ctrl b 0切到第 0 号窗口。这些快捷键不复杂但刚上手容易忘我在下面列一张速查表操作快捷键或命令说明新建会话tmux new -s 名称推荐总是给会话命名脱离会话Ctrl b然后d会话继续后台运行重新进入tmux attach -t 名称恢复现场新建窗口Ctrl b然后c开多个并行任务切换窗口Ctrl b然后对应数字在窗口之间跳跃关闭窗口Ctrl b然后x会提示确认比较安全查看所有会话tmux ls列出还在跑的会话4.3 screen 的临时替代用法虽然我主推tmux但有些老机器上可能只预装了screen新装软件又需要权限这时候screen也能顶上。核心用法跟tmux类似screen -S mysession python app.py脱离屏幕的快捷键是Ctrl a然后d重新进入用screen -r mysession列出所有 screen 会话用screen -ls。screen的窗口管理和tmux差不多但整体交互体验和配置灵活性略逊一筹。我的建议是个人主力用tmux但把screen的基本命令也记住以防在环境受限的机器上被迫使用。4.4 实战经验tmux 配合日志文件双保险用tmux管理进程时我仍然会同时设置日志输出。举个例子我在一个 tmux 会话里启动爬虫python spider.py spider_$(date %F).log 21这么做的原因是tmux 的虚拟终端虽然能保存屏幕输出但屏幕缓冲区有限跑几天后早先的日志就被滚走了。与此同时独立写一份日志文件既能用grep搜索又能按日期归档还能避免丢失历史数据。对我这种要靠日志做复盘和排查的人来说这是最稳妥的组合。还有个小技巧tmux 的会话列表偶尔会堆积很多旧会话。你可以用tmux kill-server一键杀掉所有会话但这很危险建议先用tmux ls检查再操作。平时我用完一个会话都会主动按Ctrl b然后x关掉它保持环境整洁避免下次attach的时候误入错误的会话。5. 生产环境的正规军systemd 服务5.1 为什么最终要落到 systemdnohup、tmux这套组合适合开发机和临时任务但如果是正式生产环境比如网站后端、定时任务、常驻守护进程我强烈建议直接写成systemd服务。原因很简单systemd能管住进程的一生。开机自启、崩溃自动重启、日志统一收集、权限独立控制这些都是tmux和nohup给不了的。举个最常见的场景你的服务程序因为内存溢出挂掉了。用nohup跑的进程挂了就是挂了没人会去重启它你会收到一堆告警才知道出事。但用systemd管理你可以在配置里写Restartalways程序挂掉后系统会自动拉起新的进程整个过程不需要人工介入大大缩短了业务中断时间。5.2 一个完整的 service 文件长什么样写一个最基础的服务文件假设我们的程序是/opt/myapp/app.py[Unit] DescriptionMy Python Application Afternetwork.target [Service] Typesimple WorkingDirectory/opt/myapp ExecStart/usr/bin/python3 /opt/myapp/app.py Restartalways RestartSec3 Usernobody Groupnogroup EnvironmentMY_ENVproduction StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-user.target把这个文件保存为/etc/systemd/system/myapp.service然后systemctl daemon-reload systemctl enable myapp systemctl start myappenable负责开机自启start负责立刻启动。之后你就可以用systemctl status myapp查看状态其中会显示 PID、内存占用、最近日志排查问题时特别方便。这里面有几个字段我想单独解释一下Typesimple表示ExecStart启动的进程就是主服务进程适合大多数情况。Restartalways无论什么原因退出都自动重启。如果想避免“手动停止也被重启”的尴尬可以设置Restarton-failure只在异常退出时重启。RestartSec3重启前等待 3 秒防止程序启动失败后频繁重启占满 CPU。User和Group以低权限用户运行服务避免程序被入侵后拿到管理员权限这是安全底线。如果你不想让日志只留在 journal 里也可以配置输出到文件StandardOutputfile:/var/log/myapp.log StandardErrorfile:/var/log/myapp.log但更推荐的做法是用 journal因为journalctl -u myapp可以直接按时间线查看日志还能结合-f参数实时追踪。5.3 systemd 和 nohup 的直观对比有些新项目一上来就用nohup启动各种服务当时没问题等服务一多、依赖一复杂就乱套了。我做了个对比表方便你判断自己的场景该用哪套能力维度nohup systemd 服务开机自启需要自己写 rc.local 或 crontab一行配置搞定崩溃重启无自动日志管理手动重定向到文件journal 统一收集权限控制继承启动用户可指定独立用户依赖顺序无可声明等待网络、其他服务适用场景临时任务、开发调试生产服务、常驻进程说句实话我现在碰到“把一个程序部署到正式服务器跑”的请求第一反应都是先问一句你打算怎么管理它如果对方没有明确想法我会直接帮他写一个 service 文件。看起来是多了几步操作但后面所有围绕这个进程的管理工作都会轻松一个量级。5.4 用 systemd 跑先临时任务的一个技巧systemd 不止能管常驻服务也能顺便管理临时任务这就是systemd-run。如果你不想写完整服务文件只是想把当前命令变成 systemd 管理的临时服务可以这样systemd-run --unittemp-task --collect python /opt/myapp/task.py--unit给任务一个名字--collect在任务结束自动清理单元。之后用journalctl -u temp-task查看日志systemctl stop temp-task手动停止。这个技巧适合那些又不想落 service 文件、又不想用nohup裸跑的场景兼顾了管理的规范和操作的轻量。6. 排查与避坑这些坑我都是亲自踩过的6.1 输出文件把磁盘写满最经典的坑。程序跑得欢日志文件也在疯狂增长一个月下来磁盘满了数据库、web 服务全部跟着出问题。解决方案不只是“重定向到文件”还要考虑轮转。Linux 自带的logrotate就可以对日志做定期切割和压缩比如/path/to/app.log { daily rotate 14 compress missingok notifempty }写到/etc/logrotate.d/myapp里即可。这样日志按天分割保留两周过期的自动压缩和删除磁盘空间就不会莫名其妙被吃光。很多人只知道重定向不知道轮转这是运维意识和开发意识的差别。6.2 SSH 断开瞬间任务被杀有时候你明明用了nohup但 SSH 断开时任务还是被中断了。这是我被坑得比较多的一种情况。原因通常有两个第一你只加了忘了nohup第二你用了nohup但程序是某个 shell 脚本的间接子进程信号从会话首进程传下来时子进程因为此前没有读/dev/null在终端关闭时尝试往已失效的终端输出内容挂了。正确的做法是把输出、错误、输入全部重定向再加上nohup必要时用setsid彻底脱离会话。我在跑批量脚本时最少用的命令是setsid nohup bash batch.sh /tmp/batch.log 21 /dev/null setsid和nohup同时用相当于双重保险进程既脱离了会话也忽略挂断信号绝大多数场景下都不会再被杀。6.3 双重 nohup 导致进程杀不掉有次我启动了一个任务后想更新配置就手动 kill 了旧的进程 ID结果发现服务还在响应。查来查去发现启动脚本里写了两层调用比如外层脚本nohup bash start.sh 而start.sh内部又用nohup启动了一个子进程。最终真正干活的进程是孙子进程PID 已经不是你最初记录的那个了。排查这种问题可以用pstree -ap PID查看进程树理清父子关系。或者用pgrep -af app.py找出所有包含关键词的进程。杀掉进程树时用pkill -f定向清理比手动kill一个个精确得多。但用pkill -f要小心关键字一定要足够独特否则容易误杀其他进程。6.4 进程明明在跑但你找不到输出这个坑排查起来特别费时间。程序没挂但日志文件一直不更新你怀疑程序是不是死循环了。后来才发现程序的标准输出和标准错误全部进了缓冲区而终端断了之后缓冲区没有及时刷新你没有在程序里设置无缓冲输出。如果你用 Python在启动时加一个参数python -u app.py这个-u表示强制 stdout 和 stderr 为无缓冲模式日志会实时写入文件。很多带缓冲的语言或框架都有类似的开关跑后台任务时务必打开否则日志文件会是空的或者严重滞后排查问题会变得非常痛苦。6.5 怎样才能确认进程真的在后台这里分享一个组合命令是我平时检查后台进程是否健在的“三板斧”ps -ef | grep app.py | grep -v grep这条命令能精确筛出app.py对应的进程grep -v grep是为了排除第二条 grep 命令自身。然后看日志文件的tailtail -n 50 /tmp/app.log最后查看监听端口是否正常拿 web 服务举例ss -tnlp | grep 8080如果一个进程既在进程列表里、日志又在更新、端口也能访问那基本可以断定它运行得没毛病。这套验证流程我每次部署都会走一遍几分钟的事能省掉很多后续莫名其妙的猜测。6.6 常见问题速查表现象可能原因解决方案关终端后进程消失没用 nohup 或 setsid使用nohup ... 或setsid日志文件不更新程序输出缓冲Python 加-u或配置无缓冲输出nohup.out 占满磁盘未重定向且未轮转重定向到指定文件并配置 logrotate进程杀不掉存在多层 nohup 或僵尸子进程用pstree查树用pkill -f清理环境变量丢失依赖终端手动 export把变量固化到配置文件或 systemd 的 Environment7. 我最终沉淀下来的选择顺序回头看这些方案它们之间不是替代关系而是各管一层的协作关系。我的选择顺序逐渐收敛成一套个人模板这里分享给你参考。如果你只是临时跑一个测试脚本一两分钟就结束直接前台跑就好根本不需要后台。如果任务要跑几小时到几天而且不需要交互就用setsid加输出重定向干净利落。如果任务可能需要中途查看进度、改参数、甚至临时开个终端验证点什么那直接开一个tmux会话这是我最常用的方式。如果这个程序要被当成正式服务长期运行还要支持开机自启和自动重启就直接写systemd服务文件不能图方便用nohup顶着。根据我自己这几年的经验真正让我少走弯路的不是某一个具体命令而是那个习惯每次启动重要任务之前先花三十秒想清楚这个进程在终端关闭后还要不要活。想清楚了选对方案后面就不会有一大堆半夜爬起来救火的局面。希望这篇整理能帮你把同样的坑提前绕过去。