如果有人问我 Linux 性能排查里最容易被低估的机制是什么,我的答案会是进程优先级。之前我在一台编译服务器上遇到一个怪现象:线上服务在固定时间点延迟飙升,CPU、内存、IO 全部查过都正常,后来才发现罪魁祸首是一个没有设置好优先级的日志备份进程。它一启动就大量抢占 CPU 时间,把业务进程挤到了后面。找到原因之后我用renice把备份任务降级,负载立刻恢复了平静。从那以后我把优先级这一块做了系统整理,这篇就是当时的学习笔记。
这份笔记适合所有刚开始接触 Linux 系统运维、或者已经在用 top 但不太理解 NI 列含义的读者。我会先讲清楚优先级到底是什么,再给出一套可直接操作的命令方法,接着深入内核看调度器怎么处理这些数字,最后分享几个真实踩坑现场。内容不涉及高深的内核代码分析,更偏向于"理解机制、熟练使用、遇到问题能排查"。
1. 先搞清楚进程优先级到底是怎么回事
1.1 Linux 里其实有两套优先级体系
很多人第一次接触优先级是从ps -l输出里的NI列开始的,比如看到 NI 是 0,下意识觉得这代表"没有优先级"。实际上 Linux 的优先级不是一个简单数字,而是两套体系叠加在一起。
第一套是普通进程的 nice 值,范围是 -20 到 19,默认 0。数值越小优先级越高,数值越大优先级越低。之所以叫 nice,可以理解为"这个进程有多友好":nice 值越高,越愿意把 CPU 让给别人,所以是"更 nice"。第二套是实时进程的优先级,范围是 1 到 99。实时进程在设计上就是为了处理那些不能被延迟的关键任务,比如音视频采集、工业控制,它们拥有对 CPU 的抢占特权。
举个生活化的例子:普通进程就像大家在食堂排队打饭,nice 值决定你愿不愿意往后让位;实时进程则是有专属窗口的 VIP 卡用户,只要他到场,普通队列的人就得等着。这里有个最容易搞反的点——数值越小优先级越高,和大多数人"分数越高越好"的直觉正好相反。
另外,一个进程必须属于某种调度策略。调度策略才是优先级的上层框架,常见的有SCHED_OTHER、SCHED_BATCH、SCHED_IDLE、SCHED_FIFO、SCHED_RR。前三种对应用户日常启动的大部分进程,后两种用于实时任务。你可以用chrt -p <pid>查看进程当前的调度策略,输出会明确显示是哪一种。
1.2 从创建到运行:优先级是怎么决定"谁先用 CPU"的
当一个进程被创建时,子进程会继承父进程的优先级参数。也就是说,如果你在 shell 里启动任务,shell 的 nice 值就是父进程的 nice 值,默认是 0。如果一个脚本内部又拉起了多个子进程,这些子进程会沿用同样的 nice 值。
但是"继承"不等于"永远固定"。进程创建后仍然可以通过renice修改 nice 值,也可以通过chrt改变调度策略和实时优先级。运行中的进程和未运行的进程在优先级上最大的区别是:未运行的进程在 fork 之后、初次被调度之前就固定了静态优先级;运行中的进程则随时可以动态调整。
调度器的选择过程可以简化为三步:先从所有可运行的任务里找到属于实时类的任务,根据实时优先级按序执行;实时任务全部结束后,才轮到普通进程;普通进程之间,根据 nice 值换算出的权重来分配 CPU 时间。这套先后关系是理解后面所有命令和问题的基础。
1.3 优先级不是"越快越好",而是"合理的资源分配"
我需要特别强调一点:把进程优先级调到最高,不代表它就一定跑得更快。因为付出 CPU 时间之后,进程能否快起来受制于锁、IO、网络等资源。比如一个程序在等待数据库响应,就算给它 99 的实时优先级,它也只能干等。
优先级的本质是资源调度策略:当 CPU 不够用时,决定谁先获得时间片、谁获得更多时间片。CPU 充足时,即便是 nice 值为 19 的进程,也能拿到完整的时间片跑完任务。我之前做过一个简单测试,两个循环计算程序,一个 nice 是 -5,一个 nice 是 5,在单核机器上跑同样的任务量,差距确实明显,但一旦空闲核数超过任务数,两者几乎同时完成。所以调优优先级前先确认 CPU 是否真的是瓶颈,否则就是瞎忙。
2. 命令行里操作优先级的完整笔记
2.1 启动进程时指定 nice 值
如果你在运行一个耗时任务,并且担心它影响业务,最好在启动阶段就设置好 nice 值,语法很简单:
nice -n 10 ./backup_script.sh-n后面跟的数值就是期望的新 nice 值。也可以直接写nice -10,这种写法容易和负数混淆,建议还是用-n参数更加清晰。如果脚本里需要指定 CPU 亲和性,可以和taskset结合使用:
taskset -c 2,3 nice -n 10 ./backup_script.sh这条命令把进程绑定到 CPU 2 和 3 上,同时设置 nice 值为 10。对于需要长时间占用 CPU 的批处理任务,这样组合使用比单设 nice 更有效,既能降低抢占,又避免进程在不同核心间迁移带来的缓存损耗。
启动时设置 nice 的好处是进程从一开始就用降级参数运行,不会出现"先抢占了临界区再降级"这类尴尬情况。但有一个限制:普通用户只能调高 nice 值,也就是往正数方向调,不能往负数方向调。比如你执行nice -n -5 ./app,系统会提示权限不足。这是内核层面的安全设计,防止普通用户通过调低优先级来恶意抢占系统资源。
2.2 进程启动后修改与查询优先级
进程已经在跑了,这时候需要用到renice。比如发现一个 PID 为 1234 的数据库备份进程占了很多 CPU,你想把它降下来:
renice -n 10 -p 1234执行完显示1234 (process ID) old priority 0, new priority 10,说明旧的 nice 值为 0,新的 nice 值为 10。如果你要同时调整多个进程,可以用-p跟上多个 PID,或者用-g指定进程组、-u指定用户:
renice -n 5 -u www-data这里我认为更实用的做法是,先通过pgrep找到符合条件的进程,确认 PID 之后再做调整,避免误伤同类进程。比如批量调整某个脚本相关进程:
pgrep -f backup_task | xargs renice -n 10 -ppgrep -f会匹配完整命令行,比直接按进程名匹配更准确,特别是在有多个同名 Python 脚本但参数不同的时候。
查看优先级的入口有三个,我个人最常用的是ps和top。ps -el能看到系统的完整进程表,其中NI列就是 nice 值:
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 0 S 1000 2345 2311 0 80 0 - 1024 pipe_w pts/0 00:00:00 bashPRI列是内核视角的优先级,对于普通进程,PRI实际等于nice + 120,比如 nice 为 10 时 PRI 显示 130。top交互界面按r可以交互式修改进程 nice 值,但在自动化脚本或排查现场,我更推荐直接用ps -o pid,ni,comm精确取值:
ps -eo pid,ni,comm | grep backup还有一个容易忽略的入口是/proc/<pid>/stat,里面的prio字段是动态优先级,nice字段是静态优先级。某些监控脚本会直接解析这个文件,但在日常命令行操作时,ps -el的输出已经足够。
2.3 systemd 服务如何设置优先级
现在的 Linux 发行版大量使用 systemd 管理服务,如果你写了一个自定义服务,希望在启动时固定 nice 值,可以不在 ExecStart 里写nice,而是直接在 service 配置里声明:
[Service] ExecStart=/opt/scripts/worker.py Nice=10对于需要实时调度的服务,还可以加:
CPUSchedulingPolicy=rr CPUSchedulingPriority=40CPUSchedulingPolicy支持other、batch、idle、fifo、rr几种值。设置完成后执行systemctl daemon-reload,再systemctl restart对应服务。这个配置对于部署在容器或者虚拟机上跑定时任务的场景尤其有用,不需要在业务列表里隐藏一行nice命令,运维人员一眼就能看出服务的 CPU 策略是什么。
3. 内核里的优先级运行机制
3.1 nice 值到权重的映射表
理解机制之前,先看一张内核的映射关系表。Linux 的 CFS 调度器不是直接拿 nice 值做算术,而是通过一个权重表来换算。内核源码sched/core.c中定义了sched_prio_to_weight数组,每个 nice 值对应一个权重数,下面是摘取的几个代表值:
| nice 值 | 内核权重 | 相对 nice 0 的份额比例 |
|---|---|---|
| -20 | 88761 | 约 15.38 倍 |
| -10 | 7721 | 约 1.25 倍 |
| -5 | 3355 | 约 0.77 倍 |
| 0 | 1024 | 1 倍 |
| 5 | 335 | 约 0.31 倍 |
| 10 | 110 | 约 0.10 倍 |
| 19 | 15 | 约 0.014 倍 |
从这个表能看出来,每下降一个 nice 值,权重近似增加 1.25 倍,所以 nice 值每差一级,在同样负载下 CPU 时间大概差 10% 左右。最极端的 -20 和 19 之间,权重差了上万倍。这意味着如果两个计算密集型进程竞争一个 CPU,一个设为 -20,一个设为 19,后者几乎得不到运行机会。
有了这张权重表,你就明白为什么nice 19的进程不一定会完全卡死,它只是分到的 CPU 时间极少。在 CPU 空闲的机器上,权重为 15 也能完整执行完一个任务,只是运行速度会比较慢。
3.2 CFS 调度器怎么使用这个权重
Linux 默认的普通进程调度器是 CFS(完全公平调度器)。它的设计哲学不是给每个进程固定时间片,而是维护一个虚拟运行时间vruntime。进程运行时会累积 vruntime,增长的速度由权重决定,权重越高,vruntime 增长越慢。
每次需要选择下一个运行的任务时,CFS 就挑 vruntime 最小的进程。这样从数学上看,权重高的进程虽然在相同时间片里 vruntime 增长慢,但在未来很长一段时间里会获得更多实际运行时间,从而实现了"按权重比例分配 CPU"的效果。这也解释了为什么 nice 值不是排队插队,而是分蛋糕的比例。
内核内部把 nice 值映射成static_prio,普通进程的内部优先级范围是 100 到 139,对应 nice 值 -20 到 19。一百多的数字和实时优先级 1 到 99 统一在一个坐标系里,调度器比较时就能一眼看出实时任务优先于普通任务。
实时调度类则完全不同。SCHED_FIFO是先进先出,只要实时进程可以运行,它就持续占用 CPU,普通进程只能在它主动让出 CPU 或者被更高优先级的实时任务抢占时才有机会。SCHED_RR在 FIFO 基础上增加了时间片轮转,允许同优先级的多个实时任务轮流执行,但依然压制普通任务。
3.3 实时优先级为什么这么"霸道"
实时优先级是 1 到 99,数值越大优先级越高,这和 nice 值是反着的。这个设定经常把人绕晕,但内核的实时调度器就是这么比较的。默认情况下,如果某个 CPU 上有实时任务在运行,普通任务完全没机会。为了不让实时任务把系统饿死,内核提供了两个参数来限制实时任务的总占用率:
/proc/sys/kernel/sched_rt_period_us /proc/sys/kernel/sched_rt_runtime_us默认周期是 1 秒,实时任务的运行时间上限是 0.95 秒,也就是说实时任务最多占用 95% 的 CPU 时间,剩下 5% 留给普通任务。这 5% 看起来不多,但足够让系统管理命令和 SSHD 之类的基础服务喘口气,避免出现"机器彻底失联"的极端情况。
但要注意,这个限制是一个核一个核单独计算的。如果你有 8 个核,8 个实时任务把每个核都吃满 95%,普通任务的调度会被严重挤压,交互式操作会卡到令人崩溃。所以我一直强调,普通业务场景下完全没有必要使用实时调度策略,大多数时候设置一个合适的 nice 值就够了。
4. 实战常踩的坑与排查实录
4.1 普通用户为什么调不了负 nice 值
这个坑在我刚开始做运维时踩过。当时我想把一个数据处理脚本的优先级调高一点,执行命令:
renice -n -5 -p 1234系统直接返回Permission denied。一开始我以为是 PID 输错了,后来查资料才发现这是内核有意为之:如果任何用户都能把进程优先级调到最高,那么恶意脚本完全可以把系统资源占满,其他进程什么都干不了。所以 Linux 规定,只有 root 用户才能把 nice 值往负数方向调整,普通用户只能往正数方向调整。
这个设计其实很符合直觉——调低优先级是"利他",允许任何人做;调高优先级是"利己",必须要有权限。如果你确实需要把某个普通用户的任务调成高优先级,两个办法:让管理员执行,或者配置 sudoers 白名单。但不建议随便给普通用户 root 权限,哪怕只是给个别命令放开,也要评估风险。
4.2 把进程改成实时优先级后系统卡死的翻车现场
我想重点说说实时优先级的实验风险。之前在一台多核测试机上,我为了验证一个音频采集程序的低延迟表现,直接把它的调度策略改成了SCHED_FIFO,优先级设成 99:
chrt -f 99 -p <pid>结果程序本身是一个计算密集型的循环,它一跑起来,所在 CPU 核心被完全占满。虽然 RT throttling 会留下 5% 的时间给普通任务,但这些时间分到每个普通进程头上寥寥无几,我的 SSH 会话变得极度卡顿,几乎敲一个字符要等好几秒。最后只能通过另一台机器强制 kill 进程才恢复。
这个案例给我的教训是:实时优先级 99 只适合经过严格测试、确定不会长期占满 CPU 的短任务。普通的 web 服务、数据库、批处理脚本都是绝对不碰它的。如果只是想降低延迟,先用 nice 值 -10 或 -5 测试,配合 CPU 绑核观察,不要一上来就上chrt。
4.3 autogroup 机制会悄悄干扰你的 nice 调整
这是最容易让人费解的问题。某些机器上你执行了renice -n 10 -p <pid>,用ps查看也就绪了,但进程的实际 CPU 占用率没有下降。原因很可能是autogroup(自动进程组)在起作用。
现代内核默认开启 autogroup,把同一个会话下的进程自动归于一个组,调度器先按组分配 CPU 时间,再在组内按各进程的 nice 值分配。当你在一个终端里启动多个任务时,它们属于同一个 autogroup,整体共享一个 nice 值。你单独修改组内某个进程的 nice 值,影响的是它在该组内部时间里的份额,不会改变整个组的核外份额,所以从外部看起来效果不明显。
解决办法是直接调整 autogroup 的 nice 值。autogroup 的目录挂在/sys/kernel/debug/sched/autogroup/下,里面每个会话对应一个目录:
echo 10 > /sys/kernel/debug/sched/autogroup/autogroup-100/nice或者更粗暴地关闭 autogroup,在启动参数里加上noautogroup。我个人不建议关闭,因为 autogroup 对不同终端会话做了很好的隔离。调试时先确认是不是 autogroup 影响了效果,再决定是调整组 nice 还是关闭它。
4.4 排查优先级问题的三个实用场景
第一名场景是"CPU 使用率暴涨,不知道是谁干的"。我一般用top按 CPU 排序,立刻看有几个进程在抢核心,再查看它们的 NI 列。如果异常进程的 NI 是 0 或负数,说明它没有任何退让,而这种任务往往是备份、日志采集、编译任务。解决办法不一定是杀掉,直接renice -n 10降权就能缓解。
第二名场景是"多个批处理任务互相抢 CPU 导致全部变慢"。这种情况可以给不同任务设置不同 nice 值,比如前台业务进程保持默认 0,后台编译任务设 5,日志压缩设 10,离线分析设 15,形成一套阶梯式的优先级方案。从长期运行看,这种部署比所有任务都抢默认值要稳定得多。
第三名场景是 "Docker 容器内设置优先级没效果" 的疑问。容器内的进程运行在宿主机的内核上,PID namespace 隔离了进程视图,但 nice 值调整机制本身还是生效的。不过容器中往往同时存在 cgroup 的 CPU 权重控制,比如cpu.weight和cpu.cfs_quota_us,这些是更粗粒度的资源约束。排查时先看容器所在的 cgroup 配置,如果容器整体 CPU 份额被限制了,单独调进程 nice 值当然用处不大。优先级和 cgroup 的关系可以理解为:cgroup 先划分了一块 CPU 蛋糕给容器,nice 值决定容器内进程怎么分这块蛋糕。
4.5 优先级相关的故障速查表
我把自己遇到过的现象整理成了下面这个表,排查的时候对照着看很快:
| 现象 | 可能原因 | 排查命令 | 推荐处理 |
|---|---|---|---|
| 业务延迟高,CPU 有进程占了 100% | 异常进程优先级过高 | top -c查看 PID 与 NI | renice -n 10 -p <pid> |
| renice 后无效果 | autogroup 或 cgroup 限制 | 检查 autogroup 目录与 cgroup | 调整 autogroup nice 或容器权重 |
| 普通用户无法调低 nice 值 | 内核权限限制 | 确认执行用户 | 用 root 或 sudoers 授权 |
| 系统卡死,SSH 无法响应 | 实时优先级被设置 | 后台或串口登录排查 | 限制 RT 运行时间或重启 |
| 服务重启后优先级丢失 | 未在 systemd 里配置 | systemctl cat查看单元 | 添加 Nice 或 CPUSchedulingPolicy |
| 多线程进程调优先级只影响主线程 | 线程组优先级与进程不同步 | ps -eLf查看 LWP | 根据场景对线程单独设置 |
这张表不是教科书条文,而是我实际排查中总结出的高频情况。如果你遇到类似现象,先对照排查命令确认,再动手修改,不要一上来就大改配置。
这里再补充一个我常用的观察手段。临时想观察优先级分配,可以起一个循环任务对比不同 nice 值:
for i in 1 2 3; do nice -n 0 -- bash -c 'while :; do :; done' & done for i in 1 2 3; do nice -n 5 -- bash -c 'while :; do :; done' & done然后把进程数、nice 值、CPU 占有率放到同一张监控图里,观察时间片分布。这个方法特别适合培训新人理解 nice 值的实际效果,比单纯看手册直接得多。
做这个实验时记得及时 kill 掉这些忙循环,别把测试机跑满。实际操作中可以先记录测试前的系统平均负载,再用pidstat统计每个进程的 CPU 占用,这样能更明显地看出权重比例。
5. 从优先级理解 Linux 的资源管理哲学
学习优先级的过程中,我越来越强烈地感觉到 Linux 的调度设计并不追求"让单个进程跑到最快",而是追求"整体系统响应可控"。nice 值、实时策略、cgroup、autogroup 各管一段,共同组成了一套非常灵活的资源管控体系。这也是为什么 Linux 可以在同一台机器上同时跑数据库、Web 服务和各种批处理任务而不互相拖垮。
如果你刚接触这部分内容,我建议从ps -el的 NI 列开始,逐步做三个实验:第一个,启动一个忙循环,观察它的 CPU 占用;第二个,用renice把它的 nice 值调到最高,再看 CPU 占用变化;第三个,如果条件允许,在一台空闲机器上小心测试chrt -f 99的后果。做完这三个实验,你对优先级的理解会比读十篇文章都扎实。
最后分享一个小技巧:遇到可疑的高 CPU 进程,先用ps -o pid,ppid,ni,comm,cmd -p <pid>一次性把进程信息和启动命令看全,再决定是否调整优先级。很多时候看似是优先级问题,实际是脚本逻辑问题,优先级只是背锅侠。要记住,调度机制再精巧,也不能替代你分析业务本身的资源需求。