
最近在给机房的一批新服务器做基线配置凌晨两点被监控告警吵醒——一台机器的负载飙到 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里最常见的几种状态状态码含义常见场景RRunning / Runnable正在运行或排队等待运行计算密集型的任务、CPU 被打满时大量 R 状态SSleeping可中断睡眠等待 I/O 完成、等待网络数据包绝大多数进程处于这个状态DUninterruptible Sleep不可中断睡眠等待磁盘 I/O或者内核态某些不可被打断的操作ZZombie僵尸进程子进程已退出但父进程还没收尸没调用 waitTStopped / Traced停止或调试状态按了 CtrlZ、被 gdb 断点抓住的时候IIdle内核线程的空闲状态内核线程如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 / %MEMCPU 和内存使用百分比。注意这个是进程自启动以来的平均值不是瞬时值调优时别被它误导要看就看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 和信号不是只有 -9kill命令的本质是给进程发信号信号种类非常多。日常用得上的最主要是这几个信号编号作用使用场景SIGTERM15请求进程正常终止优雅停机让进程有机会释放资源、保存状态SIGKILL9强制终止进程无法捕获和处理进程无响应时兜底SIGHUP1挂断信号让 daemon 重新加载配置文件比如kill -HUP nginx可以平滑 reloadSIGSTOP19暂停进程执行临时冻结进程一般配合 SIGCONT 使用SIGCONT18恢复暂停的进程和 SIGSTOP 配套很多人一上来就kill -9这是我最想纠正的习惯。SIGKILL 是最后手段因为它不给进程任何清理的机会如果进程正在写配置文件直接 SIGKILL 很可能留下一堆半截的数据。正确的姿势是先kill -15SIGTERM等个几秒确认进程没退再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/systemdPPID 为 1没有控制终端工作目录往往是/标准输入输出重定向到/dev/null或日志文件。要真正理解守护进程必须知道会话Session这个概念。会话是一个或多个进程组的集合当你通过 SSH 登录时系统会为你的登录创建一个新会话你的 shell 就是这个会话的领导者。默认情况下当你退出登录会话终止内核会向这个会话中的所有前台进程发送 SIGHUP 信号进程收到后默认行为是退出——这就是为什么直接./app启动的服务在你断开 SSH 后就挂了。nohup的原理就是忽略 SIGHUP 信号让进程在会话结束时不会被自动杀掉。但nohup不是万能的它只是忽略挂断信号如果你用CtrlZ把进程挂起然后退出或者进程主动和终端断开连接还是可能出各种奇怪问题。更可靠的后台化方案是setsid命令它能创建一个全新的会话让进程彻底脱离当前的终端。不过在我日常的运维实践中现在最推荐的还是用 systemd 的 service 单元去托管进程这个后面讲计划任务的时候一起说。3.3 僵尸进程和进程等待 wait父进程的收尸责任僵尸进程是面试高频题也是线上环境偶发的问题。它的成因其实很简单子进程结束时会向父进程发送 SIGCHLD 信号如果父进程没有调用wait()或waitpid()去读取子进程的退出状态那么子进程的 task_struct 不能被完全释放残留一个尸体在进程表里状态就是 Z。为什么内核不干脆直接把子进程的信息删掉因为父进程可能需要知道子进程的退出码用于判断任务是否成功。所以这个尸体是留给父进程认领的属于合理设计。问题出在父进程不认领。处理僵尸进程的正确顺序是ps -ef找到僵尸进程的 PID 和 PPID。查看 PPID 对应的父进程是什么判断它是否还活着。如果父进程活着先给它发 SIGTERM让它优雅退出。父进程退出后僵尸进程会被 systemdPID 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-70 和 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 21出了问题才有得查。排查计划任务不执行的标准链路我后面专门讲先记一个位置CentOS/RHEL 的 cron 日志在/var/log/cronDebian/Ubuntu 在/var/log/syslog搜CRON关键字就能看到每次任务的触发记录。4.3 at 和 anacron一次性任务和补跑机制cron 适合每天/每周/每月固定执行的任务但如果你只需要今晚 8 点跑一次那就用at。at 20:00 at /usr/local/bin/do-something.sh at CtrlD执行后会提示任务 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] DescriptionDaily backup service [Service] Typeoneshot ExecStart/usr/local/bin/backup.sh再创建/etc/systemd/system/backup.timer[Unit] DescriptionRun backup service daily at 02:00 [Timer] OnCalendar*-*-* 02:00:00 [Install] WantedBytimers.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 支持精确到秒。更重要的是它支持单调时间事件比如OnBootSec5min开机后 5 分钟执行、OnUnitActiveSec1h上次任务执行后 1 小时执行这种相对时间是 cron 完全做不到的。对周期性巡检类的任务在关机后重新开机时会自动按启动时间重新计算体验很好。还有一个实用细节OnCalendar可以同时写多行下面这样表示每天 10:00 和 22:00 各执行一次[Timer] OnCalendar*-*-* 10:00:00 OnCalendar*-*-* 22:00:00如果你用Persistenttrue它和 anacron 的效果类似在系统开机后会补跑上次因关机而错过的任务。5.3 cron 和 systemd timer 的取舍建议我不会说 systemd timer 全面碾压 cron它们各有优势。列个对比给你维度cronsystemd timer最小精度分钟秒环境变量极简容易踩坑继承 service 配置可自定义补跑机制不支持错过就没了Persistenttrue可补跑状态查看/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再看topCPU 使用率并不高但有一堆进程的 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,timeo30,retrans2至少不会让进程卡死。这是很多存储类故障的共性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 21 再去看输出。检查脚本文件权限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 进程挂了会拉起 workerJava 应用往往也有 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 --signalSIGKILL --kill-whoall 服务名强制杀掉这个 unit 下的所有进程。这一招几乎每次遇到杀不完的进程时都特别管用。我个人实际操作的体会是进程和计划任务这两个主题看似基础但把它们彻底吃透之后日常运维的故障排查效率会提升非常大。尤其是先看状态、再想对策这种思路——遇到 D 状态先查存储遇到 zombie 先找父进程遇到 cron 不执行先看日志顺序对了问题就解决了一半。最后再提醒一句给计划任务的脚本里都加上日志给关键服务写 systemd 单元而不是随便nohup跑你会在半夜少爬起来几次。