【Linux 系统编程】进程优先级、调度与切换
当系统中只有一个可运行进程时,CPU 直接执行它即可;当多个进程同时处于可运行状态时,就必须决定谁先运行、运行多久,以及什么时候换成另一个进程。这三个问题分别对应进程优先级、进程调度和上下文切换。
关键词: nice、PRI、并发、并行、上下文切换、运行队列、O(1) 调度器、CFS、EEVDF
0. 先把三个问题串起来
调度过程可以拆成三步:
多个任务具备运行条件
↓
调度器选择下一个任务
↓
CPU 执行该任务一段时间
任务不会一直占着 CPU。时间配额用完、主动等待资源、更高优先级任务被唤醒或任务退出时,内核都可能重新调度:
任务 A 正在运行
↓ 发生调度时机
保存 A 的执行现场
↓
选择任务 B
↓
恢复 B 的执行现场并继续运行
因此,优先级解决的是“多个任务怎样竞争 CPU”,调度解决的是“选择谁”,上下文切换解决的是“怎样从一个任务安全地换到另一个任务”。
1. 优先级和权限不是一回事
权限解决“能不能”,优先级解决“竞争时怎样分配”。
例如,一个进程没有读取某个文件的权限,那么无论它的 CPU 优先级有多高,都不能绕过文件权限检查。只有多个任务已经具备使用同一种资源的条件时,优先级才会参与先后顺序或资源份额的计算。
还需要注意:优先级高不等于一定立刻运行,也不等于会一直运行到结束。 调度结果还会受到调度策略、CPU 亲和性、任务是否阻塞、控制组限制以及系统负载等因素影响。对普通分时任务来说,更高的优先级通常表示在 CPU 竞争时获得更有利的运行机会或更多 CPU 份额。
2. Linux 普通进程中的 nice 值
普通进程常用 nice 值表达“愿意对其他任务友好到什么程度”:
| nice 值 | 含义 |
|---|---|
-20 |
优先级最高,竞争 CPU 时最有利 |
0 |
默认值 |
19 |
优先级最低,愿意让出更多 CPU |
数值越小,调度越有利;数值越大,调度优先级越低。
nice 与 PRI 的关系
nice 会影响调度器为普通任务计算的权重,但 nice 本身不是一个通用的内核优先级编号。ps 输出中的 PRI 也不适合记成固定的“默认 80、范围 60~99”,因为不同显示格式、调度策略和实现可能采用不同的换算方式。
几个概念需要分开:
| 名称 | 所处层次 | 说明 |
|---|---|---|
| nice / NI | 用户可调整属性 | 现代 Linux 中范围为 -20~19 |
| PRI | 观察工具的输出字段 | 显示方式可能随工具和调度策略变化 |
static_prio |
内核历史实现中的字段 | 旧 O(1) 调度器中普通任务常映射到 100~139 |
| 实时优先级 | 实时调度策略 | 与普通任务的 nice 不是同一套规则 |
所以,PRI(new) = PRI(old) + nice 可以帮助理解“nice 会修正调度倾向”,但不能当成适用于所有 Linux 版本和调度策略的固定公式。
谁可以修改 nice
普通用户通常可以把自己的 nice 值调大,也就是主动降低调度优先级;把 nice 值调小相当于提高优先级,一般需要相应权限,例如 CAP_SYS_NICE,或者受到合适的 RLIMIT_NICE 配置允许。
3. 使用系统调用读取和修改 nice
#include <errno.h>
#include <stdio.h>
#include <sys/resource.h>
static int current_nice(int *value)
{
errno = 0;
int result = getpriority(PRIO_PROCESS, 0);
if (result == -1 && errno != 0) {
return -1;
}
*value = result;
return 0;
}
int main(void)
{
int before = 0;
int after = 0;
if (current_nice(&before) == -1) {
perror("getpriority");
return 1;
}
if (setpriority(PRIO_PROCESS, 0, 10) == -1) {
perror("setpriority");
return 1;
}
if (current_nice(&after) == -1) {
perror("getpriority");
return 1;
}
printf("before: nice = %d\n", before);
printf("after : nice = %d\n", after);
printf("larger nice value -> lower scheduling priority\n");
return 0;
}
运行结果:
before: nice = 0
after : nice = 10
larger nice value -> lower scheduling priority
程序把自己的 nice 从 0 调整为 10,这是降低优先级,普通用户通常可以完成。反过来把 nice 从 10 改成负数属于提高优先级,在没有相应权限时会失败。
getpriority() 成功时也可能返回 -1,所以代码先清空 errno,再结合 errno 判断 -1 是合法 nice 值还是错误返回。
4. 竞争、独立、并发和并行
竞争性
可运行任务的数量通常多于 CPU 核心数量,因此任务需要竞争 CPU。优先级和调度策略的作用,就是在竞争中兼顾吞吐量、响应速度和公平性。
独立性
不同进程通常拥有彼此隔离的虚拟地址空间,一个进程的普通内存访问不能直接修改另一个进程的地址空间。但“独立”不是完全没有联系:进程仍然可以有意共享文件、共享内存、管道、套接字等资源。
并发与并行
| 概念 | 条件 | 观察效果 |
|---|---|---|
| 并发 | 一个 CPU 核心交替推进多个任务 | 一段时间内多个任务都有进展 |
| 并行 | 多个 CPU 核心在同一时刻执行不同任务 | 多个任务真正同时运行 |
单核 CPU 在某一个瞬间只能执行一个任务,但切换足够快时,用户会感觉多个程序同时运行;多核 CPU 则既可以在每个核心上并发调度,也可以让多个任务真正并行。
5. 哪些情况会触发调度
进程切换不只发生在“时间片用完”这一种情况下。
| 情况 | 当前任务发生了什么 |
|---|---|
| 运行配额到达调度边界 | 内核重新判断是否应该换任务 |
| 等待键盘、磁盘、网络、锁或定时器 | 任务主动离开可运行集合 |
| 更有资格运行的任务被唤醒 | 当前任务可能被抢占 |
| 主动让出 CPU | 调度器重新选择任务 |
| 任务退出 | 当前任务不再参与后续调度 |
中断、异常或系统调用会让 CPU 进入内核,但进入内核并不必然发生任务切换。只有内核认为需要重新调度,并选中了不同任务时,才会真正进行上下文切换。
6. 上下文切换保存了什么
进程运行时,CPU 寄存器中保存着当前计算所需的数据。不同体系结构的寄存器名称不同,但通常需要关注:
- 程序计数器或指令指针:下一条从哪里继续执行;
- 栈指针:当前函数调用栈位于哪里;
- 通用寄存器:运算参数、中间结果和返回值;
- 标志寄存器:条件码、控制位等状态;
- 浮点和 SIMD 寄存器:在需要时保存;
- 与内核栈、地址空间和线程状态相关的管理信息。
切换过程可以分开看:
第一步:任务 A 进入内核
第二步:保存 A 的寄存器现场和调度相关状态
第三步:调度器选择任务 B
第四步:切换到 B 的内核栈、地址空间等执行环境
第五步:恢复 B 上次保存的寄存器现场
第六步:B 从上次停下的位置继续执行
一个逻辑 CPU 同一时刻只有一套正在工作的物理寄存器,但寄存器中的值在逻辑上属于当前任务。任务被换下时,这些值会保存到内核管理的数据结构或内核栈中;任务再次运行时再恢复回来。具体保存位置和切换细节与处理器架构、内核版本有关,不能简单理解成所有内容都直接放在 task_struct 中。
上下文切换还会带来额外开销:除了保存和恢复状态本身,缓存、TLB 和分支预测器中的有效信息也可能受到影响。因此切换过于频繁会降低 CPU 的有效工作时间。
7. 调度器怎样选择下一个任务
Linux 调度并不是把所有任务放进一条简单队列。内核会先区分调度类别和策略,例如普通公平调度、实时调度和截止时间调度,再由相应调度器选择任务。
对于普通任务,nice 主要影响公平调度中的权重;实时任务则有独立的实时优先级规则,不能用 nice 直接解释。
在多核系统中,每个 CPU 都有自己的运行队列。任务优先从本 CPU 的可运行集合中选择,同时系统还要进行负载均衡,避免某个 CPU 排满任务、另一个 CPU 却处于空闲状态。
调度器需要同时考虑:
- 优先级或调度权重;
- 任务已经获得了多少 CPU 时间;
- 响应延迟和吞吐量;
- 任务是否刚刚被唤醒;
- CPU 亲和性与多核负载;
- 避免低优先级任务长期得不到运行机会。
8. Linux 2.6 早期的 O(1) 调度器
课程中的 active、expired、queue[140] 和 bitmap,描述的是 Linux 2.6 早期使用过的 O(1) 调度器,而不是 CFS。
优先级数组
旧调度器使用 prio_array 组织任务:
queue[140]对应 140 个优先级队列;0~99用于实时优先级;100~139用于普通任务;- 相同优先级的任务进入同一条队列;
bitmap标记哪些优先级队列非空。
位图让调度器不必逐条遍历所有任务,就能快速定位最高优先级的非空队列。
active 与 expired
调度过程按下面的顺序推进:
从 active 的位图找到最高优先级非空队列
↓
取出该队列前端的任务运行
↓
任务的时间配额耗尽
↓
重新计算后放入 expired 的对应队列
↓
active 为空时交换 active 与 expired 指针
这里只交换两个指针,不需要搬运两个数组中的全部任务。选择任务的时间复杂度不随可运行任务数量线性增长,因此称为 O(1) 调度器。
这个模型适合理解“优先级队列、位图和轮换”怎样配合,但它是历史实现。把这套结构直接称为 CFS,或者用它解释当前 Linux 的普通任务调度,都会混淆不同年代的内核设计。
9. 从 CFS 到 EEVDF
Linux 2.6.23 引入 CFS,替代了此前普通任务调度中的旧实现。CFS 不再使用 active/expired 两套优先级数组,而是根据虚拟运行时间 vruntime 维护任务,优先选择获得 CPU 时间相对较少的任务,以逼近“理想公平 CPU”。经典 CFS 实现使用按时间排序的红黑树组织可运行任务。
Linux 从 6.6 开始向 EEVDF 过渡。EEVDF 同样关心任务是否获得了公平份额,但进一步计算任务的 lag 和虚拟截止时间,从满足条件的任务中选择虚拟截止时间最早者,以兼顾公平性和响应延迟。
| 调度器 | 主要组织思想 | 选择依据 |
|---|---|---|
| O(1) | 优先级数组、位图、active/expired | 最高优先级非空队列 |
| CFS | 虚拟运行时间、时间有序结构 | 较小的 vruntime |
| EEVDF | lag 与虚拟截止时间 | 最早的合格虚拟截止时间 |
虽然实现不断变化,nice 的核心作用仍然是影响普通任务在公平调度中的权重,而不是给出一个绝对的执行顺序。
10. 常见误区
| 容易混淆的说法 | 准确理解 |
|---|---|
| 有权限就一定先执行 | 权限决定能否访问,优先级决定竞争时的调度倾向 |
| 优先级高的任务会一直运行到结束 | 任务仍可能被抢占、阻塞或退出 CPU |
| nice 就是内核优先级编号 | nice 是影响普通任务调度权重的用户属性 |
| Linux 的 PRI 永远默认 80 | PRI 的显示与换算依赖工具、调度策略和实现 |
PRI(new)=PRI(old)+nice 是固定内核公式 |
只能作为入门类比,不能跨版本和策略机械套用 |
| 进程独立表示完全不共享资源 | 地址空间默认隔离,但进程可以有意共享多种资源 |
| 并发就是并行 | 并发强调一段时间共同推进,并行强调同一时刻同时执行 |
| 上下文切换只保存一个 PC 指针 | 还涉及栈、通用寄存器、标志、地址空间和内核执行环境 |
| 进入内核就一定发生进程切换 | 只有重新调度并选中不同任务时才切换 |
| active/expired 是 CFS 的结构 | 它属于 Linux 2.6 早期的 O(1) 调度器 |
11. 总结
进程优先级、调度和切换是一条连续的逻辑:
- 多个可运行任务竞争有限的 CPU;
- 调度策略结合权重、运行时间和系统状态选择任务;
- 内核保存当前任务的执行现场;
- CPU 恢复另一个任务的现场并继续执行;
- 多次快速切换形成并发,多核同时执行形成并行。
nice 只是普通任务调度中的一个输入,不代表绝对执行顺序;active/expired 是理解旧 O(1) 调度器的历史模型,不是现代 CFS/EEVDF 的数据结构。把这些层次分开,才能正确解释 ps 中的优先级字段,也能避免把课程中的历史实现误认为当前内核的完整机制。
更多推荐



所有评论(0)