如果你管理过一台跑着多个任务的Linux服务器,大概率遇到过这样的场景:一个后台备份任务突然把CPU吃满,前端业务的响应时间跟着飙上去,kill掉它不合适,重启更不现实。这时候你可能会想到一个词——进程优先级。没错,Linux很早就提供了nice、renice、chrt这样一套工具,专门用来调整进程在CPU调度中的权重。这篇笔记想把这些年关于Linux进程优先级的实战经验整理出来,从内核调度的基本逻辑讲到命令行实操,再讲几个真实环境中的调优案例和踩坑记录。内容不算高深,但足够帮你把优先级这个概念从"看过"变成"会用",尤其是刚接触Linux运维和嵌入式开发的朋友,应该能少走不少弯路。
1. 进程优先级到底在解决什么问题
1.1 从一次CPU满载事故想起的
先说个真实经历。有一年我维护的一台8核服务器,白天跑着Java网关,凌晨两点有个定时任务起来做数据归档。这个归档任务本身涉及大量压缩和排序,直接把所有核都占满了。结果就是凌晨的报表接口时不时超时,监控一拉发现CPU的user态冲到95%以上,Java进程的线程大量处于R状态但拿不到CPU时间片。当时第一反应是降低归档任务的优先级,一条renice -n 10 -p 12345下去,几分钟内业务接口的RT就回落了,归档任务也从一个半小时延长到两个小时多一点,但完全不影响核心业务。
这个案例就是进程优先级最典型的应用场景:多个进程同时争抢CPU时,操作系统要决定谁先用、谁多用。Linux把决策权交给了调度器,而调度器依据的核心指标之一,就是进程优先级。可以简单理解为:它不是"能不能运行"的开关,而是"多快能轮到、能占多久"的权重。
1.2 优先级不是一个数值那么简单
很多人第一次接触这个主题,以为就是nice -n后面跟一个数字。实际上Linux进程的优先级体系比这复杂,它至少包含三套并行的概念:普通进程的nice值、内核里的静态优先级/动态优先级,以及实时进程的实时优先级。这几套东西分别对应不同的调度策略,也对应不同的修改工具。
用大白话解释:普通进程之间用nice值分轻重,nice值越低越"有礼貌",反而越容易被优先调度;实时进程则走另一套逻辑,优先级数值从1到99,数值越大越优先,它能直接抢占普通进程的CPU时间。所以后面你会看到,用renice调整普通进程,用chrt才能动实时进程的属性。这两套机制彼此独立,混着用会出大问题。
2. Linux调度器视角下的优先级体系
2.1 从nice值到内核优先级,调度器是怎么算的
要理解优先级,不能只看表面数值。Linux内核里,每个非实时进程都有一个静态优先级static_prio,范围是100到139。这个数值和nice值有一个明确的换算关系:static_prio = 100 + nice。也就是说nice范围是-20到19,对应静态优先级100到139。数值越小,优先级越高,所以nice=-20的进程几乎是最"横"的普通进程。
但是在CFS(完全公平调度器)里,真正参与调度计算的是权重,不是直接的优先级数值。内核维护了一张从nice值到权重的映射表,nice每差1,权重约差1.25倍;nice差5,权重差约3倍。这意味着一个nice=0的CPU密集型进程,和一个nice=5的同类进程同时跑在单核上,前者获得的CPU时间大约是后者的3倍,而不是简单的5%差别。很多新手改了nice之后觉得"没效果",就是因为他们没搞懂这个非线性关系,也没有制造CPU争抢的背景。
2.2 实时优先级和普通优先级为什么是两套体系
实时进程走的是SCHED_FIFO和SCHED_RR两种调度策略,内核里的rt_priority范围是1到99。注意,这里的1到99和普通进程的100到139不是同一个尺度,实时优先级数值越大越优先,这和nice的方向完全相反。当一个实时进程处于可运行状态时,它会优先于所有普通进程被调度,哪怕普通进程的nice值已经是-20。
我用一个比喻来帮理解:普通优先级像排队买奶茶,可以加钱插队,但终归是按顺序来;实时优先级则像急诊室抢救,医生到了就必须立刻处理,其他排队的人全得暂停。举个例子,一个SCHED_FIFO优先级50的音频采集进程,和一个nice=-20的数据分析任务抢CPU时,调度器会先满足音频进程的运行需求,数据分析任务只能捡剩余时间。这也是为什么我后面会反复强调:实时优先级是给特定场景准备的,不能随手乱设。
2.3 动态优先级和"饥饿"机制是怎么运作的
还有一个容易被忽略的概念:动态优先级。为了不让某个进程长期霸占CPU,内核会在运行时对优先级做微调。对普通进程,CFS通过虚拟运行时间确保公平性;对实时进程,则通过rt_priority加上一个可配置的rt_runtime限制,避免实时任务把系统完全锁死。RHEL系内核里,/proc/sys/kernel/sched_rt_period_us和sched_rt_runtime_us这两个文件控制实时进程在一个周期内最多占多少CPU,默认情况下即使有实时进程,也会给普通内核线程留出大约95%周期里的5%时间。
这个设计非常关键。我曾经见过有人把某个死循环进程设成SCHED_FIFO优先级99,结果整个系统几乎失去响应,SSH都敲不动命令,就是因为实时进程把CPU全吃光,内核自身的维护线程连喘息的机会都没有。理解了动态优先级和实时限制机制,再遇到这种"假死"才有思路去恢复。
3. 查看进程优先级的实用命令全解
3.1 ps、top里那几个字段,30秒看懂优先级
想在命令行里看进程优先级,最常用的是ps和top,但很多人分不清输出里的PRI、NI、PR到底代表什么。
用ps -l看当前shell的进程,输出里有两个关键字段:
ps -l F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD 0 S 1000 1234 1200 0 80 0 - 3456 do_wai pts/0 00:00:00 bash这里的NI就是nice值,普通进程是0。PRI是内核动态优先级,对普通进程通常是static_prio加上一些动态调整,大致可以看作100到139之间的数值。注意,top命令里的PR和ps里的PRI含义不同,top里为了显示直观,普通进程的PR一般等于20 + nice,范围是1到39;实时进程则显示为RT,不会看到一个具体数字。
如果用top,建议按f键把P和NI列都调出来,再按Shift+P按CPU排序,这样谁在抢CPU、谁优先级低被饿着,一眼就能扫出来。对运维排查来说,这个技巧比装什么监控工具都直接。
3.2 再往底层看:procfs和系统调用获取完整调度属性
命令行工具封装了底层数据,但真要深究,还是得看内核暴露的接口。/proc/<pid>/stat的第18个字段是nice值,第19个字段是进程的优先级数值(对普通进程是nice对应内核封装后的值,对实时进程则是rt_priority加1000),不过直接数stat字段太容易错位,我更推荐看/proc/<pid>/sched:
cat /proc/1234/sched输出里有prio、policy、se.nr_migrations等信息。其中policy字段的数值和调度策略对应关系是:0表示SCHED_NORMAL(普通CFS),1表示SCHED_FIFO,2表示SCHED_RR,3表示SCHED_BATCH,5表示SCHED_IDLE。这个文件还包含进程实际运行了多少时间、被调度了多少次,排查"为什么这个进程这么卡"时非常有价值。
如果想要更完整的调度参数,可以用chrt -p <pid>查看,也可以用sched_getattr系统调用在代码里获取。对绝大多数场景来说,ps、top、cat /proc/<pid>/sched三件套已经够用了。
4. 修改进程优先级的三种部署手段
4.1 nice命令:启动进程时指定优先级
nice命令用于在启动程序时设置优先级,语法很简单:
nice -n 15 ./backup.sh这条命令表示以nice=15启动backup.sh。另一种老式写法是nice -15,但这里有个容易踩的坑:nice -15究竟表示"-15"还是"15",不同版本和不同shell的解析并不一致。为了不出歧义,强烈建议统一用nice -n加参数的写法。
需要注意的是,普通用户用nice只能把进程的优先级调低,也就是让nice值变大(正向调整),不能调低nice值来提升优先级。这个限制来自Linux的权限模型,目的是防止普通用户恶意抢占资源。只有root用户或具备CAP_SYS_NICE能力的进程,才能把nice值往负数方向调。
# root可以把进程优先级调高 nice -n -5 ./important_server4.2 renice命令:调整已经在运行的进程
启动时忘了设置优先级,或者运行中情况变化,就需要用renice。最典型的用法是给指定PID调整nice值:
renice -n 10 -p 12345也支持按用户名调整该用户启动的所有进程:
renice -n 8 -u deployrenice调整的是运行中进程的nice值,效果立刻生效。实际操作中我一般会先用pgrep -f或者top找出进程PID,再执行renice,比如:
pgrep -f "backup_script.py" | xargs -I{} renice -n 15 -p {}这里再强调一次权限问题:普通用户只能调整自己拥有的进程,而且只能往positive方向调(降低优先级)。root用户或者具备相应capability的进程可以往negative方向调,把进程优先级提高。如果看到Operation not permitted,先检查是不是权限问题,别盲目以为命令写错了。
4.3 chrt命令:设置实时调度策略和优先级
chrt是调整实时调度策略和实时优先级的工具,但它修改的是另一个维度的"优先级",和nice是平行的体系。
查看进程当前调度策略:
chrt -p 12345把进程设置为SCHED_RR抢占式轮转,优先级50:
chrt -r -p 50 12345设置为SCHED_FIFO先进先出,优先级60:
chrt -f -p 60 12345注意:
chrt对实时优先级的设置范围是1到99,数值越大优先级越高。普通用户能设置的实时优先级上限受ulimit -r限制,root用户不受限。但root也不建议乱设,尤其是计算密集型任务,把它设成高优先级实时进程,极容易让其他进程甚至系统本身失去响应。
还有一个高频组合用法:taskset配合chrt,让实时进程绑定到特定CPU核心上运行:
taskset -c 0 chrt -f -p 50 12345这样既避免实时进程在所有核之间频繁迁移,又能减少缓存抖动。具体在下一节实战场景里展开。
4.4 代码层面:C和Python如何控制进程优先级
命令行工具管的是外部调整,有些业务场景需要在代码启动时主动设置优先级。C语言里最常用的是getpriority/setpriority和sched_setscheduler两套接口。
setpriority用于设置普通进程nice值:
#include <sys/resource.h> #include <stdio.h> int main(int argc, char **argv) { if (setpriority(PRIO_PROCESS, 0, 10) == -1) { perror("setpriority"); return 1; } printf("current nice: %d\n", getpriority(PRIO_PROCESS, 0)); return 0; }PRIO_PROCESS表示按进程维度设置,第二个参数0表示当前进程。如果想按进程组或者用户维度设置,分别用PRIO_PGRP和PRIO_USER。
设置实时调度策略则要调用sched_setscheduler:
#include <sched.h> struct sched_param param; param.sched_priority = 50; if (sched_setscheduler(0, SCHED_FIFO, ¶m) == -1) { perror("sched_setscheduler"); return 1; }Python的话更简单:
import os os.setpriority(os.PRIO_PROCESS, 0, 10)不过我的建议是:业务代码里能不动优先级就不动,尤其是实时调度策略。因为代码一旦写死,你在不同的部署环境里很难灵活调整,出了问题还不好排查。把调整交给运维侧的启动参数或者systemd配置,比写在代码里可控得多。
5. 真实场景里的优先级调优实战
5.1 场景一:备份任务和在线服务抢CPU
再回到开头那个场景。凌晨跑数据归档,压缩任务用gzip或者自研脚本打满CPU,网关响应变慢。这个场景的解法不是调高网关的优先级,而是调低归档任务的优先级,让调度器在两者竞争时偏向网关。
实际操作我会分两步。第一步调整CPU优先级:
renice -n 15 -p $(pgrep -f backup_archive)第二步千万别忽略I/O优先级。归档任务除了吃CPU,还会产生大量磁盘读写,磁盘I/O争抢同样会影响业务。用ionice把I/O优先级降到最低等级idle:
ionice -c 3 -p $(pgrep -f backup_archive)ionice -c 3表示只使用空闲I/O时间,系统有其他I/O请求时,归档任务会被强制让路。两条命令配合,归档任务变"佛系",对业务的冲击降到最低。
5.2 场景二:音频采集进程不准有卡顿
另一个经典场景是嵌入式设备或者桌面机上跑音频采集,进程需要每几十毫秒就处理一次数据,稍微被调度延迟就会产生爆音。这种场景靠renice -n -5往往不够,因为普通进程的优先级再高,也还是会被其他普通进程抢占。正确思路是把采集进程转成实时调度。
先看采集进程PID,比如1892,然后执行:
chrt -f -p 50 1892如果这个进程是多线程的,建议结合taskset绑定到一个不跑其他中断密集型任务的核上:
taskset -pc 2 1892这里有个细节:chrt -f -p设置的SCHED_FIFO优先级,对内核而言是"很硬"的保证,但前提是该进程不能长时间不释放CPU。音频采集进程通常是"短促执行然后让出CPU等数据",非常适合FIFO。反过来,如果给一个纯计算的渲染进程设FIFO,它就有可能长期霸占CPU,把系统拖死,下一节我会专门讲这个坑。
5.3 场景三:systemd服务里固化优先级设置
如果希望某个服务启动后自动带上优先级,与其在脚本里写renice,不如直接写在systemd的Unit文件里:
[Service] ExecStart=/usr/bin/my_server Nice=10 IOSchedulingClass=idle CPUSchedulingPolicy=fifo CPUSchedulingPriority=45这样服务启动时内核就直接把调度属性和优先级设置好,比启动后再renice更可靠,也不会出现脚本顺序问题。注意CPUSchedulingPolicy=的可选值有other、batch、idle、fifo、rr几种,如果设置成fifo或rr,必须配合CPUSchedulingPriority指定1到99之间的值。
6. 常见问题与排查技巧实录
6.1 renice提示Operation not permitted是怎么回事
这个问题在论坛里被问了无数遍,基本都是权限问题。普通用户执行renice -n -5时,内核会检查是否拥有目标进程的PRIO权限,由于降低nice值等于提高优先级,属于特殊能力,普通用户直接拒绝。解决办法要么切root,要么给用户加CAP_SYS_NICE能力。如果目标是别人的进程,即使调低优先级也没权限。
另外,实时优先级还受RLIMIT_RTPRIO资源限制管束。在/etc/security/limits.conf里配置:
myuser hard rtprio 50这是allow用户把实时优先级最多设到50,如果配成0,普通用户就连chrt -f -p 1都无法执行。
6.2 改了nice值,为什么进程还是抢CPU
原因之一是CFS调度只在CPU满负载的时候才体现差异。如果系统有大量空闲核,nice值差10的任务照样跑得飞快,优先级不会主动限制进程使用空闲CPU。所以在做优先级测试时,必须先人为制造CPU负载,否则你会觉得renice根本没有用。
原因之二是权重差异是非线性的。nice每差1,权重差约1.25倍,看起来不多,但nice=0和nice=19的权重比接近10倍。调整之后观察实际效果,最好用top按CPU时间看比例,不要只看心理预期。
原因之三可能就是你的进程本身不在CPU队列里,而是卡在I/O或锁上。这时候调CPU优先级毫无帮助,得用iostat、pidstat这类工具定位瓶颈到底在哪一层。
6.3 把进程设为SCHED_FIFO后系统突然卡死了
这是我见过最高危的操作,没有之一。有人为了优化一个自研服务,把它的调度策略设成了SCHED_FIFO且优先级99,结果服务CPU占用一高,整个Linux失去响应,鼠标不动、SSH连不上、网络监控也挂了。
原因是实时进程可以无限抢占普通进程,包括内核的一些关键线程。虽然内核有rt_runtime机制保护,但如果实时进程始终可运行,它会把周期里绝大部分时间都吃掉。进程越多、优先级越高的实时任务,系统越容易"假死"。
如果已经卡成这样,怎么自救?我的经验是尽可能通过硬件外的方式重启系统,如果还在可操作窗口,立刻把该进程的调度策略改回SCHED_NORMAL:
chrt -o -p 0 <pid>或者直接kill掉。但这属于事后的补救,所以我在实际项目中定了两条规矩:第一,不为计算密集型任务设SCHED_FIFO;第二,实时进程必须限制CPU占用,配合taskset绑定单独核,并做好监控告警。
6.4 容器和cgroup让优先级变得不"那么"直接
最后提醒一个容易被忽略的问题:现在很多服务跑在Docker容器里,或者受systemd cgroup v2限制。这时即使你手动调了进程的nice值,cgroup的cpu.weight和cpu.max设置也会影响实际分配比例。如果容器的cpu.weight很低,你在容器内把进程的nice调到-20,能分到的CPU配额仍然有限。
docker里可以这样设置CPU相对权重:
docker run --cpu-shares=1024 --cpu-quota=50000 ...--cpu-shares对应CFS的权重值,默认1024;--cpu-quota限制这个容器在每100ms周期内最多用多少CPU时间。在排查优先级问题时,先确认自己修改的是调度层还是cgroup层,否则你会陷入"明明改了优先级却毫无变化"的困惑。
我自己这些年调过不少进程优先级,最大的体会是:这玩意不是调完就万事大吉的银弹,它只是调度体系里的一个维度,后面还站着CFS、实时调度、cgroup、I/O优先级、CPU亲和性这些相互缠绕的机制。遇到CPU争抢问题,先画清楚是谁在抢、抢的是CPU还是磁盘、谁必须优先,再去决定用nice、renice还是chrt。把整套逻辑理清了,比记住多少条命令都管用。实际工作中我也一直按这个顺序来排查,出错的概率会小很多。