从终端敲下ps -ef的那一刻,所有藏起来的进程都会现出原形。我入行做运维的头两年,几乎每天都要跟进程和计划任务打交道:服务器无缘无故CPU飙到100%、凌晨的备份脚本没跑、某个Java进程悄悄挂掉又自动拉起来。这些看起来零散的故障,最后都能归结到两个基本功上——把进程看清、把计划任务管好。这篇文章就把我这些年用下来的排查思路和实操命令整理一遍,覆盖进程查看、信号控制、优先级调整、cron与at任务配置,以及高频故障的处理套路。适合刚接触Linux的运维新手,也适合写了多年脚本但没系统梳理过任务编排的开发者参考。
1. 先搞清楚进程的底层逻辑
1.1 程序与进程的区别
很多人刚开始学Linux,会把程序和进程当成一回事。这两个概念差得很远。程序是躺在磁盘上的静态文件,比如/usr/sbin/sshd这个二进制文件,它不会消耗CPU,也不占内存,只是一堆指令和数据。进程是程序被加载到内存之后、正在运行的那一整套状态,包括代码段、数据段、堆栈、打开的文件描述符、环境变量,以及CPU寄存器的当前值。
可以这样理解:程序是菜谱,进程是按照菜谱正在锅里炒的那道菜。菜谱可以同时被多个厨师使用,一个程序也可以被多个用户同时启动成多个进程。比如你开三个SSH窗口登录服务器,就会看到三个sshd进程,它们对应同一个sshd程序文件,但是PID不同、内存空间独立、互不干扰。这种"一份程序、多份进程"的模式,正是多用户操作系统的基本工作方式。
1.2 进程状态与转换
进程在运行过程中不会一直处于"运行中"状态,它会根据是否等待IO、是否被暂停、是否在等待资源,在几种状态之间来回切换。用ps查看时,STAT列显示的就是当前状态。常见的状态码要熟:
| 状态码 | 含义 | 典型场景 |
|---|---|---|
| R | Running或Runnable | 正在CPU上执行,或处于就绪队列等待调度 |
| S | 可中断睡眠 | 等待IO完成、等待网络数据,最常见的状态 |
| D | 不可中断睡眠 | 等待磁盘IO等内核态操作,不能被杀掉 |
| T | 停止 | 被Ctrl+Z暂停,或被SIGSTOP信号挂起 |
| Z | 僵尸状态 | 子进程结束但父进程未回收,残留进程表项 |
| I | 空闲内核线程 | 内核线程的空闲状态,正常现象 |
重点是S和D的区别。S睡眠是可以被信号唤醒的,你kill一个处于S状态的进程通常有效;D状态是进程在等内核IO完成,期间你发任何信号都没用,甚至kill -9也杀不掉。很多运维新手看到kill -9杀不死进程就慌,其实先看状态是不是D,如果是,只能等IO超时或者重启系统,没有别的捷径。
1.3 PID、PPID与进程树
每个进程都有唯一的PID(进程ID),同时还有一个PPID(父进程ID)。几乎所有进程都是由另一个进程通过fork()系统调用创建出来的,父子关系就记录在PPID里。
查看进程树最简单的方法是pstree -ap,它会以树状结构展示父子层级。整棵树的根是systemd(PID为1),所有用户进程追根溯源都是它的后代。排查"这个进程是谁拉起来的",靠的就是PPID这条线索。比如一个可疑的挖矿进程,通过ps -o ppid= -p <PID>找到它的父进程,再顺着树往上捋,通常能追溯到某个被篡改的定时任务或者被入侵的web服务。
僵尸进程也跟父子关系直接相关。子进程结束后会向父进程发送SIGCHLD信号,父进程需要调用wait()系统调用回收子进程的退出状态。如果父进程自身逻辑有问题,没有及时wait,子进程就会残留成Z状态。僵尸进程不占用CPU和内存,但会占着进程表项,堆积太多会导致PID不足,新进程创建失败。
2. 进程查看三板斧:ps、top、htop
2.1 ps命令的花式用法
ps是查看进程快照的老牌命令,但参数组合很容易记混。我建议记住三个组合就够日常使用了:
# 查看当前终端相关进程 ps # 查看所有进程,包含完整命令行,BSD风格 ps aux # 查看所有进程,显示进程树关系,System V风格 ps -ef区别在于ps aux比ps -ef多显示CPU和内存占用百分比,排查资源占用时更直观。我自己最常用的筛选组合:
# 查找特定进程 ps aux | grep java # 精确匹配进程名,排除grep自身 ps aux | grep [j]ava # 按CPU使用率排序,看前10个 ps aux --sort=-%cpu | head -10 # 按内存使用率排序,看前10个 ps aux --sort=-%mem | head -10 # 查看指定PID的详细信息 ps -fp 12345 # 查看指定PID的父进程 ps -o ppid= -p 12345用[j]ava这种写法避免grep进程本身混进结果,是老运维的惯用技巧。另一个容易忽略的点是ps aux里VSZ和RSS的含义:VSZ是虚拟内存大小,RSS是实际驻留物理内存的大小。判断内存占用以RSS为准,公司里两台配置不同的机器可能java进程VSZ都是好几G,不代表真的都吃进了物理内存。
2.2 top与htop的实时监控差异
top是实时刷新视图的标配工具。启动后按下P按CPU排序,按下M按内存排序,按下1展开每个CPU核心的使用率。这三个按键我闭着眼都能按,排查性能问题第一步就是从这里看。
top最大的坑在于默认显示的CPU使用率是总的百分比,如果服务器是16核,一个进程占满一个核,显示可能是6.3%,新手会以为很低。要看单个进程的真实占用,需要按视图里的%CPU列并留意它是否超过100%,超过100%说明进程多线程并行跑在多个核上。这时候用top -Hp PID可以查看该进程内部每个线程的使用情况,定位到具体的线程ID,再配合jstack这类工具查线程栈。
htop是top的增强版,把按键操作改成了快捷键,颜色更丰富,支持树状视图和鼠标操作。它的实用功能是直接在进程列表上按F9发送信号、按F6选择排序字段,还可以按住F4做进程名过滤。不是所有Linux发行版都预装了htop,需要yum install htop或apt install htop,我在生产环境的跳板机上常备一份。
2.3 查端口、查资源占用
运维需求里有一类高频问题:端口被占、某个进程在监听哪个地址。这种场景用ss命令,取代了老旧的netstat:
# 查看所有监听端口 ss -lntp # 查看指定端口的占用进程 ss -lntp | grep 8080 # 查看所有TCP连接 ss -tan-p参数显示占用端口的进程PID和名称,需要root权限才能看到别人的进程信息。注意ss -lntp里的-n表示不解析服务名,直接显示端口号,-t只显示TCP连接。如果你在服务器上明明启动了Nginx,但ss -lntp | grep 80什么都没显示,多半是Nginx只监听了IPv6地址,用ss -lntp | grep :80能同时匹配两种地址。
查看进程打开了哪些文件,用lsof:
# 查看进程打开的文件 lsof -p 12345 # 查看谁占用了某个文件 lsof /var/log/messages # 查看某个目录下被进程占用的文件 lsof +D /var/loglsof在排查"文件删了磁盘空间没释放"的问题时有奇效。删掉的日志文件只要还有进程持有句柄,磁盘空间就不会释放,du看不到文件但是df显示空间已满。用lsof | grep deleted找到占用进程,重启它或者kill它,空间就回来了。这是运维老手都知道的经典坑。
3. 进程控制:信号、优先级与前后台调度
3.1 kill与Linux信号机制
kill命令名字带"杀",但它的本质是向进程发送信号。进程收到不同信号会执行不同的默认动作。常驻的信号表:
| 信号 | 编号 | 默认动作 | 用途 |
|---|---|---|---|
| HUP | 1 | 终止进程 | 常被守护进程重载配置 |
| INT | 2 | 终止进程 | 等价于Ctrl+C |
| QUIT | 3 | 终止并转储 | 生成core文件 |
| KILL | 9 | 强制终止 | 不可被捕获/忽略 |
| TERM | 15 | 终止进程 | 默认终止信号,可被捕获 |
| CONT | 18 | 继续运行 | 恢复被暂停的进程 |
| STOP | 19 | 暂停进程 | 不可被捕获/忽略 |
kill -15是默认操作,发给进程请它自行清理现场并退出;kill -9是强制干掉,内核直接回收,不给程序任何善后机会。凡是用Java、Nginx这类带优雅停机逻辑的程序,能先kill -15就不要直接kill -9,否则数据库连接没释放、临时文件没清理,下次启动可能出问题。
实际工作中还有一个高频场景:改完Nginx配置不想重启服务,因为它正在跑着大量连接,直接重启会闪断。就用:
nginx -s reload # 等价于 kill -HUP $(cat /var/run/nginx.pid)HUP信号被Nginx捕获后重新加载配置文件,保持进程不退出。理解信号原理之后,这类操作就不需要一条条背了,遇到"重载配置""平滑重启"的需求,第一反应就是去找它捕获了哪个信号。
3.2 优先级调整:nice与renice
Linux内核调度器根据进程的nice值决定优先级。nice值范围是-20到19,数值越小优先级越高,普通用户只能把nice调大(降低优先级),root可以把nice调小(提高优先级)。
如果想启动时就指定优先级:
# 以较低优先级运行备份任务 nice -n 10 ./backup.sh # 以较高优先级运行关键服务 nice -n -5 ./redis-server对已经运行的进程调整优先级,用renice:
# 将PID 1234的nice值调整为5 renice 5 -p 1234 # 将属于用户www的所有进程nice值统一调整 renice 5 -u www我的经验是:千万不要为了"./backup.sh跑得快点"把大量进程的nice值调成负数。生产环境只要有一个进程优先级过高,就可能把SSH、监控agent这些基础进程饿死,整台机器完全失去响应。默认nice值0通常已经够用,调优优先级的场景主要是两类:一类是夜间批量处理任务调低优先级避免影响在线业务,另一类是核心数据库进程调高一点点优先级保证响应速度,仅此而已。
3.3 前后台切换与nohup
在终端里执行一个耗时任务,最怕的就是关掉终端任务跟着死。这里要分清两件事:用Ctrl+Z把任务挂起放后台,用的是jobs系列命令;让任务彻底脱离终端的生命周期,靠的是nohup或setsid。
常用的作业控制命令:
# 让命令后台执行 sleep 100 & # 查看当前终端的后台任务 jobs -l # 把后台任务切回前台 fg %1 # 把前台任务暂停(Ctrl+Z)后放入后台 bg %1这里要强调一个容易踩的坑:&只是把命令放到当前shell的后台,命令仍然持有终端的文件描述符,终端关闭时它依然可能收到SIGHUP信号被终止。所以真正可靠的做法是:
# nohup忽略HUP信号,输出重定向到文件 nohup java -jar app.jar > app.log 2>&1 & # setsid让进程独立于会话 setsid python3 worker.py > worker.log 2>&1 &2>&1表示把标准错误也重定向到同一个文件,顺序不能写成2>&1 > app.log。我自己还会加一步确认:执行完nohup后先echo $!记下PID,然后tail -f app.log观察启动情况,确认没有立刻崩溃。
对于需要长期稳定运行的服务,nohup只是临时方案,正规做法是交给systemd托管或者用supervisor这类进程管理器,由它们负责开机自启和崩溃拉起。
4. 计划任务编排:cron、at与systemd timer
4.1 crontab语法与实践
cron是Linux系统里最经典的计划任务服务,它由crond守护进程驱动,每秒醒来检查一次任务表,到点就执行。每个用户有自己的crontab文件,root可以编辑/etc/crontab设置系统任务。
用户级任务用crontab -e编辑,里面每行一条任务,格式是五个时间字段加命令:
# 分 时 日 月 周 命令 */5 * * * * /usr/local/bin/check_disk.sh 30 2 * * * /usr/local/bin/backup_db.sh 0 9 * * 1-5 /usr/local/bin/send_report.py时间字段的含义要熟记:分(0-59)、时(0-23)、日(1-31)、月(1-12)、周(0-7,其中0和7都表示周日)。支持的操作符有:
*:每个取值都匹配,:枚举多个值,比如1,3,5-:范围,比如1-5/:步长,比如*/10每隔10个单位
看几个典型场景的写法:
# 每5分钟执行一次健康检查 */5 * * * * /usr/local/bin/healthcheck.sh # 每天凌晨3点30分执行数据库备份 30 3 * * * /usr/local/bin/backup_db.sh # 每周一至周五上午9点整执行日报生成 0 9 * * 1-5 /usr/local/bin/generate_report.py # 每月1号和15号晚上10点执行清理任务 0 22 1,15 * * /usr/local/bin/cleanup.sh # 每小时的15分和45分各执行一次 15,45 * * * * /usr/local/bin/sync_data.sh新手最容易犯的错是分和时不颠倒。* 2 * * *表示每分钟执行一次、但只在凌晨2点这个小时内每分钟执行一次,如果本意是每天凌晨2点执行一次,应该写成0 2 * * *。这个坑我在早期踩过不止一次,排查看执行日志全乱套了才反应过来。
4.2 crontab管理技巧与日志
系统里所有用户的crontab文件分散在不同的位置,管理起来有套路可循:
# 查看当前用户的定时任务 crontab -l # 查看指定用户的定时任务(需root) crontab -l -u www # 编辑当前用户的定时任务 crontab -e # 删除当前用户的全部定时任务 crontab -r # 从文件导入定时任务 crontab /etc/cron.d/myapp.cron我建议团队协作时把每条crontab里的命令都写成绝对路径,因为cron执行环境里PATH很精简,可能不包含/usr/local/bin。脚本里也尽量不要依赖环境变量,开机加载的.bash_profile不会在cron环境里生效。排查时先看/var/log/cron日志,确认任务是否被调度,再看脚本自己的日志,确认任务是否执行成功。
还有一个容易被忽略的点:cron任务执行时的工作目录是用户的家目录,不是你当时编辑文件时的目录。脚本内部如果用了相对路径,比如./backup.sql,结果可能跟你预期完全不一样。对策是脚本开头统一cd到固定目录,或者所有路径写绝对路径。
4.3 at一次性任务与batch队列
cron适合周期性任务,如果只想执行一次"半小时后做某事",要用at命令。atd服务需要先确认在运行:
# 确认atd服务状态 systemctl status atd # 安排一个一次性任务 echo "/usr/local/bin/restart_nginx.sh" | at now + 30 minutes # 或者交互式输入 at 14:30 > /usr/local/bin/clean_cache.sh > Ctrl+D 结束输入 # 查看待执行任务 atq # 删除某个待执行任务 atrm 5时间格式支持now + 5 minutes、14:30、10:00 2026-01-01等写法。at任务执行时会把标准输出以邮件形式发给用户,服务器通常没配邮件服务,所以别忘了把输出重定向到日志文件。
batch命令是at的兄弟,它是等系统负载降到某个阈值以下才执行任务。我用得不多,但有一种场景很合适:批量导入数据这种IO密集的任务,想趁负载低时自动跑,用batch就不用自己写等待逻辑了。
4.4 systemd timer的现代替代
如果服务器用的是systemd(现在主流发行版基本都是),定时任务还有另一套实现方式:systemd timer。它比cron多了几个明显的优势:
- 可以把任务封装成service单元,统一管理日志、依赖、资源限制
- 支持错过启动补执行(
Persistent=true) - 支持随机延迟,避免多台机器同时执行任务把数据库打崩
一个最小示例,先定义service单元:
# /etc/systemd/system/backup-job.service [Unit] Description=Database Backup Job [Service] Type=oneshot ExecStart=/usr/local/bin/backup_db.sh再定义对应的timer单元:
# /etc/systemd/system/backup-job.timer [Unit] Description=Run backup daily at 3:30 [Timer] OnCalendar=*-*-* 03:30:00 Persistent=true RandomizedDelaySec=120 [Install] WantedBy=timers.target启用方式:
systemctl daemon-reload systemctl enable --now backup-job.timer systemctl list-timersOnCalendar的语法支持daily、weekly、*-*-* 03:30:00、Mon..Fri 09:00:00等写法,比cron的五位字段更可读。项目里我一般这样选型:简单的周期脚本用cron,涉及服务依赖、需要防并发、需要日志集中管理、或者要求错过执行时间必须补跑的任务用systemd timer。系统迁移cron到timer的成本主要在重写执行时间规则,其余都很顺畅。
5. 排障实战:高CPU、僵尸进程与定时任务不执行
5.1 CPU飙高的排查流程
服务器CPU突然100%,最忌讳的做法是直接重启或者随手kill -9。规范的排查路径分四步:
第一步,定位肇事进程:
top -c -o %CPU按CPU排序,记下PID。
第二步,如果是Java这类多线程应用,定位到具体线程:
top -Hp 12345记下耗CPU最高的线程PID。将它转换为十六进制:
printf "%x\n" 23789第三步,用线程号去抓线程栈:
jstack 12345 | grep -A 20 "0x5ced"如果现场不方便执行jstack,也可以用更轻量的方式先看进程在做什么:
strace -p 12345 -c -f -o /tmp/strace.log等几秒后Ctrl+C,查看统计结果,看它卡在什么系统调用上。
第四步,结合日志判断业务逻辑。曾经遇到一个case,某个Python脚本每分钟写一次日志,但日志文件被误删,脚本陷入无限重试,CPU直接爆掉。strace一抓,全是write系统调用反复返回ENOENT,问题一目了然。不要一上来就杀进程,先记录现场再动手。
5.2 僵尸进程与不可中断进程的处理
僵尸进程处理起来有一套固定思路:
# 统计僵尸进程数量 ps aux | awk '$8=="Z"' | wc -l # 找出所有僵尸进程及其PID ps aux | awk '$8=="Z" {print $2, $11}'僵尸进程无法被kill -9清理,因为它在内核里的状态就是"已经死了",只是进程表项没被父进程回收。常规处理办法是按父子关系找源头:
# 查看僵尸进程的父进程 ps -o ppid= -p <僵尸PID>如果父进程是init或systemd(PID为1),说明这个僵尸很快会被系统自动回收,可以不管。如果父进程是业务进程,就要看父进程状态:父进程卡住拿不到信号回收不了子进程,就需要重启父进程来触发系统回收。父进程每次退出,它名下的僵尸子进程会被过继给systemd,由systemd统一清理。
D状态进程(不可中断睡眠)是更麻烦的存在。它通常卡在内核态IO上,比如磁盘故障导致IO请求一直不返回。这时候kill -9无效,只能等IO超时。如果等了很久还是D状态,说明内核跟硬件层的IO卡死了,只能重启系统。生产环境的建议是:确认磁盘硬件健康,对于NFS挂载目录尤其要小心,NFS服务端不可用时,客户端进程很容易全体陷入D状态。
5.3 定时任务不执行的常见原因
定时任务不执行是运维咨询里最频繁的问题,原因不外乎几类,我按出现频率排个序:
第一,crond服务没运行。检查方式:
systemctl status crond第二,权限问题。脚本本身没有执行权限,或者脚本内依赖的命令在cron的PATH里找不到。排查时先把脚本手动跑一遍,用绝对路径执行,确认能成功。
第三,时间字段写错。30 3 * * *和3 30 * * *天差地别,一个每天3点30分执行,一个每小时的3分执行。这类拼写错误在crontab -l里肉眼很难发现,建议把任务跑一段时间后对照日志结论回看。
第四,脚本内部路径问题。脚本里用了相对路径,导致在cron环境下找不到文件。我的固定做法是在脚本第一行加上:
#!/bin/bash cd "$(dirname "$0")" || exit 1这样不管cron从哪个目录启动脚本,工作目录都会切换到脚本所在位置。
第五,任务执行了但报错被忽略。cron会向用户发邮件反馈错误,但多数服务器没配邮件服务,错误信息就丢了。建议所有定时任务都显式重定向输出到日志文件,比如:
30 3 * * * /usr/local/bin/backup_db.sh >> /var/log/backup_db.log 2>&1这样至少留了现场。同时注意脚本执行环境尽量独立,避免多个cron任务同时跑同一个临时文件导致并发冲突。
这类问题排查到最后常常是"小失误"造成的,但一次备份没跑、一次清理没执行,代价可能就是整个目录数据全满。我在项目里会给所有关键定时任务配监控,任务执行失败就告警,宁可多一个告警也别让自己后知后觉。
6. 把这些技巧串成一个可复用的日常操作集
项目里带新人,我常让他们把下面这套命令当成肌肉记忆来练。查进程、看状态、找父子关系、看端口、查计划任务,一套组合下来,大部分"服务器又出问题了"的瞬间判断都能覆盖。
# 进程全景 ps aux --sort=-%cpu | head -20 pstree -ap | head -30 # 实时监控 top -c htop # 端口与句柄 ss -lntp lsof -i :8080 # 信号控制 kill -15 <PID> kill -9 <PID> renice -n -5 -p <PID> # 计划任务 crontab -l systemctl list-timers atq日常维护中我还坚持一个习惯:所有涉及进程调整的操作,先记录当前状态再动手。比如要重启业务进程,先拍下ps截图或者存一份输出;要改crontab,先crontab -l > /tmp/crontab.bak留着。这些习惯在出故障需要回滚时能省出大量时间。
最后分享一个小技巧:排查定时任务时,如果crontab -e里的任务怎么看都没问题,就去检查一下系统时区是否被改过。服务器时区不一致或者时间被NTP校准过,crontab里固定时间点的任务执行情况会和直觉对不上。timedatectl看一眼,再决定要不要怀疑调度本身。这个坑我踩过一次之后,凡是"定时任务偶尔不跑"的case,都会先确认时区和系统时间。