资讯动态

Linux后台运行与守护进程:SIGHUP、nohup、systemd实战

发布时间:2026/10/26 11:47:15 来源:尧图企业网站定制
凌晨两点被叫起来处理一次线上异常最常见的场景不是代码写错了而是有人在远程终端里敲了一条python train.py接着把窗口一关第二天回来发现进程早就没了日志停在半截数据文件写了一半。问题不在命令本身而在于对 Linux 的会话模型没有概念终端一断内核会给这个会话里的所有进程发一个SIGHUP前台后台一视同仁谁没做处理谁就走人。这篇内容就把Linux 系统中的前后台任务与守护进程这件事从头到尾捋一遍从前台、后台、作业控制、进程组会话这些底层结构到nohup、disown、setsid、tmux的边界再到传统双 fork 写法和 systemd unit 的现代做法最后落到日志、权限、资源三个最容易翻车的点以及一份可以直接抄的排查速查表。适合谁看刚接触 Linux、被关窗口进程就死坑过的同学写过服务但不确定Typeforking和Typesimple差在哪的同学还有那些手上有几个跑在后台的脚本、心里没底的运维和开发。基础概念我会铺开讲但不会停在概念上每个环节都给出能直接落地的命令和配置。1. 前台、后台、守护进程三个词三套完全不同的运行模型1.1 关掉窗口进程就死本质是控制终端没了先把最基本的一条说清楚Linux 里一个交互式登录内核层面会建立一个会话session会话里有一个控制终端controlling terminal通常是pts/0这种伪终端。会话首进程一般是 shell你敲的每条命令都会由 shellfork出来挂在这个会话下面。关键点在于控制终端一旦消失关窗口、断网、终端设备被注销内核会向该会话的前台进程组发送SIGHUP很多 shell比如 bash还会在退出时给所有作业发SIGHUP。SIGHUP的默认动作是终止进程而且是不做清理的那种直接终止。所以你在窗口里跑的python、java、ffmpeg窗口一关全灭这不是 bug是设计如此。反过来如果你希望进程活得比终端久就必须让它脱离这个会话或者让它在收到SIGHUP时选择忽略。前者是setsid、tmux、systemd 干的事后者是nohup干的事。这两条路线的差别后面会细说很多人把它们混为一谈结果踩坑。1.2 前台任务和后台任务差的不只是一个前台任务的定义很明确它所在的进程组是终端的前台进程组它可以直接读键盘输入stdin指向终端也会接收键盘产生的信号——CtrlC发SIGINTCtrl\发SIGQUITCtrlZ发SIGTSTP。一个终端同一时刻只能有一个前台进程组这是硬件时代留下来的规矩因为键盘只有一个。后台任务则是作业控制job control的产物。你敲cmd shell 把它放进一个新的进程组但不把它设为前台组于是它拿不到键盘输入也收不到CtrlC。注意这句话的推论后台任务如果去读终端会被内核用SIGTTIN停住进程状态变成T看起来像卡死了其实是被挂起了。往终端写也一样如果终端设置了tostop写操作会触发SIGTTOU。这个坑在跑交互式程序时特别常见。再强调一次后台任务仍然是这个会话的成员仍然挂在那个控制终端上。所以cmd 之后关窗口它照样死。只解决了不占着命令行的问题没解决活得久的问题。1.3 守护进程生下来就没有终端守护进程daemon的判定标准很短它不属于任何控制终端。技术上表现为——它是自己会话的首进程SID PID没有控制终端ps里TTY显示为?通常父进程是 1 号进程被 init/systemd 收养工作目录一般是/标准输入输出重定向到/dev/null或日志文件。因为压根没有控制终端SIGHUP这类跟终端绑定的信号根本没机会找上它也就谈不上被终端关闭杀死。这就是它和前两种模型的根本区别前两种是会话内的成员守护进程是脱离会话的独立个体。由此还带来两个副产品。第一守护进程不能指望用户输入任何需要交互的设计都是错的第二守护进程出问题时你看不到它的输出所以日志必须自己写到文件或系统日志里这是硬性要求不是可选项。很多人写完 daemon 发现什么都没输出就是漏了这一步。维度前台任务后台任务守护进程控制终端有且是前台进程组有但不是前台组无能读 stdin能不能读会被SIGTTIN挂起不能一般重定向到/dev/null响应CtrlC响应不响应不响应终端关闭后被SIGHUP终止被SIGHUP终止不受影响父进程shellshell1 号进程或 subreaper典型用途交互式命令短时并行任务常驻服务2. 把进程关系理清进程组、会话、控制终端2.1 三层结构一层都不能跳理解作业控制必须建立进程 → 进程组 → 会话这三层模型。**进程组process group**是一组相关进程的集合组长进程的 PID 就是 PGID信号可以发给整个组kill -- -PGID。**会话session**是一组进程组的集合会话首进程的 PID 就是 SID一个会话最多关联一个控制终端。为什么要这么分层因为信号需要广播。你按一次CtrlC内核不是给某一个进程发信号而是给前台进程组发这样cmd | grep xxx这种管道里的两个进程会一起收到不会出现只死一半的尴尬局面。同理终端关闭时SIGHUP也是按组、按会话发的。用命令看这三层关系一条就够ps -o pid,ppid,pgid,sid,tty,stat,cmd -p 12345输出里PID是自己PPID是父进程PGID是所在进程组SID是会话号TTY是控制终端。如果TTY是?说明没有控制终端如果PID SID说明它是会话首进程。这两个判断条件是识别守护进程最快的办法比看什么启动脚本都靠谱。2.2 用/proc交叉验证比ps更直接ps有时候会骗人比如容器里的 PID 命名空间更硬的证据在/proc。看/proc/PID/stat的第 4、5、6、7 个字段分别是ppid、pgrp、session、tty_nr。tty_nr为 0 就代表没有控制终端。awk {print ppid$4, pgrp$5, session$6, tty$7} /proc/12345/stat另外/proc/PID/fd/下面能看到它继承了哪些文件描述符0 1 2分别指向哪里一目了然ls -l /proc/12345/fd如果0/1/2还指着一个pts/N那这个进程还挂在终端上随时可能被SIGHUP带走。这个检查动作我建议养成习惯尤其是接手别人留下的服务时先看一眼它到底脱离干净没有。2.3 作业控制相关的信号一张表记住信号是作业控制的执行手段把常见的几个记住排查时能省一半时间信号编号谁来触发默认动作备注SIGHUP1终端关闭、会话首进程退出终止可被忽略nohup就是干这个的SIGINT2CtrlC终止发给前台进程组SIGQUIT3Ctrl\终止 core dump排查崩溃时有用SIGKILL9kill -9强制终止不可捕获不给清理机会SIGTERM15kill默认终止可捕获应作为优雅退出首选SIGSTOP19kill -STOP暂停不可捕获SIGTSTP20CtrlZ暂停可捕获所以程序能选择不暂停SIGCONT18bg、fg、kill -CONT恢复运行配合SIGTSTP使用SIGTTIN21后台进程读终端暂停后台任务卡住的头号原因SIGTTOU22后台进程写终端开启tostop暂停默认多数终端没开SIGCHLD17子进程状态变化忽略处理不当会产生僵尸进程一个实操细节kill -9之所以被反复告诫慎用是因为进程拿不到清理机会缓存里的数据没落盘、临时文件没删、锁没释放下次启动可能直接起不来。写服务的时候SIGTERM里挂一个优雅退出的处理函数是基本功。3. 前台后台的实操从到disown的完整路径3.1 六个动作覆盖 90% 的日常场景日常操作其实就六个动作练熟就够了cmd 直接扔后台shell 会打印[1] 12345方括号里是作业号后面是 PID。CtrlZ把当前前台任务挂起发SIGTSTP进程状态变T。jobs -l列出当前 shell 的所有作业-l会带上 PID。bg %1把 1 号作业放到后台继续跑发SIGCONT。fg %1把 1 号作业拉回前台。CtrlC给前台进程组发SIGINT。举一个很典型的救场场景你敲了vim结果想起还有个文件要改CtrlZ挂起处理完别的再fg拉回来编辑状态全在。这个技巧在只有单个终端窗口的时候特别救命。再举一个你启动了一个耗时十分钟的压缩任务忘了加直接CtrlZ再bg %1它就转到后台继续跑了。注意bg之后它的输出还是会往终端打如果输出量很大终端会被刷屏这种时候用CtrlZ→bg→ 重定向或者干脆一开始就重定向好。3.2 后台任务的输入输出必须提前处理后台任务卡在T状态十有八九是它想读stdin。比如你了一个交互式脚本它执行到read那一步内核发现它在后台读终端直接SIGTTIN停住。解决方法很简单启动时把stdin重定向掉./script.sh /dev/null ./run.log 21 /dev/null让读操作立刻拿到 EOF不会挂起。 ./run.log 21把标准输出和标准错误都收进文件避免刷屏也避免程序因为终端写失败而报错。21的位置有讲究必须放在之后否则stderr还是指向原来的终端——这个顺序问题新手几乎都踩过一次。还有一点21之后再顺序不能颠倒。cmd 21 log 是错的stderr会指向终端stdout才进文件。判断方法很简单ls -l /proc/PID/fd/1 /proc/PID/fd/2看一眼两个链接指向是不是同一个文件。3.3nohup、disown、setsid、tmux四条路线的边界这四个工具经常被混着用其实解决的问题层次完全不同工具解决的层次是否脱离会话是否能抗终端关闭局限nohup忽略SIGHUP否是进程还在原会话但忽略了信号不处理其他信号不重定向会生成nohup.outdisown从 shell 作业表移除否是shell 退出时不再给它发SIGHUP只对当前 shell 生效shell 被kill -9时无效setsid新建会话是是完全断开无法再用fg拉回tmux/screen提供虚拟终端是服务端独立会话是需要额外进程适合需要交互的场景nohup的坑要单独说。它的作用只是忽略SIGHUP进程依然挂在原会话和终端上。如果终端设备被彻底注销进程后续对终端的写操作可能返回EIO。而且默认情况下nohup会把输出追加到当前目录的nohup.out一个高频打印的服务跑两天就能把这个文件撑到几十 GB把磁盘打满。所以nohup一定要配重定向nohup ./worker --config ./conf.yaml ./logs/worker.log 21 disown -h %1和disown %1的区别也值得记一笔。-h是标记这个作业shell 退出时不要给它发SIGHUP进程还在作业表里不带-h是直接从作业表删掉jobs都看不到了但进程还在跑。两个都能让进程活下来选哪个取决于你还要不要管它。setsid是最彻底的直接setsid ./worker ./logs/worker.log 21 它在新的会话里跑SID PIDTTY为?标准意义上就是半个守护进程了。缺点是彻底断开想再拉回前台不可能只能靠日志和信号去管。3.4 一个可以直接抄的启动模板把这些拼起来我常用的一套启动模板是这样的#!/usr/bin/env bash set -euo pipefail APP_HOME/opt/myapp LOG_DIR${APP_HOME}/logs PID_FILE${APP_HOME}/run/app.pid mkdir -p ${LOG_DIR} ${APP_HOME}/run # 1. 防重复启动 if [[ -f ${PID_FILE} ]] kill -0 $(cat ${PID_FILE}) 2/dev/null; then echo already running: $(cat ${PID_FILE}) exit 0 fi # 2. 脱离终端 重定向 记录 PID setsid ${APP_HOME}/bin/app --config ${APP_HOME}/conf/app.yaml \ /dev/null ${LOG_DIR}/app.log 21 echo $! ${PID_FILE} echo started pid$(cat ${PID_FILE})这段脚本里有三个细节值得注意set -euo pipefail让脚本在出错时立刻停下避免带着半截状态继续kill -0只检查进程是否存在、不发信号是做防重复启动的标准手法而不是避免重启时把历史日志冲掉。用setsid而不是nohup是因为它真正脱离会话比忽略信号更干净。4. 守护进程从双 fork 到 systemd unit4.1 传统双 fork每一步都在解决一个具体问题教科书上的守护进程写法是双 fork setsid 收尾很多人背下来了但不知道每一步为什么。拆开看每一步都对应一个具体约束第一次fork后父进程退出让子进程变成孤儿被 1 号进程收养。这样做的好处是子进程不再持有 shell 的作业控制关系shell 退出时不会连带处理它。副作用是父进程立刻返回启动脚本能马上拿到退出码。setsid()创建新会话调用者成为会话首进程和新进程组的组长同时脱离原控制终端。这一步是脱离终端的关键不做这一步后面全是白搭。第二次fork这一步最容易被忽略。setsid之后进程成了会话首进程而只有会话首进程才有资格打开终端并获得控制终端。再 fork 一次让子进程继承会话但不再是首进程它就没法获得控制终端了——这才是永久脱离终端的保险栓。没有这一步进程后续open(/dev/tty)之类的操作仍可能把终端重新绑回来。chdir(/)防止进程占着某个挂载点导致无法卸载。这个坑在嵌入式设备上特别致命一个 daemon 的当前目录停在 U 盘挂载点umount永远返回 busy。umask(0)或显式设置daemon 通常需要自己控制文件权限继承来的umask不确定显式设成 0 或者设成业务需要的值比如022更可控。关闭并重定向文件描述符把继承来的0/1/2全指向/dev/null或者把1/2指向日志文件。这一步不做进程会一直占着父进程的管道或终端导致调用方read不到 EOF 而卡住也可能导致端口或文件句柄泄漏。4.2 一个最小可用的守护进程骨架下面这段 C 代码把上面的步骤串起来去掉业务逻辑只保留骨架可以直接往里面填#include stdio.h #include stdlib.h #include unistd.h #include fcntl.h #include sys/stat.h #include signal.h static void daemonize(void) { pid_t pid fork(); if (pid 0) exit(EXIT_FAILURE); if (pid 0) exit(EXIT_SUCCESS); /* 父进程退出 */ if (setsid() 0) exit(EXIT_FAILURE); /* 新建会话脱离控制终端 */ signal(SIGHUP, SIG_IGN); signal(SIGPIPE, SIG_IGN); pid fork(); /* 第二次 fork放弃会话首进程身份 */ if (pid 0) exit(EXIT_FAILURE); if (pid 0) exit(EXIT_SUCCESS); umask(022); if (chdir(/) 0) exit(EXIT_FAILURE); int fd open(/dev/null, O_RDWR); if (fd 0) { dup2(fd, STDIN_FILENO); dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); if (fd STDERR_FILENO) close(fd); } } int main(void) { daemonize(); /* 业务初始化打开日志、监听端口、加载配置…… */ for (;;) { /* 主循环 */ sleep(10); } return 0; }编译验证gcc -O2 -Wall -o /tmp/mydaemon daemon.c /tmp/mydaemon ps -o pid,ppid,pgid,sid,tty,stat,cmd -C mydaemon正常的话应该看到PPID是 1TTY是?PID SIDSTAT是S可中断睡眠。这三个特征同时成立才算真正脱离干净。注意这段骨架故意没有处理SIGTERM。真实业务里必须注册SIGTERM的处理函数在里面停止接收新请求、等当前任务跑完、刷盘、删 PID 文件然后再退出。否则systemctl stop等超时后会升级成SIGKILL数据就可能丢。4.3 systemd 的做法不 fork交给 1 号进程管现代发行版上绝大多数场景不该自己写 daemon 化逻辑了。systemd 的设计思路是进程不要自己 fork 到后台就老老实实待在前台退出和重启由我来管。这样做的收益很实在进程的父子关系清晰崩溃能被立刻感知日志自动进 journal重启策略、资源限制、依赖顺序全部声明式配置。一个够用的 unit 文件长这样[Unit] DescriptionMy Worker Service Afternetwork-online.target Wantsnetwork-online.target [Service] Typesimple ExecStart/opt/myapp/bin/app --config /opt/myapp/conf/app.yaml WorkingDirectory/opt/myapp Userappuser Groupappuser EnvironmentAPP_ENVprod EnvironmentFile-/opt/myapp/conf/env StandardOutputappend:/var/log/myapp/app.log StandardErrorappend:/var/log/myapp/app.err.log Restarton-failure RestartSec5 LimitNOFILE65535 TimeoutStopSec30 KillSignalSIGTERM [Install] WantedBymulti-user.target几个字段值得单独解释。Typesimple表示进程启动即视为服务就绪适合绝大多数常驻服务如果程序自己会 fork 到后台老式 daemon得用Typeforking并配PIDFile如果是启动后监听某个 socket 才算就绪用Typenotify更准。这个选错会导致systemctl start回来了但服务其实还没起来后面调用的脚本直接失败。Restarton-failure只在非零退出、被信号杀死、超时这几种情况下重启正常exit 0不会重启。如果不管怎么退出都要拉起来用Restartalways。但要小心一个启动即崩的程序配了always会变成疯狂重启把日志刷爆systemd有默认的启动频率限制DefaultStartLimitIntervalSec和DefaultStartLimitBurst超过会进入 failed 状态并停止重试这个机制其实是在保护你。KillSignalSIGTERM是默认值TimeoutStopSec30表示给 30 秒优雅退出超时后systemd会用SIGKILL补刀。如果你的程序退出时要刷大量数据把这个值调大。写完 unit 之后的标准动作systemctl daemon-reload systemctl enable --now myapp.service systemctl status myapp.service journalctl -u myapp.service -f -n 100daemon-reload这一步新手最容易漏改完 unit 不 reloadsystemctl用的还是旧配置然后纳闷我明明改了。4.4 两条路线怎么选给一个明确结论我的判断标准很直接只要能上 systemd就别手写 daemon 化逻辑。理由是手写方案要自己处理日志轮转、崩溃重启、开机自启、资源限制、依赖顺序每一项都是坑而 systemd 全部免费提供还能journalctl统一查日志。只有三种情况需要传统双 fork目标环境是容器且基础镜像里没有 init这种情况下通常连守护进程都不该有前台跑就对了运行在 BusyBox 环境或没有 systemd 的精简系统程序需要兼容非常老的发行版。维度双 fork 自管理systemd 托管日志自己写文件 自己做轮转journal或append:自动处理崩溃重启自己实现Restart一行搞定开机自启写 init 脚本或 rc 文件systemctl enable资源限制setrlimit手写LimitNOFILE等声明式配置状态查询查 PID 文件systemctl status进程关系父子被切断排查困难父子清晰systemd是直接父进程适用场景老环境、精简系统现代发行版推荐结论明确新项目用 systemd把精力花在业务上。5. 三个隐形雷区日志、权限、资源5.1 日志不写日志的守护进程等于黑盒守护进程没有终端所有printf全部丢失这是它最大的调试障碍。所以第一件事就是把输出接出来。最低要求是启动时把stdout/stderr重定向到日志文件更好一点是接syslog或journal让日志系统统一管理最省心的是直接交给 systemd 的StandardOutputappend:或者干脆不重定向让它进 journal。自己写文件的话轮转必须自己做或者交给logrotate。一个每天输出几 GB 的服务不轮转就是等着磁盘满。logrotate的配置大概是这个形状/var/log/myapp/*.log { daily rotate 14 compress delaycompress missingok notifempty copytruncate }copytruncate是个务实的选择它先复制文件再清空原文件不需要给程序发信号重新打开日志代价是复制瞬间可能丢几行。如果程序支持SIGUSR1重开日志用create模式更好不丢数据。提示copytruncate在日志写入非常频繁时有小概率丢内容对账类业务要慎用优先改成程序支持信号重开日志。5.2 工作目录、umask、继承的 fd一个都不能放过chdir(/)不只是看起来干净它有实际后果。前面说过挂载点被占住的问题还有一个更隐蔽的如果进程的当前目录在一个被删除的目录里比如你rm -rf了部署目录再重建进程还抓着旧 inode磁盘空间不释放df看着满了但du找不到大文件。这种问题排查起来非常费时间lsof L1能列出所有指向已删除文件的句柄。umask的影响在于文件创建权限。守护进程创建的数据文件、日志文件如果权限过宽是安全隐患过窄又会导致其他用户或备份程序读不到。显式设置umask(022)生成644的文件是比较稳妥的默认值敏感数据用077。继承的文件描述符是最容易被忽视的一条。如果启动脚本用管道接了守护进程的输出而守护进程没有关闭这些 fd那么管道永远不会EOF调用方会一直阻塞。这也是为什么我的启动脚本卡住了的常见原因之一。排查方法还是看/proc/PID/fd数一数有多少个不该存在的句柄。5.3 资源限制与重启策略要配套LimitNOFILE这类限制在 systemd 里配置很直观但要注意两个层次systemd 的DefaultLimitNOFILE全局和 unit 里的LimitNOFILE单个服务后者覆盖前者。改完必须daemon-reload再restart只restart不生效这是高频踩坑点。重启策略要有节流。RestartSec5是最小间隔但更稳的做法是配合StartLimitBurst[Unit] StartLimitIntervalSec60 StartLimitBurst5含义是 60 秒内最多启动 5 次超过就进入 failed 不再重试。这个机制能防止配置写错 → 无限重启 → 日志刷满磁盘 → 整机不可用的雪崩链条。我见过一次因为缺了这个配置一个服务把日志分区写满最后连systemd自己都写不了日志。内存层面MemoryMax可以给服务设上限超过会被 OOM killer 干掉然后按Restart策略重启。这个要比让整个系统被拖垮好得多。设置之前先用systemd-analyze或实际压测确认峰值内存别拍脑袋写个 512M 结果业务一跑就挂。6. 排查实录常见故障与定位手法6.1 常见问题速查表下面这张表是我这些年遇到频率最高的问题按现象查就行现象最可能原因快速验证处理方式关终端后进程消失没脱离会话收到SIGHUPps -o tty,sid -p PID看TTY是否为pts/N用setsid或 systemd 启动后台任务不输出也不动读终端被SIGTTIN挂起ps看STAT是否为Tkill -CONT恢复启动时 /dev/nullnohup.out撑爆磁盘nohup默认写出到当前目录du -sh nohup.out显式重定向并清理文件服务起不来但无报错日志被丢到/dev/nullls -l /proc/PID/fd/1重定向到文件或交 systemd目录删了磁盘不释放进程当前目录或 fd 指向已删除文件lsof L1重启进程启动时chdir(/)端口被占无法重启旧进程没退干净或 fd 泄漏ss -lntp | grep 端口kill -TERM旧进程检查子进程systemctl start成功但服务不可用Type配错systemctl show -p Type改simple/forking/notify改完 unit 不生效忘了daemon-reload对比systemctl cat与实际文件先 reload 再 restart进程变Z僵尸父进程没waitps -eo stat,ppid,pid | grep Z修父进程代码或干掉父进程让 init 接管优雅停止超时被杀SIGTERM处理缺失或太慢journalctl看停止耗时补信号处理调大TimeoutStopSecumount一直 busydaemon 工作目录在挂载点内fuser -vm 挂载点让 daemonchdir(/)或先停服务重启风暴刷爆日志Restartalways 启动即崩journalctl看重启间隔配StartLimitBurst节流6.2 定位流程从状态到信号四步走完遇到进程好像活着但没反应我一般按这个顺序走四步基本能定位第一步看状态。ps -o pid,ppid,pgid,sid,tty,stat,etime,cmd -p PID。STAT里的字母是核心线索R在跑S在等事件D在等不可中断 IO通常是磁盘或 NFST被暂停Z僵尸。D状态要特别注意kill -9对它无效只能等 IO 返回或者处理底层问题。T状态多半是被SIGTTIN或SIGTSTP挂住了kill -CONT一下试试。第二步看它在等什么。cat /proc/PID/wchan能看到内核里的等待点ls -l /proc/PID/fd看句柄strace -p PID -f -tt挂在系统调用上看它到底卡在哪一步。strace对正在跑的服务有性能影响生产环境用要谨慎加个-e tracenetwork,file缩小范围。第三步看日志和资源。journalctl -u 服务名 --since 10 min ago看最近的日志dmesg -T | tail -50看内核有没有 OOM 或者 IO 错误。OOM killer 杀进程这件事只在dmesg和 journal 里留痕应用日志里什么都不会有所以这一步别省。第四步小心地发信号。顺序是SIGTERM15→ 等TimeoutStopSec→SIGKILL9。如果进程对SIGTERM无响应先确认它是不是真的注册了处理函数cat /proc/PID/status | grep SigCgt能看到已捕获的信号位图。别一上来就-9那等于放弃所有清理动作。# 查看进程捕获了哪些信号十六进制位图 grep -E SigCgt|SigIgn|SigBlk /proc/12345/status # 按名字找进程比 grep ps 输出更准 pgrep -af myapp # 看进程树确认父子关系有没有被切断 pstree -ap 123456.3 几条踩出来的经验第一条判断一个进程是不是守护进程别看启动脚本看TTY。启动脚本写着daemon不代表真脱离了ps -o tty显示?才是硬证据。我接手过一个守护进程TTY显示pts/3结果运维一退出登录它就挂了排查了半天才发现启动方式不对。第二条kill -9之后一定要检查有没有留下子进程。父进程被强杀它的子进程会被 1 号进程接管继续跑端口还占着systemd重启服务时直接报Address already in use。这种时候pstree -p或者ss -lntp一查就露馅。第三条和nohup都不是后台服务方案只是临时手段。临时跑个十分钟的脚本用它们没问题但任何需要活过一天以上的东西都应该进 systemd或者容器里当前台进程跑。用跑生产服务本质上是在赌没人关窗口、没人重启机器。第四条遇到疑难杂症先做减法。停掉一半服务看问题还在不在把日志级别开到 debug 跑一轮用strace抓一遍系统调用。很多莫名其妙的问题追下去都是某个 fd 没关、某个信号没处理、某个路径写错了只是被层层封装盖住了。最后分享一个小习惯写任何常驻服务之前先花五分钟把启动脚本、日志路径、PID 文件位置、停止方式这四件事定下来写成一份固定的模板之后所有服务都套这个模板。这件事看起来琐碎但能省掉后面无数次这个服务到底怎么重启来着的翻找。我自己的模板里还有一行固定注释写着这个服务对SIGTERM的处理逻辑和预期停止耗时接手的人看一眼就知道该等多久。这类不起眼的约定比任何文档都管用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑