最近在给机房的一批新服务器做基线配置,凌晨两点被监控告警吵醒——一台机器的负载飙到 30 多,SSH 登录都要卡好几秒。上去一看,ps里躺着一个 D 状态的进程,怎么kill -9都不掉;再去翻/var/log/cron,发现昨天配置的备份计划任务压根没执行。这俩问题凑一块儿,逼着我又把 Linux 进程和计划任务这两块从头到尾捋了一遍。今天就把这些经验整理出来,从进程的本质说到计划任务的排障,希望能给你省点半夜爬起来看告警的时间。
1. 先搞懂进程到底是什么,后面所有的排查才有方向
很多新手拿到一台 Linux 机器,上来就敲ps -aux或者top,看到一大堆输出直接懵掉。这很正常,因为进程这个话题,表面上是几个命令,实际上牵扯到操作系统最核心的一套机制。先把底层概念理清楚,后面无论做监控、调优还是排障,都会顺手非常多。
1.1 程序和进程:一个是菜谱,一个是下锅的菜
教科书上的定义是:程序是静态的指令集合,进程是程序的一次执行实例。这个说法没错,但我更喜欢打一个比方:程序就是菜谱,存储在你的磁盘上,安安静静躺着;进程是你按照菜谱下锅炒菜的过程,食材下锅了、燃气开着了、锅里冒着烟,这时候才算一个进程。同一个菜谱,你可以同时炒三锅,对应同一个程序可以同时跑多个进程。
这个区别在实际运维中非常有用。比如你用nginx这个程序启动了 master 进程和一堆 worker 进程,它们共享同一个可执行文件,但是各自的 PID、内存空间、打开的文件描述符都是独立的。排查的时候,你kill掉的必须是被 PID 对应的那个具体进程,而不是"程序名",这一点新手最容易搞混。
1.2 内核视角下的进程:一切皆文件之外的"一切皆进程"
Linux 内核管理进程的核心数据结构叫进程描述符(task_struct),里面保存了进程的状态、优先级、打开的文件列表、信号处理函数、内存布局等等信息。你可以把 task_struct 理解为每个进程的"户口本",内核调度器就是靠这一堆户口本决定下一个该让谁上 CPU 跑的。
当你敲下ps命令时,实际做的事情就是通过/proc虚拟文件系统去读取这些户口本。/proc下面每个数字目录就对应一个 PID,比如/proc/1234/就是 PID 为 1234 的进程信息目录。命令行敲ls /proc/1234/你能看到cmdline、fd、status这些文件。之所以提这个,是因为很多时候进程出了问题、普通的ps命令看不到有效信息时,直接去翻/proc/下面的文件往往能拿到第一手线索,这是排查疑难杂症非常实用的一招。
1.3 进程的三态模型和 Linux 特有状态
老八股一点的操作系统教材会讲进程的三态:就绪、运行、阻塞。Linux 为了更精确地描述现实情况,把状态分得更细,ps命令里的STAT字段就是干这个的。下面这张表是你在top或者ps里最常见的几种状态:
| 状态码 | 含义 | 常见场景 |
|---|---|---|
| R | Running / Runnable,正在运行或排队等待运行 | 计算密集型的任务、CPU 被打满时大量 R 状态 |
| S | Sleeping,可中断睡眠 | 等待 I/O 完成、等待网络数据包,绝大多数进程处于这个状态 |
| D | Uninterruptible Sleep,不可中断睡眠 | 等待磁盘 I/O,或者内核态某些不可被打断的操作 |
| Z | Zombie,僵尸进程 | 子进程已退出,但父进程还没收尸(没调用 wait) |
| T | Stopped / Traced,停止或调试状态 | 按了 Ctrl+Z、被 gdb 断点抓住的时候 |
| I | Idle,内核线程的空闲状态 | 内核线程如kworker的常态 |
这里我想特别强调D 状态。它是无数运维人的噩梦,原因在于"不可中断"。你kill -9发给它,信号不是被忽略,而是内核根本不会去处理——因为进程正卡在内核态的一段代码里,比如正在等硬盘响应。这就像是你在银行柜台办业务,柜员锁死了系统,你喊破喉咙也没用,只能等柜员把系统恢复过来。碰上 D 状态进程,第一反应不该是 kill,而是先查存储:是不是磁盘阵列出故障了、是不是 NFS 连不上远程服务器导致 IO 卡死、是不是磁盘 quota 满了。等底层 I/O 恢复,D 状态自然消除。
2. 进程管理实战:ps、top、kill 的正确打开方式
理清了概念,我们来看实打实的命令。市面上讲 Linux 命令的文章多如牛毛,我在这里只挑那些真正能提高排查效率的用法和容易被忽略的坑讲。
2.1 ps:快照式查看进程,输出字段要读懂
ps的用法很多,但日常排查我几乎只会用两个组合:ps -ef和ps aux。它们展示的信息大同小异,核心字段就那几个:
- PID:进程 ID,排障时盯住它。
- PPID:父进程 ID。查一个进程是谁拉起来的,看 PPID 是最快的方式。
- %CPU / %MEM:CPU 和内存使用百分比。注意这个是进程自启动以来的平均值,不是瞬时值,调优时别被它误导,要看就看
top的瞬时值。 - STAT:前面表格里的状态码,后面可能带符号,比如
S+表示前台进程组中的进程,Ss表示会话领导者的子进程。 - TIME:进程累计占用 CPU 的时间,这个值越大说明进程消耗 CPU 越多,比 %CPU 更稳定可靠。
一个实用的排障小技巧:想找回某个进程的启动路径,用ls -l /proc/PID/exe,它是一条软链接,直接指向可执行文件的完整路径。有时候系统里一堆同名进程,比如 Java 程序,光看ps -ef分不清谁是谁,这时候ls -l /proc/PID/cwd(当前工作目录)能帮你快速锁定你到底该 kill 哪一个。
2.2 top:动态监控面板,按需按列排序
top是排查"机器到底卡在哪"的第一工具。进去之后不要傻傻地盯着屏幕看,按下面几个键来操作:
- 按
P:按 CPU 使用率降序排列,CPU 打满时第一眼就看这个。 - 按
M:按内存占用排序,查内存泄漏时配合TIME列一起观察。 - 按
k:输入 PID 后选择信号,默认 SIGTERM,也可以输入 9 发 SIGKILL。比另开一个终端kill要快。 - 按
f:进入字段管理,可以把你关心的列(比如 PPID、TIME)加进来或去掉。
有个容易被忽略的指标是load average,它显示在 top 的第一行,三个数字分别代表 1 分钟、5 分钟、15 分钟的平均负载。注意,CPU 负载不等于 CPU 使用率,它统计的是处于 R 和 D 状态的进程数量。所以如果你看到 load average 很高但 CPU 使用率并不高,基本可以确定有进程卡在 D 状态等 I/O,排查方向直接转向磁盘和网络存储。
2.3 kill 和信号:不是只有 -9
kill命令的本质是给进程发信号,信号种类非常多。日常用得上的最主要是这几个:
| 信号 | 编号 | 作用 | 使用场景 |
|---|---|---|---|
| SIGTERM | 15 | 请求进程正常终止 | 优雅停机,让进程有机会释放资源、保存状态 |
| SIGKILL | 9 | 强制终止,进程无法捕获和处理 | 进程无响应时兜底 |
| SIGHUP | 1 | 挂断信号 | 让 daemon 重新加载配置文件,比如kill -HUP nginx可以平滑 reload |
| SIGSTOP | 19 | 暂停进程执行 | 临时冻结进程,一般配合 SIGCONT 使用 |
| SIGCONT | 18 | 恢复暂停的进程 | 和 SIGSTOP 配套 |
很多人一上来就kill -9,这是我最想纠正的习惯。SIGKILL 是最后手段,因为它不给进程任何清理的机会,如果进程正在写配置文件,直接 SIGKILL 很可能留下一堆半截的数据。正确的姿势是先kill -15(SIGTERM),等个几秒,确认进程没退再kill -9。另外,pkill可以按进程名批量发信号,pkill -f甚至能匹配完整命令行,非常方便,但要注意它可能误伤同名进程,用的时候多看一眼pgrep -af的输出再动手。
3. 进程的协作机制:IPC、守护进程与"无人收尸"的僵尸
单独一个进程好理解,多个进程互相配合才是常态。这里讲三个绕不开的话题:进程间通信(IPC)、守护进程和会话、以及让无数人头疼的僵尸进程。
3.1 进程通信(IPC):管道、共享内存、信号到底怎么选
热搜里有人搜"进程通信(ipc)",说明这是一个高频困惑点。Linux 下进程间通信的手段极其丰富,我把最常用的几个和它们各自的适用场景整理一下:
- 管道(Pipe):最简单,
ls -l | grep txt这种就是匿名管道,本质上是一段内核缓冲区。它的限制是通信双方必须有亲缘关系(父子进程);命名管道(FIFO)可以在无亲缘关系的进程间用,但依然是半双工。 - 消息队列:内核维护一个消息链表,进程可以往里面写、读消息。优点是解耦,但缺点是消息大小受限,性能一般。
- 共享内存(shm):最快的 IPC 方式,多个进程把同一块物理内存映射到自己的虚拟地址空间。自己写程序做大数据量交换时首选共享内存,但必须配合信号量之类的同步机制,否则就是个数据竞争炸弹。
- 信号(Signal):前面讲的 kill 就是信号,它是异步通知机制,适合做事件通知,不适合传大量数据。
- Socket:这个大家最熟,不但能本机通信还能跨网络,
Unix Domain Socket在本机进程间通信时性能比 TCP loopback 好很多,很多数据库服务(比如 PostgreSQL、Redis 的默认配置)都用它。
选型建议很简单粗暴:要简单就管道,要性能就共享内存,要跨机器就 Socket,要事件通知就信号。做 Web 后端、写运维脚本的话,管道和 Socket 用得最多,共享内存一般只在写中间件或者性能敏感的服务时才会碰。
3.2 守护进程与会话:为什么 nohup 有时候不够
守护进程(daemon)是后台运行、不受终端控制的进程,典型代表是 sshd、crond、nginx。它有几个特征:父进程通常是 init/systemd(PPID 为 1);没有控制终端;工作目录往往是/;标准输入输出重定向到/dev/null或日志文件。
要真正理解守护进程,必须知道"会话"(Session)这个概念。会话是一个或多个进程组的集合,当你通过 SSH 登录时,系统会为你的登录创建一个新会话,你的 shell 就是这个会话的领导者。默认情况下,当你退出登录,会话终止,内核会向这个会话中的所有前台进程发送 SIGHUP 信号,进程收到后默认行为是退出——这就是为什么直接./app启动的服务在你断开 SSH 后就挂了。
nohup的原理就是忽略 SIGHUP 信号,让进程在会话结束时不会被自动杀掉。但nohup不是万能的,它只是"忽略挂断信号",如果你用Ctrl+Z把进程挂起然后退出,或者进程主动和终端断开连接,还是可能出各种奇怪问题。更可靠的后台化方案是setsid命令,它能创建一个全新的会话,让进程彻底脱离当前的终端。不过在我日常的运维实践中,现在最推荐的还是用 systemd 的 service 单元去托管进程,这个后面讲计划任务的时候一起说。
3.3 僵尸进程和进程等待 wait:父进程的"收尸"责任
僵尸进程是面试高频题,也是线上环境偶发的问题。它的成因其实很简单:子进程结束时会向父进程发送 SIGCHLD 信号,如果父进程没有调用wait()或waitpid()去读取子进程的退出状态,那么子进程的 task_struct 不能被完全释放,残留一个"尸体"在进程表里,状态就是 Z。
为什么内核不干脆直接把子进程的信息删掉?因为父进程可能需要知道子进程的退出码,用于判断任务是否成功。所以这个"尸体"是留给父进程认领的,属于合理设计。问题出在父进程不认领。
处理僵尸进程的正确顺序是:
ps -ef找到僵尸进程的 PID 和 PPID。- 查看 PPID 对应的父进程是什么,判断它是否还活着。
- 如果父进程活着,先给它发 SIGTERM,让它优雅退出。父进程退出后,僵尸进程会被 systemd(PID 1)领养,而 systemd 会周期性地调用 wait,替它收尸。
- 如果父进程本身就是故意不处理的(比如设计有问题的服务),就得考虑重启父进程服务,或者联系开发改代码。
这里有个非常容易踩的坑:光kill -9僵尸进程本身是无效的,因为它已经死了,你杀的是一个死人。必须把它的父进程干掉,僵尸才会消失。
4. 计划任务:让 Linux 在指定的时间自己干活
进程的话题聊得差不多了,接下来进入专门计划任务的部分。定时跑批是运维最日常的操作之一,备份日志、清理临时文件、数据同步、健康巡检,统统靠它。Linux 计划任务生态里,最经典的是 cron 家族,现在 systemd timer 也越来越普及。我们先从 cron 讲起。
4.1 crontab 的五个时间字段,别把顺序记反了
crontab -e会打开一个编辑界面,每行格式是:
分 时 日 月 周 命令这五个字段的顺序是新手的重灾区,我见过有人把0 2 * * *理解成"每分钟执行一次"的。记住口诀:分、时、日、月、周,前面是短的粒度,后面是长的粒度。每个字段的取值:
| 字段 | 取值范围 | 说明 |
|---|---|---|
| 分钟 | 0-59 | — |
| 小时 | 0-23 | — |
| 日 | 1-31 | — |
| 月 | 1-12 | — |
| 星期 | 0-7 | 0 和 7 都代表周日,1-6 对应周一到周六 |
几个高频写法给你列一下:
*/2 * * * * /path/to/script.sh:每 2 分钟执行一次0 3 * * * /path/to/backup.sh:每天凌晨 3 点执行0 2 * * 1 /path/to/report.sh:每周一凌晨 2 点执行30 22 * * 5,6 /path/to/sync.sh:每周五和周六晚上 10 点半执行0 9 1 * * /path/to/cleanup.sh:每月 1 号早上 9 点执行
需要注意,日和星期字段同时设置时是"或"的关系,不是"与"。比如0 2 15 * 1会被解释成"每周一执行,以及每月 15 号也执行",而不是"每月 15 号且恰好是周一才执行"。很多人在这里翻过车,切记。
4.2 cron 环境变量和日志排查:任务"没执行"不等于"没运行"
计划任务最常见的诡异现象是:脚本手动跑完全正常,放到 crontab 里就不干活了。十有八九是环境变量问题。cron 执行命令时,用的是最精简的环境变量,一是不加载/etc/profile这类交互式 shell 配置,二是不继承你手动登录时的 PATH。这就导致你在命令行能直接用的docker、python3、mysql等命令,在 cron 里可能根本找不到,报 command not found,但这个报错你不在日志里仔细看根本发现不了。
解决办法有两个方向:
- 在 crontab 文件顶部显式声明 PATH,比如
PATH=/usr/local/bin:/usr/bin:/bin,再加SHELL=/bin/bash。 - 或者在脚本开头写死环境变量,比如
source /etc/profile,或者把要用的命令路径卸载脚本里用绝对路径。
第二个常见坑是输出重定向。cron 会把命令的 stdout 和 stderr 通过邮件或者丢弃,你不重定向的话,可能连日志线索都没有。建议每条任务都加上>> /var/log/xxx.log 2>&1,出了问题才有得查。
排查计划任务不执行的标准链路我后面专门讲,先记一个位置:CentOS/RHEL 的 cron 日志在/var/log/cron,Debian/Ubuntu 在/var/log/syslog,搜CRON关键字就能看到每次任务的触发记录。
4.3 at 和 anacron:一次性任务和补跑机制
cron 适合每天/每周/每月固定执行的任务,但如果你只需要"今晚 8 点跑一次",那就用at。
at 20:00 at> /usr/local/bin/do-something.sh at> <Ctrl+D>执行后会提示任务 ID,用atq查看队列,atrm 任务ID删除任务。它内部依赖 atd 守护进程,安装 cron 的时候一般会带上,但记得确认服务是启动状态。
再讲一个很多人不知道的anacron。cron 假设你的机器 7x24 小时开机,但笔记本或者测试机会频繁关机。如果原定凌晨 3 点执行的备份任务因为关机没跑,cron 不会补跑,它错过了就是错过了。anacron 则是专门解决这个问题的,它会记录每个任务上次执行的时间,开机后检查如果有任务错过运行,就在开机后补跑。
Debian 系发行版里/etc/cron.daily/下的任务默认就是由 anacron 代为调用的,所以你发现明明 crontab 里没写每日任务,系统还是会在开机后跑了一堆/etc/cron.daily/,就是这个机制在起作用。理解这一点对排查"为什么我的计划任务在我不知道的时候多跑了一次"很有帮助。
5. systemd timer:更现代的定时任务方案
前面讲的 cron 很经典,但如果你在用 CentOS 7+、Ubuntu 16+ 这些带 systemd 的发行版,我强烈建议你了解一下 systemd timer。它不是要取代 cron,但某些场景下比 cron 优雅得多。
5.1 timer 单元和 service 单元:触发器和执行器的关系
systemd timer 由两个单元文件组成。一个是.timer单元,负责定义触发时间;一个是.service单元,负责定义实际要执行的任务。两者通过文件名前缀关联,比如backup.timer对应backup.service。
在/etc/systemd/system/下创建/etc/systemd/system/backup.service:
[Unit] Description=Daily backup service [Service] Type=oneshot ExecStart=/usr/local/bin/backup.sh再创建/etc/systemd/system/backup.timer:
[Unit] Description=Run backup service daily at 02:00 [Timer] OnCalendar=*-*-* 02:00:00 [Install] WantedBy=timers.target然后:
systemctl daemon-reload systemctl enable backup.timer --nowOnCalendar的语法和 cron 很像但更严格,比如Mon..Fri 02:00:00表示周一到周五凌晨两点。用systemctl list-timers可以查看所有定时器的最近运行时间和下次触发时间,用systemctl status backup.timer可以看状态——这在可观测性上比 cron 强太多了。
5.2 Timer 的精确性和日历事件
cron 的最小精度是分钟,而 systemd timer 支持精确到秒。更重要的是它支持单调时间事件,比如OnBootSec=5min(开机后 5 分钟执行)、OnUnitActiveSec=1h(上次任务执行后 1 小时执行),这种"相对时间"是 cron 完全做不到的。对周期性巡检类的任务,在关机后重新开机时会自动按启动时间重新计算,体验很好。
还有一个实用细节:OnCalendar=可以同时写多行,下面这样表示每天 10:00 和 22:00 各执行一次:
[Timer] OnCalendar=*-*-* 10:00:00 OnCalendar=*-*-* 22:00:00如果你用Persistent=true,它和 anacron 的效果类似,在系统开机后会补跑上次因关机而错过的任务。
5.3 cron 和 systemd timer 的取舍建议
我不会说 systemd timer 全面碾压 cron,它们各有优势。列个对比给你:
| 维度 | cron | systemd timer |
|---|---|---|
| 最小精度 | 分钟 | 秒 |
| 环境变量 | 极简,容易踩坑 | 继承 service 配置,可自定义 |
| 补跑机制 | 不支持,错过就没了 | Persistent=true可补跑 |
| 状态查看 | /var/log/cron翻日志 | systemctl status timer |
| 依赖管理 | 没有内置 | 可以写After=、Requires=依赖 |
| 配置门槛 | 低,一行命令 | 需要写两个单元文件,稍重 |
我的习惯是:临时任务或很简单的任务用crontab -e顺手就写了;需要在开机后补跑、需要精确到秒、或者任务之间有依赖关系的,就用 systemd timer。比如数据库备份这种重要任务,我基本都会写 timer,因为它的失败状态可以用systemctl status一眼看到,配合journalctl -u backup.service排查也非常顺畅。
6. 排障实录:进程和计划任务最常踩的坑
最后这部分,我把几个亲身踩过的坑、以及完整的排查思路写出来。因为这类问题不是知道命令就能解决的,关键的其实是排查路径。
6.1 进程 D 状态 + load average 高:不要先杀进程,先查存储
回到开头那个凌晨告警的场景。我登录后第一件事是uptime,发现 load average 是 35;再看top,CPU 使用率并不高,但有一堆进程的 STAT 列都是 D。这就基本锁定了是 I/O 瓶颈。
下一步排查思路:
iostat -x 1看磁盘%util和await,如果%util接近 100%,说明磁盘在满负荷运转。dmesg -T | tail看内核日志有没有 I/O error、NFS 服务器不响应这样的记录。- 确认了是 NFS 挂载的目录不可达,NFS 客户端在内核态等待远程服务器响应,进入不可中断睡眠。
这时候再怎么 kill 都没用,只能等 NFS 恢复,或者用umount -f强制卸载挂载点,让等 I/O 的进程返回错误并退出。如果你设置的 NFS 挂载参数里有hard,进程会无限期等待;改成soft,timeo=30,retrans=2至少不会让进程卡死。这是很多存储类故障的共性:D 状态通常不是进程本身的问题,而是它依赖的下层资源出了问题。
6.2 计划任务"不执行"的完整排查链路
有次同事喊我:"我明明写了 crontab,凌晨的脚本就是没执行到,手动跑完全正常。"我按下面的顺序排查,几分钟就定位了。你自己遇到同样问题也可以按这个链路走:
- 先排除时间问题:
date查看服务器当前时间、时区是否正确。曾经遇到一台 UTC 时区的服务器,凌晨 2 点执行的任务实际上对应北京时间的上午 10 点,看起来就像"没执行"。 - 确认 crontab 条目确实保存了:
crontab -l查看,注意确认你编辑的是不是当前用户的 crontab。用sudo crontab -e编辑的是 root 的任务,普通用户的定时任务需要自己在自己的账号下写。 - 看系统日志:
grep CRON /var/log/syslog或/var/log/cron。如果看到CMD (xxx.sh),说明任务被调度、命令被启动了,问题出在脚本本身;如果日志里完全没有记录,说明 crontab 里的语法可能有问题,或者 cron 服务没在跑。 - 检查脚本是否真的执行成功:很多任务"没执行"实际上是"执行了但失败了"。确认 crontab 里有没有加日志重定向;如果没有,先在命令行手动异步执行
setsid /path/to/script.sh >> /tmp/xxx.log 2>&1 &,再去看输出。 - 检查脚本文件权限:cron 执行任务时,脚本必须有可执行权限,而且脚本首行的
#!/bin/bash不能少。用ls -l看一眼就能发现。
这个场景里,根因是脚本第一行写了#!/usr/bin/env python3,而 cron 的 PATH 里没有/usr/bin(其实通常有),但env找 python3 时又去翻了其他路径,结果失败。解决方案是在 crontab 顶部写PATH=/usr/local/bin:/usr/bin:/bin,或者直接在脚本里用绝对路径调用解释器。
6.3 计划任务重复执行的坑:小心 cron 和 anacron 双层叠加
还有一种问题是"任务莫名其妙执行了好几次"。这种情况大多是混用了多个调度机制。比如你在 crontab 里设置了每天执行/etc/cron.daily/xxx下的脚本,而系统又通过 anacron 每天晚上自动触发/etc/cron.daily/下的所有脚本,就会造成同名任务被跑两次。
排查方法是先梳理这个任务到底被哪几个机制引用:
crontab -l看当前用户的定时任务;ls /etc/cron.daily/ /etc/cron.weekly/看系统级任务;systemctl list-timers看里有没有对应的 timer 单元。
如果一个脚本既被 crontab 引用,又被 anacron 调用,必须去掉其中一个。我的习惯是把所有业务任务统一放到/etc/cron.d/下以 root 身份管理,并且每个脚本入口加一个flock锁,防止并发执行导致的数据错乱:
#!/bin/bash exec 9>/var/lock/mytask.lock flock -n 9 || exit 1 # 业务逻辑从这里开始flock -n的非阻塞模式会保证同一时间只有一个实例在跑,如果上一个还没跑完,新触发的直接退出。这个技巧对处理"计划任务迟迟没结束、又到了下一个触发点"的场景特别有用。
6.4 善用 /proc 和 systemctl 定位顽固进程
最后分享一个综合性的经验。如果你遇到一个进程反复重启、杀掉一个 PID 又冒出一个新 PID(热搜里提到的"杀了一个 pid,又换了一个"的类似场景),这通常不是进程本身有问题,而是它的父进程是守护进程,会自动拉起子进程。比如 Nginx 的 master 进程挂了会拉起 worker,Java 应用往往也有 supervisor 在管理。
这时的排查思路:
ps -ef找到所有相关进程,重点看 PPID。如果 PID 每次都不一样但 PPID 始终相同,说明元凶是那个固定的父进程。systemctl status PID或者cat /proc/PID/cmdline看看是不是 systemd 在管理它。- 要"斩草除根",不是 kill 子进程,而是停掉父进程对应的 systemd 服务,或者把负责拉起它的守护进程停掉,比如 Supervisord 或 Nginx master。
用 systemd 管理现代 Linux 服务时,systemctl stop会递归处理它的子进程,但在 stop 之前,systemd会先发 SIGTERM,等一段时间再 SIGKILL。如果服务在响应用户命令时卡住,systemctl stop可能也会等很久,此时可以用systemctl kill --signal=SIGKILL --kill-who=all 服务名强制杀掉这个 unit 下的所有进程。这一招几乎每次遇到"杀不完的进程"时都特别管用。
我个人实际操作的体会是:进程和计划任务这两个主题看似基础,但把它们彻底吃透之后,日常运维的故障排查效率会提升非常大。尤其是"先看状态、再想对策"这种思路——遇到 D 状态先查存储,遇到 zombie 先找父进程,遇到 cron 不执行先看日志,顺序对了,问题就解决了一半。最后再提醒一句:给计划任务的脚本里都加上日志,给关键服务写 systemd 单元而不是随便nohup跑,你会在半夜少爬起来几次。