作为一个常年跟 Linux 服务器打交道的运维我几乎每天都要跟“进程”和“任务计划”打交道。很多时候新同事问我为什么服务器负载突然高了为什么某个脚本每天早上会自动跑为什么 kill 掉一个进程又冒出来一个——这些问题绕来绕去其实都逃不开这两个核心概念。这篇内容我就结合自己的实际经验把 Linux 进程管理和任务计划调度这件事从头到尾捋一遍既有原理层面的解释也有可以直接抄作业的命令和思路希望对正在学 Linux 或者刚入行运维的朋友有帮助。1. 先理解进程到底是什么1.1 进程与程序的区别一个是菜谱一个是端上桌的菜我记得刚接触 Linux 的时候总是搞不清程序和进程的区别。后来我用一个做饭的类比才彻底想明白程序是存放在磁盘上的静态文件就像一本菜谱它躺在那里不占多少运行资源而进程是程序被执行后系统为它分配的运行实体就像厨师照着菜谱真的把菜端上了桌这中间涉及灶台、锅铲、食材、火候等一大堆资源的占用。在 Linux 系统里每次你执行一条命令shell 就会通过 fork 系统调用创建一个子进程再通过 exec 系列调用把新的程序加载进这个子进程的内存空间。这也就是为什么我们说 Linux 的进程创建是“forkexec”两段式的。fork 出来的子进程会复制父进程的地址空间、环境变量、文件描述符等然后再被 exec 替换成新的程序。理解这个机制你就明白为什么进程有父子关系为什么会有僵尸进程以及为什么 PID 会被反复复用。1.2 进程的常见状态与生命周期进程不是一直处于运行状态的它在不同阶段会切换状态。最核心的几个状态你一定要记住R (Running)进程正在运行或在运行队列中等待调度。CPU 时间片到了就会从这里切出去。S (Sleeping)可中断睡眠进程在等待某个事件比如等待 I/O、等待网络数据可以被信号唤醒。D (Uninterruptible Sleep)不可中断睡眠通常是在等待内核 I/O 完成。这种状态很难被 kill 掉因为内核不让它在 I/O 完成前被打断。T (Stopped)进程被暂停比如你按了 CtrlZ或者给进程发送了 SIGSTOP 信号。Z (Zombie)僵尸进程子进程已经结束但父进程还没有调用 wait() 来回收它的退出状态。这时子进程的进程描述符还残留在内核里。有一次我排查一台数据库服务器的故障发现系统负载很高但 CPU 使用率却低得离谱。用top一看一堆进程处于 D 状态后来定位到是存储系统出问题磁盘 I/O 完全卡死导致所有读写请求的进程都阻塞在不可中断睡眠里。这种情况你再怎么 kill 都没用只能修底层的存储问题。2. 进程管理的日常操作从查看到终止2.1 查看进程的几大利器ps、top、pgrep、pstree进程查看是我们最频繁的操作几乎每次排障都离不开。最基础的是ps命令但很多新手只会用ps aux然后对着输出发愣。我建议至少记住这么几组用法ps -ef以标准格式显示所有进程包含 UID、PID、PPID、C、STIME、TTY、TIME、CMD 这些列。排查父子关系时这个格式最清晰。ps aux以 BSD 格式显示进程多了 %CPU、%MEM、VSZ、RSS 这些资源占用信息。ps -eo pid,ppid,user,stat,cmd --sort-%cpu自定义字段并且按 CPU 使用率排序能快速找到谁在吃 CPU。pgrep -l 进程名直接通过进程名反查 PID比ps aux | grep更精准而且不会把自己这个 grep 进程也匹配进去。top是动态实时的我一般用它先看全局再按 P 键按 CPU 排序、按 M 键按内存排序。不过top的输出在脚本里不好处理所以我写脚本或者远程排查时更多用ps配合awk。还有htop如果你装了这个工具颜色高亮加上树状结构看着会舒服很多尤其是分析线程和进程树的时候。pstree -ap也是很实用的命令它能把整个进程的家族谱系画出来。有一次用户报告说有进程删不掉我一跑pstree发现那个进程的父进程是 initPID 1说明它已经成了孤儿进程被 init 收养了。这种情况下排查它的来源就得去看它启动时的日志或脚本而不是单纯 kill 这么简单。2.2 前台后台切换CtrlZ、jobs、bg、fg很多人在命令行启了个耗时任务结果终端被卡住什么都干不了只能再开一个窗口。其实 Linux 本身就支持任务的前后台切换。你运行一个长时间命令时按 CtrlZ 可以把它暂停然后输bg让它到后台继续跑或者直接输fg把它拉回前台。jobs -l能看到当前 shell 管理的所有后台任务及其 PID。比如我经常要跑一个数据导入脚本时间很长用 CtrlZ 暂停bg转到后台然后我继续做别的操作。但要注意这种后台任务跟你的终端会话是绑定的如果你直接关掉终端任务可能会被挂断。要真正脱离终端独立运行得用nohup或者setsid或者干脆到后面讲的任务计划里去跑。这里有个小坑nohup command 虽然能把进程放到后台且不挂断但它默认会把标准输出重定向到 nohup.out 文件里如果你忘了重定向这个文件会一直膨胀。所以我一般会写成nohup command /var/log/xxx.log 21 不但日志位置可控还能保留错误输出。2.3 kill 的学问进程信号与安全终止方式kill命令是每个学 Linux 的人都会用的但大多数人只会用kill -9这其实是个很危险的习惯。kill的本质是给进程发送信号不同的信号有不同的含义和行为kill -15SIGTERM默认信号请求进程正常退出进程可以捕获这个信号去做清理工作比如关闭文件、释放资源这是最温柔的方式。kill -9SIGKILL强制杀死进程内核直接终止它不给进程任何清理机会。如果一个进程正在写数据库用 -9 可能导致数据损坏。kill -1SIGHUP让进程重新加载配置很多守护进程会监听这个信号来做热更新比如 nginx 和 sshd。所以“平滑重载”其实很多时候就是用 kill -HUP 实现的。kill -18SIGCONT让暂停的进程继续运行bg和fg底层就是用这个信号。kill -19SIGSTOP暂停进程不能被捕获相当于命令行的 CtrlZ。我在生产环境的原则是先尝试kill -15等几秒看进程是否退出如果确实是死循环或者卡死的进程才用kill -9兜底。还有个细节kill -9对处于 D 状态不可中断睡眠的进程是无效的因为内核根本不会去处理该信号直到 I/O 完成。所以你不必反复去 kill 一个 D 状态的进程先从存储和 I/O 层面找原因。2.4 僵尸进程的识别与清理思路僵尸进程是我见过新手最容易恐慌的问题。从ps输出看到 STAT 列是 Z就以为系统要崩了。其实僵尸进程本身不占 CPU 也不占内存它就是一个“残留的退出状态记录”等着父进程来收尸。真正的危害是如果父进程一直不调用 wait()僵尸会堆积而内核的 PID 数量是有限的PID 耗尽了系统就没法创建新进程。清理僵尸进程的思路要顺着父进程来找到僵尸进程的 PPID父进程 ID。检查父进程是做什么的如果是 init/systemdPID 1那通常问题不大init 会周期性回收孤儿进程。如果是某个应用程序的子进程一般通过重启那个父进程就能让内核回收所有僵尸。如果父进程本身就是个写得很烂的程序不处理子进程退出事件那就得考虑修代码在父进程里调用 wait/waitpid或者注册 SIGCHLD 信号处理函数。我在实践中发现很多僵尸进程的根源是“父进程先挂了子进程变成孤儿被 init 收养”这种反而不容易积累因为 systemd 会接管并回收。真正棘手的恰恰是父进程存活但不回收子进程的场景比如某些 Java 应用没正确处理子进程退出码。这种时候最彻底的办法就是重启该应用的服务一言以蔽之杀僵尸不如杀它的爹。3. 深入进程调度优先级、CPU 亲和性与资源控制3.1 nice 值与优先级为什么有的进程抢不到 CPULinux 内核用完全公平调度器CFS来分配 CPU 时间它依据的不是绝对的优先级数值而是虚拟运行时间。每个进程有一个 nice 值范围是 -20 到 19nice 值越低代表“越友好地抢占 CPU”默认是 0。这里特别容易搞反nice 值越小优先级越高nice 值越大反而越谦让。你可以用nice -n -5 命令来以更低 nice 值启动进程也可以用renice -n 5 -p PID来调整运行中进程的优先级。但要注意普通用户只能调高 nice 值更谦让只有 root 才能调低 nice 值更抢占。我记得有次给一台 web 服务器做优化有个统计脚本每隔几分钟就跑一次全表扫描把数据库 IO 吃得很厉害导致用户请求变慢。我没有去改脚本的业务逻辑而是直接给它的进程设置了renice 10让它变得“更谦让”这样碰到业务高峰时它自动把 CPU 让出来效果立竿见影。在资源受限的环境里用 nice 值来约束非关键任务是成本最低的调度手段。3.2 进程与线程top 里的 %CPU 为什么会超过 100很多人在top里看到某个进程的 CPU 占用率超过 100%立刻就觉得不对。其实这完全正常因为 Linux 的 top 默认展示的是进程内所有线程的累计 CPU 使用率。现代 CPU 多核场景下一个多线程进程确实可以同时跑在多个核心上。你在 top 界面按 H 键可以看到具体线程的 CPU 分布对照 Java 的jstack或jstat就能定位到是哪条线程在作妖。进程和线程的关系可以理解成一个公司进程和里面的员工线程进程是资源分配的最小单位有独立的内存空间、文件描述符等线程是 CPU 调度的最小单位共享进程的资源。Linux 下的线程是用轻量级进程LWP实现的所以在 ps 里你看到的很多 PID 其实是线程 IDTGID 和 PID 概念微有差别这一点排查多线程应用时要格外注意别拿线程 ID 直接去 kill 整个进程。3.3 使用 taskset 与 ulimit 做基础资源管控如果某个进程对特定 CPU 核有要求比如网卡多队列绑定或者实时任务处理可以用taskset -c 0,1 命令把它绑到指定的核心上避免频繁的上下文切换。虽然现代内核调度器已经很聪明了但有些延迟敏感的场合手动绑核还是有价值的。另一方面ulimit能限制当前 shell 及子进程的资源使用比如ulimit -c 0可以关闭核心转储文件防止系统磁盘被 dump 塞满ulimit -n 65535可以提高文件描述符限制。这里我要提醒一下ulimit是会话级的你永久修改得写到/etc/security/limits.conf或者 systemd 服务文件里的LimitNOFILE仅仅在命令行执行那只是临时生效。4. 任务计划管理一次性任务与周期任务4.1 一次性任务 at适合“今天下午三点跑一次”有些任务只需要在某个特定时间点执行一次这用 crontab 反而不合适因为 cron 是周期性的。这时候用at更顺手。安装 at 服务后不同发行版包名略有不同你可以这样用echo /opt/scripts/backup.sh | at 15:00或者交互式输入at 22:30 at /opt/scripts/clean_logs.sh at EOTatq查询待执行的任务队列atrm 任务编号删除某个任务。实际工作中我用 at 最多的场景是深夜临时维护前提前把停机脚本排到凌晨两点或者某个一次性迁移任务在业务低峰自动执行一次。注意 at 的守护进程叫 atd如果命令提示没有 at 服务先确认服务是否在运行。4.2 cron 的配置格式五个星号的秘密cron 是 Linux 里最常用的周期任务工具几乎每个运维的服务器上都跑着一堆 cron 任务。它的配置格式非常紧凑分、时、日、月、周五个字段。我见过很多新手把顺序记反这里给个记忆锚点从最小的时间单位到最大的时间单位去理解依次是分、时、日、月、周。一个典型的 crontab 条目长这样30 2 * * * /opt/scripts/daily_backup.sh /dev/null 21意思是“每天凌晨 2 点 30 分执行备份脚本并丢弃所有输出”。字段里还可以用*/5表示每 5 分钟0 9-18/2表示 9 点到 18 点之间每 2 小时一次1,15,30表示多个具体值。这里要特别强调cron 的最小粒度是分钟没有秒级任务。如果你确实需要秒级调度要么用脚本内部循环要么用 systemd timer后面会讲不要硬去 hack crontab。编辑当前用户的 crontab 用crontab -e查看用crontab -l删除用crontab -r这命令很猛不建议轻易敲。系统级的计划任务放在/etc/crontab或者/etc/cron.d/目录下与用户 crontab 的区别是系统级需要多指定一个执行用户比如30 2 * * * root /opt/scripts/daily_backup.sh4.3 环境变量陷阱为什么 cron 里执行的脚本会报错这是我踩过最多坑的地方必须单独拿出来说说。cron 执行任务时它使用的环境变量跟你手动登录 shell 相比极其精简PATH 通常只有/usr/bin:/bin很多你平时直接能用的命令比如 /usr/local/bin 下的工具在 cron 里就是“command not found”。另外像 JAVA_HOME 这类变量在 cron 环境中根本没有。解决方法也很简单在脚本开头显式导出环境变量或者直接用绝对路径调用命令。我写脚本的习惯是开头先写#!/bin/bash export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin export JAVA_HOME/usr/local/java这样脚本不管在手动执行还是在 cron 环境里行为都是一致的。还有一个相关的问题是cron 执行任务时它的工作目录是执行者的家目录而不是脚本所在目录。如果你的脚本里用了相对路径引用其他文件很容易找不到文件。所以脚本内部尽量用绝对路径或者先在脚本开头cd到固定的工作目录。4.4 日志与输出管理别让 cron 邮件塞爆你的磁盘默认情况下cron 会把任务的输出通过邮件发给用户。如果你的服务器没配邮件服务这些内容会堆积在本地 mail spool 里久而久之是个隐患。所以我建议在执行命令或脚本时把标准输出和错误输出都重定向到日志文件或者直接丢弃30 2 * * * /opt/scripts/daily_backup.sh /var/log/daily_backup.log 21这里21表示把标准错误也重定向到标准输出这样错误信息也能进日志。如果是调试阶段可以先不重定向到文件让它直接发邮件给 root快速在 mail 里查看错误信息。上线稳定之后再改成落盘日志是更稳妥的思路。排查 cron 任务为什么不执行我有三个切入点第一看/var/log/cron或journalctl -u crond的日志确认调度是否触发第二检查 crontab 里写的脚本是否有执行权限第三手动执行脚本看是否因为缺环境变量或依赖而中途挂掉。按这个顺序走百分之九十九的问题都能定位。5. 进阶systemd timer 与现代任务管理5.1 为什么我用 systemd timer 替代了一部分 cron近几年新装的发行版都是 systemd 体系systemd 自带的 timer 单元其实是一个非常现代的任务计划方案。它的优势在于跟服务的依赖关系、日志收集journald、失败重试策略都可以统一管理比纯 cron 脚本要规范很多。举个例子我要每天凌晨 3 点执行一次日志清理可以写一个 service 单元[Unit] DescriptionClean old logs [Service] Typeoneshot ExecStart/opt/scripts/clean_logs.sh再写一个对应的 timer 单元[Unit] DescriptionRun clean logs daily [Timer] OnCalendar*-*-* 03:00:00 Persistenttrue [Install] WantedBytimers.target然后执行systemctl daemon-reload和systemctl enable --now clean_logs.timer任务就生效了。Persistenttrue的含义是如果系统在计划时间点处于关机或休眠状态下次开机后会补执行错过的任务这是 cron 默认做不到的。我用 systemd timer 最顺手的一点是通过journalctl -u clean_logs.service能直接看脚本运行的完整日志不用再额外配日志文件路径排障体验好太多。如果你追求标准化、可监控的系统级任务认真做好迁移到 systemd timer 是值得投资的方向。5.2 在脚本中实现等待与并发控制任务计划并不只是“到了时间就执行”这么简单很多时候脚本里面还有大量细节要处理。比如脚本内部要等待某个后台任务完成后再做下一步最简单的就是wait命令/opt/scripts/backup_db.sh BACKUP_PID$! /opt/scripts/backup_files.sh FILE_PID$! wait $BACKUP_PID wait $FILE_PID echo All backups are done$!是上一个后台进程的 PIDwait会阻塞直到对应进程结束。这个机制可以让你在 shell 里实现简单的并行与汇合配合set -e和trap还能控制出错时的退出行为。另外任务计划中经常出现的问题是上一次没跑完下一次又启动了两个实例互相打架。这时候可以用 PID 文件pidfile做单实例控制PIDFILE/var/run/my_task.pid if [ -f $PIDFILE ]; then PID$(cat $PIDFILE) if kill -0 $PID 2/dev/null; then echo Another instance is running, exit. exit 1 fi fi echo $$ $PIDFILE trap rm -f $PIDFILE EXITkill -0不是发信号给进程而是探测进程是否存在的一个技巧非常实用。写计划任务脚本前先想好并发问题能省掉很多半夜被电话吵醒的情况。6. 常见故障排查与实用经验总结6.1 系统负载高的排查路线服务器负载高load average 很大是运维最常遇到的事但负载高不等于 CPU 忙。load average 实际是处于可运行状态和不可中断状态的进程平均数量。所以我排查的思路通常是先用top或uptime看 load 值和 CPU 使用率判断是 CPU 密集还是 I/O 密集。用top按 CPU 排序再用ps -eo pid,ppid,stat,wchan看进程在等什么内核函数。用iostat -x 1看磁盘 I/O 的 util 和 await 指标确认是否有存储瓶颈。用vmstat 1看 r 队列运行队列和 b 队列阻塞队列这两个数字直接反映进程调度压力。有一次 MySQL 实例 CPU 使用率不高但负载很高最后发现是 swap 在频繁读写内存严重不足导致进程状态频繁切换。那次的教训是看负载一定要结合内存和 swap 一起看单看 load 数值有欺骗性。6.2 进程杀不掉和端口被占用的解决实录“端口被占用”这个问题新手上线时几乎都会遇到。定位方法就是用ss -lntp或者lsof -i:8080找到占用端口的 PID然后按情况处理。比较恶心的是有些进程是双开的你杀了一个 PID另一个又起来了。这种情况一般是因为有守护进程在自动拉起比如 Java 应用用了 supervisor或者 systemd 设置了 Restartalways。你需要先停守护再杀进程否则就是野火烧不尽。还有一次我遇到某个进程 kill -15 之后一直退不掉ps看状态变成 S睡眠中等了几分钟都没反应。后来发现是它的线程阻塞在一个 FIFO 的读写操作上没有任何超时机制。这种代码层面的问题从运维侧只能找业务方配合改代码或者临时用 kill -9 兜底但要提醒业务方会有数据不一致的潜在风险。6.3 我的常用进程与任务管理命令速查表场景命令说明查看所有进程ps -ef标准格式含 PPID按资源排序ps aux --sort-%cpuCPU 占用降序动态监控top/htophtop 更友好按名称找 PIDpgrep -a nginx避免 grep 自匹配发送优雅终止kill -15 PID默认信号强制终止kill -9 PID最后手段暂停/恢复kill -STOP PID/kill -CONT PID对应 CtrlZ 与 bg/fg调整优先级renice -n 10 -p PID普通用户只能调高一次性任务at 15:00需要 atd 服务周期任务crontab -e编辑当前用户任务现代定时器systemctl list-timers查看全部 timer查看日志journalctl -u 服务名systemd 统一日志这个表算是我日常工作中最常用的浓缩版新手可以照着练手。特别提醒一下pkill和killall这类按名字批量杀进程的命令在生产环境用的时候一定要非常小心因为名字匹配的规则可能误杀无关进程比如pkill java会杀掉所有 Java 进程如果机器上同时跑着多个业务那场面会很酸爽。6.4 从“能用”到“好用”任务计划的几个最佳实践任务计划看着简单但要把几百台服务器的计划任务管明白还是有一些值得坚持的习惯所有脚本统一目录比如/opt/scripts/并做好命名规范。不要今天放 /root 下一个脚本明天放 /home 下一个脚本排障时要找半天。统一日志策略每个计划任务的日志输出固定到/var/log/cron/下对应文件并按日期滚动。我自己的做法是脚本里自己处理日志循环避免单文件无限增长。加执行结果通知重要任务的脚本执行完通过钉钉或企业微信机器人推送一条状态消息。以前我半夜被叫起来看任务是否成功后来做了通知推送省心太多了。权限最小化crontab 里尽量用专用系统账号而不是直接用 root。如果确实需要 root脚本内部做好精细的权限控制降低被滥用的风险。脚本幂等性确保同一个任务重复执行多次结果是一致的不会因为上一次没有清理干净导致这一次出错。幂等性在设计脚本时就要考虑而不是事后打补丁。说实话进程管理和任务计划管理这两块内容是 Linux 运维里的基本功但基本功扎实了很多上层架构问题都能迎刃而解。我在处理线上故障时百分之六十以上最后都会归结到“某个进程异常”或者“某个计划任务没按预期执行”。把这些命令和思路练到条件反射的程度遇到任何机器异常都能快速定位不会一头雾水。最后再分享一个我个人的习惯每周抽十分钟用ps -eo pid,ppid,user,stat,etime,cmd --sort-etime | head -50看看服务器上运行时间最长的一批进程是哪些。这个操作能帮你确认有没有漏掉的旧版本应用、有没有没人管的残留进程。很多安全隐患就是这样被提前发现的别等到它们出问题才追着日志到处查。