优先级是什么,系统如何通过NI(nice)和PRI(priority)值调整进程优先程度
一、什么是"优先级"
优先级 = 调度器决定"谁先用 CPU"的依据
-
优先级高 → 更容易被调度、抢占别人
-
优先级低 → 更晚得到 CPU
⚠️ 注意:
优先级不是"一定先跑",而是在竞争 CPU 时更有优势。
二、PRI(priority)是什么?
1️⃣ PRI 是什么
PRI 是 内核真正用来比较的调度优先级(内部值)
你在 top、ps -l 里看到的 PRI,本质是:
调度器眼里的"真实优先级"
特点:
-
数值越小,优先级越高
-
包含内核计算结果
-
用户不能直接精确设置
例子(直觉):
| PRI | 优先级 |
|---|---|
| 80 | 很高 |
| 100 | 普通 |
| 120 | 很低 |
2️⃣ PRI 的来源
-
普通进程:
👉 PRI 主要由 NI 计算而来
-
实时进程:
👉 PRI 由实时优先级决定(0~99)
三、NI(nice)是什么?
1️⃣ NI 是什么
NI 是 用户可以调节的"友好度"
名字就很形象:
越 nice,越"客气",越愿意让 CPU 给别人
范围:
-20 ~ 19
-
-20:一点也不客气(优先级最高)
-
19:非常客气(优先级最低)
2️⃣ NI 和优先级的关系
⚠️ 这里有一个非常重要但容易反直觉的点:
NI 越小 → 优先级越高
| NI | 含义 |
|---|---|
| -10 | 比普通进程更容易抢到 CPU |
| 0 | 默认 |
| 10 | 更容易被别人抢 |
注:NI范围是-20~19
3️⃣ NI 能干嘛?
-
只能影响普通进程(CFS)
-
不能影响实时进程
你可以理解为:
NI 是你给调度器的"建议",不是命令
四、PRI 和 NI 的关系(核心公式)
对普通进程来说,可以近似理解为:
PRI ≈ 80 + NI
(具体数值和内核版本有关,但趋势永远成立)
👉 NI 越小,PRI 越小,优先级越高
五、为什么既要 PRI 又要 NI?
因为角色不同:
| 项目 | PRI | NI |
|---|---|---|
| 谁用 | 内核 | 用户 |
| 能不能改 | ❌(直接改不行) | ✅ |
| 数值意义 | 越小越高 | 越小越高 |
| 是否动态 | ✅ | ❌ |
一句话总结:
NI 是"你说你想多重要",PRI 是"内核算出来你到底多重要"
六、举例子
想象排队买咖啡 ☕:
-
NI:你自己给自己贴的标签
"我很急" (-10),"我不急" (10)
-
PRI:店员综合判断
看你急不急,看你等多久了,看你是不是 VIP(实时进程)
👉 最终谁先买到咖啡,看 PRI。
好,这个问题其实正好是"为什么 Linux 要这样设计调度器"的核心动机 。我按一个很自然的逻辑来讲:
👉 先讲分时 vs 实时 → 再讲公平从哪来 → 最后回到 NI 为什么只是"建议"。
分时操作系统与实时操作系统
一、分时操作系统是什么?
1️⃣ 核心目标
让"很多人/程序"感觉自己在"同时"用一台电脑
关键词只有两个:共享和公平
典型场景:
- 桌面 Linux,服务器(Web、数据库),多用户系统
分时操作系统的核心目标是让多个用户或多个程序共享同一台计算机,并且让每个任务都"感觉自己在同时运行"。它通过把 CPU 时间切分成极短的时间片,在不同进程之间快速切换,从而实现并发效果。分时系统关注的是整体体验和响应性,而不是某一个任务是否绝对优先。
2️⃣ 分时系统怎么做到的?
靠两件事:
-
时间片
- CPU 被切成很多很小的时间段
-
快速切换
-
进程 A 跑一会
-
切到进程 B
-
再切回来
-
因为切换太快,人感觉是"同时运行"。
3️⃣ 分时系统最怕什么?
❌ 某个进程霸占 CPU
如果一个程序一直跑:
- 其他程序卡顿,shell 无法响应,系统"假死"
在分时系统中,最重要的问题是避免某个进程长期独占 CPU。如果一个进程无限制地占用处理器,会导致其他进程得不到运行机会,最终造成系统卡顿甚至"假死"。因此,分时操作系统必须从设计上防止进程饥饿,保证所有可运行任务都能获得 CPU。
👉 所以分时系统的第一原则是:
谁也不能太自私
二、实时操作系统是什么?
1️⃣ 核心目标(和分时完全不同)
不是"公平",而是"确定性"
实时操作系统的核心目标不是公平,而是确定性,即任务必须在规定的时间内完成。相比"运行得快",实时系统更关心"能不能准时"。只要满足时间约束,即使牺牲系统的公平性也是可以接受的。
实时系统关心的不是"快不快",而是:
能不能"在规定时间内一定完成"
2️⃣ 两种实时系统
软实时(Soft RT)
-
偶尔超时可以接受
-
比如:音视频播放
硬实时(Hard RT)
-
超时 = 系统失败
-
比如:飞控,刹车系统,医疗设备
3️⃣ 实时系统最怕什么?
❌ 不确定性
实时系统通常采用严格的优先级调度,高优先级任务可以随时抢占低优先级任务,低优先级任务甚至可能长期得不到执行。这种设计保证了关键任务的时间确定性,但也意味着系统整体公平性并不是首要目标。
- 什么时候能跑?会不会被抢占?会不会被延迟?
👉 所以实时系统通常:
- 高优先级任务 绝对先跑 ,低优先级任务 可以永远饿死
三、分时 vs 实时,一眼对比
分时操作系统强调公平和系统整体可用性,尽量让所有任务都有机会运行;而实时操作系统强调时间确定性,允许为了保证关键任务准时完成而牺牲其他任务的执行机会。这两种系统在调度设计上的出发点完全不同。
| 维度 | 分时 OS | 实时 OS |
|---|---|---|
| 目标 | 公平、响应快 | 确定性、准时 |
| 是否允许饿死 | ❌ 不允许 | ✅ 允许 |
| 调度策略 | 尽量公平 | 绝对优先级 |
| 用户体验 | "大家都顺" | "我一定准时" |
四、为什么调度时要"尽可能公平"?
现在回到 Linux(默认模式)。
Linux 绝大多数场景是:
- 桌面,云服务器,多用户环境
👉 本质是一个 分时操作系统。
Linux 在默认情况下是一种以分时为主的操作系统,主要面向桌面、服务器和多用户环境。这决定了 Linux 调度器在设计上必须优先保证系统整体的公平性和响应性,而不是满足单个进程的极端需求。
1️⃣ 不公平会发生什么?
假设没有"公平":一个 CPU 密集进程:while(1);它一直占着 CPU
结果:SSH 卡死,top 打不开,系统"活着但不能用"
在 Linux 这样的分时系统中,公平意味着不会让任何一个进程长期霸占 CPU,也不会让任何一个进程长期得不到执行。只有保证公平,系统才能保持可交互性和稳定性,这是分时操作系统能够正常工作的基础。
👉 这在分时系统里是 灾难。
2️⃣ 所谓"公平"不是"完全一样"
这是重点⚠️:
公平 ≠ 每人给一样多的 CPU
而是:
按照"你该得到的比例"分配 CPU
这个比例来自:
- 优先级,nice 值,等待时间
这就是 CFS 的设计初衷。
五、从"公平"引出 CFS 的设计哲学
CFS 的一句话目标:
模拟一个"理想的、完全公平的 CPU"
-
如果有 N 个进程
-
每个进程都应该得到:
1 / N 的 CPU 时间
(再乘上权重)
调度中的公平并不是让每个进程获得完全相同的 CPU 时间,而是根据进程的重要性和优先级分配合理比例的 CPU 资源。高优先级进程可以获得更多 CPU,但不能无限制地占用处理器。
Linux 的完全公平调度器(CFS)通过追踪每个进程已经获得的 CPU 时间,优先调度"欠 CPU 时间最多"的进程,从而逼近理想的公平状态。这种设计保证了系统长期运行时的公平性,而不是短时间内的绝对优先。
👉 所以 CFS 不是问:
"你有多高优先级?"
而是问:
"你相对于别人,欠了多少 CPU?"
六、为什么 NI 只是"建议",不是命令?
NI(nice 值)是用户对进程调度优先级的期望表达,用来告诉调度器这个进程"愿不愿意让出 CPU"。NI 值越小,表示进程越不"客气",希望获得更多 CPU 时间;NI 值越大,则表示更愿意让出 CPU。
1️⃣ 因为 Linux 首先是分时系统
在分时系统里:
- 系统整体可用性 > 单个进程意愿
如果 NI 是"命令",会发生什么?
-
用户把所有进程都设成
nice -20 -
公平性直接崩掉
-
系统退化成"伪实时系统"
2️⃣ NI 只影响"权重",不决定"生死"
在 CFS 里:
- NI → 权重,权重 → vruntime 增长速度,vruntime → 谁先跑
也就是说:
NI 只能让你"慢点亏 / 快点亏 CPU"
但不能:
- 一直霸占,让别人永远跑不到
由于 Linux 首先是一个分时操作系统,内核必须保证整体公平和系统可用性,因此不能无条件服从某个进程的意愿。NI 只会影响进程在公平调度中的权重,而不会赋予进程无限制占用 CPU 的能力。
3️⃣ 内核必须"兜底"
哪怕你 NI 很高:
- 你跑久了,vruntime 还是会变大,还是会被换下去
即使进程设置了较高的优先级或较小的 NI 值,内核仍然会根据进程已经运行的时间动态调整调度顺序。当进程运行时间过长时,其调度优先级会自然下降,从而让其他进程获得运行机会,这是内核维护公平性的关键机制。
👉 这是 公平性的底线保障。
进程的竞争,独立,并行,并发
竞争(Competition)
竞争指的是多个进程或线程同时争夺有限的系统资源,比如 CPU、内存、磁盘或锁。由于资源是有限的,不可能所有任务同时满足,因此调度器或同步机制必须决定谁先用、谁后用。竞争本身并不是坏事,它是多任务系统的常态,但如果处理不好,就会导致饥饿、死锁或性能急剧下降。
独立(Independence)
独立指的是多个进程或线程在逻辑上互不影响,它们之间没有共享数据,也不存在直接的协作关系。一个任务的执行结果不依赖另一个任务的状态,即使同时运行,也不会产生干扰。独立性是并发和并行安全的理想状态,因为不需要额外的同步或协调。
并发(Concurrency)
并发指的是在同一时间段内,系统中存在多个任务处于"进行中"的状态,但不要求它们在同一时刻真正执行。通过时间片轮转和快速切换,单核 CPU 也可以实现并发效果。并发的核心是结构上的同时存在,强调任务之间的交错执行,而不是物理上的同时运行。
并行(Parallelism)
并行指的是在同一时刻,多个任务真正同时执行,通常依赖多核 CPU 或多个处理器。每个任务可以在不同的核心上独立运行,不需要频繁切换。并行关注的是物理层面的同时执行,其目标是提升吞吐量和计算速度。
并发与并行的关系
并发是一种程序或系统的组织方式,描述的是"能否同时处理多件事";并行是一种硬件和执行状态,描述的是"是否真的同时做多件事"。并发不一定并行,但并行一定并发。在单核系统上可以有并发但不可能有并行。
竞争与并发的关系
只要存在并发,就几乎一定存在竞争,因为多个任务会在时间上交错使用共享资源。并发本身并不等同于竞争,但竞争是并发环境下的常见问题,需要通过调度策略和同步机制来控制。
独立性与并发安全的关系
独立的任务在并发或并行执行时不会发生竞争,因此天然是并发安全的。现实系统中,完全独立的任务并不多,所以操作系统和程序设计必须通过锁、原子操作等方式来人为保证"看起来独立"。
从操作系统角度的整体理解
操作系统通过调度机制在并发任务之间分配 CPU,通过同步机制处理因竞争带来的问题,并通过多核硬件实现并行执行。并发描述问题结构,并行提升执行效率,竞争是不可避免的现实,而独立是最理想但最难完全实现的状态。
linux中程序停止再恢复后如何回到停止前的瞬间状态继续运行
这种"停止 → 再调度 → 从原地继续执行"的能力,完全由 Linux 内核自动完成,用户态程序什么都不用、也不可能自己去做。
一、抢占发生时,内核到底做了什么?
当你的进程正在 CPU 上跑,出现以下情况之一:
- 时间片用完,更高优先级进程就绪,硬件中断 / 系统调用,主动阻塞(
sleep/wait/read)
CPU 会陷入内核态,随后可能发生一次上下文切换。
1️⃣ 保存"CPU 上下文"
内核会把当前执行点的完整执行状态 保存到进程/线程的 task_struct 中
至少包括:
- 程序计数器(PC / x86 的 RIP),栈指针(SP / RSP),通用寄存器(RAX、RBX、RCX...),标志寄存器(EFLAGS),部分 FPU / SIMD 状态(延迟保存)
发生抢占时:当时间片用完或出现中断,CPU 进入内核态,Linux 内核会把当前线程的执行状态保存起来,包括程序计数器(PC/RIP)、栈指针(SP/RSP)、通用寄存器、标志寄存器以及必要的浮点/SIMD 状态。
👉 这一步保证了:
"下次再运行时,CPU 能继续执行刚才那条指令的下一步"
二、进程被切走后处于什么状态?
- 进程依然存在,地址空间不变(代码段、堆、栈全部在),用户态栈里的局部变量还在,锁状态、函数调用栈全都没动
只是:
不在 CPU 上执行而已
保存到哪里 :这些状态被存放在该线程对应的内核数据结构中(如 task_struct 关联的内核栈和 thread_struct),用户态的代码段、堆和栈都保持原样,不会被破坏或重置。
所以它不是"暂停执行后再重新开始",而是被按下了"暂停键"。
三、再次被调度时如何"原地复活"?
当调度器选中该进程:
-
切换页表(CR3) → 恢复该进程的虚拟地址空间
-
恢复保存的寄存器
-
从保存的 PC/RIP 继续执行
在用户程序的视角里:
x++;
y = foo();
z++;
如果在 y = foo(); 里面被抢占:
- 恢复后你看到的效果是:
foo()像从来没被打断过一样继续执行
这就是你要的"恢复到停止的那一瞬间"。
四、这个机制的关键点(也是很多人误解的地方)
❗ 用户态程序感知不到"被抢占"
-
抢占是透明的
-
你不能:获取"上次被抢占的 PC",手动保存寄存器,控制恢复位置
被切走后的状态:线程仍然存在,只是不再占用 CPU,地址空间、函数调用栈、局部变量和锁状态都保持在被抢占那一刻的样子。
所有这些都属于 内核调度职责。
❗ 抢占不是"安全点"
-
抢占可能发生在任意指令之间
-
所以多线程里才需要:原子操作,内存屏障,锁
重新被调度时:调度器选中该线程后,内核恢复其页表和寄存器,把保存的执行状态重新加载到 CPU 中。
否则恢复后,别的线程已经把共享数据改掉了。
五、线程 vs 进程(很关键)
在 Linux 中:
调度单位其实是线程(task)
-
每个线程都有独立的:PC,栈,寄存器上下文
-
同一进程的多个线程:共享地址空间,但分别被抢占、分别恢复
对用户程序的意义:这一切完全由内核自动完成,用户态程序无法感知或控制抢占与恢复,只需要按并发安全的方式编写代码即可。
所以你看到的"程序被抢占恢复",本质是某个线程被抢占恢复。
六、TSS(Task State Segment)是什么
tss_struct 是 Linux 在 x86 架构下用于管理特权级切换时 CPU 所需关键信息 的数据结构,本质上是硬件定义的 TSS 在内核中的软件表示。最核心的作用 :当 CPU 从用户态(Ring 3)进入内核态(Ring 0)时(如中断、异常、系统调用),CPU 需要知道内核栈在哪 ,tss_struct 提供的就是每个 CPU 的内核栈指针。现代 Linux 的使用方式 :早期 x86 可以用 TSS 做硬件任务切换,但 Linux 不使用硬件任务切换 ,而是自己实现上下文切换;因此 tss_struct 只保留和特权级切换相关的最小功能。里面通常包含什么 :主要是 Ring 0 的栈指针(如 sp0),以及在 x86-64 下用于中断栈切换的 IST(Interrupt Stack Table)指针。和 task_struct 的关系 :task_struct 描述的是"哪个线程在跑、保存了哪些执行状态",而 tss_struct 描述的是"CPU 进内核态时该用哪块安全的内核栈",两者分工不同、但在上下文切换中协同工作。
cpu调度队列,runqueue
一、为什么需要 runqueue?
先问一个最本质的问题:
CPU 从哪知道"下一个该运行谁"?
答案就是:
👉 runqueue(运行队列)
没有 runqueue 会怎样?
- 就绪进程散落在系统各处,每次调度都要全系统扫描,成本高、不可控
👉 所以内核必须有一个集中、结构化的"候选名单"。
这份名单就是 runqueue。
二、runqueue 是什么?
一句话定义:
runqueue 是"当前可以立刻运行在 CPU 上的任务集合"
关键点:只能是就绪态(Runnable) ,不包括 sleep / IO 阻塞 ,调度器只从 runqueue 里选人
三、一个非常重要的事实:每个 CPU 都有自己的 runqueue
这是理解多核调度的核心 ⚠️
CPU0 → runqueue0
CPU1 → runqueue1
CPU2 → runqueue2
...
为什么不是全局一个?
- 减少锁竞,提高并发,提升 cache 命中率
👉 Linux 的调度是"以 CPU 为中心"的,而不是"以进程为中心"的
四、runqueue 的内部结构(重点)
在内核里(struct rq):
1️⃣ runqueue ≈ 一个大容器
它不是一个简单队列,而是:
runqueue
├── CFS runqueue(普通进程)
├── RT runqueue(实时进程)
├── DL runqueue(deadline 进程)
└── 当前正在运行的任务
2️⃣ CFS runqueue(最重要)
-
类型:
cfs_rq -
内部数据结构:红黑树
-
排序依据:
vruntimecfs_rq
└── 红黑树
├── task A (vruntime=10)
├── task B (vruntime=20)
└── task C (vruntime=30)
👉 最左边的节点 = 最该运行的进程
3️⃣ RT runqueue
- 类型:
rt_rq,结构:优先级数组,优先级范围:0--99
特点:
- 高优先级一定先跑,可以饿死普通进程
4️⃣ DL runqueue
- 基于 EDF(最早截止时间),严格时间约束,一般系统很少用
五、调度时 runqueue 是怎么用的?
调度发生时(简化版)
-
先看 RT runqueue
- 有实时任务?👉 直接选
-
再看 DL runqueue
-
最后看 CFS runqueue
- 从红黑树取 vruntime 最小的
⚠️ 这一步顺序非常重要:
RT > DL > CFS
runqueue 中真正参与调度的部分
一、结论
在 普通 Linux 系统的真实运行状态下:
绝大多数情况下,runqueue 中真正参与调度的只有 100--139 这个优先级区间
原因一句话:
0--99 是"实时世界",普通系统默认不用,也不应该用
下面我们拆开讲清楚。
二、Linux runqueue 的"完整优先级空间"
在 O(1) 调度器时代(也是很多文档的基础):
优先级范围(数值越小,优先级越高)
0 ────────────┐
│ 实时进程(RT)
99 ────────────┘
100 ────────────┐
│ 普通进程(Normal)
139 ────────────┘
总共 140 个优先级。
三、为什么 0--99 "不用"?
1️⃣ 0--99 是为实时进程保留的
这一点是设计上的硬隔离 :SCHED_FIFO,SCHED_RR
这些进程:不受 CFS / 时间片公平约束,可以抢占一切普通进程,可以饿死系统
👉 所以:
普通用户进程,永远进不了 0--99
2️⃣ 普通系统默认没有实时进程
在现实中:桌面系统,Web 服务器,数据库服务器
👉 99% 的进程都是 SCHED_OTHER
也就是说:runqueue 的 RT 队列 通常是空的,0--99 对系统来说只是"预留区"
3️⃣ 如果你"真的用"了 0--99,会发生什么?
举个非常现实的后果:
chrt -f 99 ./while_true
可能导致:SSH 断连,shell 卡死,watchdog 触发系统假死甚至重启
👉 所以 0--99 在理念上是:
"你要非常清楚自己在干什么,才能用"
四、100--139:runqueue 的"真实世界"
1️⃣ 普通进程全部映射到 100--139
对普通进程来说:
nice: -20 → 100
nice: 0 → 120
nice: 19 → 139
所以:
你在 top / ps 里看到的绝大多数 PRI,都会在 100--139 之间
2️⃣ runqueue 的真实状态长这样
在一个典型系统中:
runqueue
├── RT queue (0--99) → 空
├── Normal queue
│ ├── prio 100
│ ├── prio 101
│ ├── ...
│ ├── prio 120 ← ★绝大多数进程
│ ├── ...
│ └── prio 139
└── expired queue(O(1) 时代)
👉 真正活跃、频繁操作的,就是 120 附近
五、为什么偏偏是 100--139?
这不是随便定的。
1️⃣ 设计目的:隔离实时与普通
-
实时:绝对优先
-
普通:相对公平
用一个"硬边界"隔开:
99 / 100
👉 内核逻辑非常清晰。
2️⃣ 给 nice 值留足空间
nice 有:
-20 ~ +19 → 共 40 个等级
而普通优先级正好:
100 ~ 139 → 40 个等级
👉 一一对应,计算简单,调试友好
六、结合 runqueue 的"真实调度顺序"
无论是 O(1) 时代还是后来:
调度器的选择顺序都是:
1. RT runqueue(0--99)是否有任务?
└── 有 → 立刻调度
2. 普通 runqueue(100--139)
└── 再谈公平
👉 这也是为什么:
一旦 RT 进程出现,普通进程"感觉优先级全失效"
这是设计,不是 bug。
七、一个"真实系统"的直觉总结
你可以这样理解 Linux 的 runqueue:
99 层楼的"应急通道"(0--99),平时没人走;
40 层楼的"日常办公区"(100--139),才是系统每天真正运转的地方。在 Linux 的 runqueue 中,0--99 的优先级专用于实时进程,普通系统默认不会使用;
真实运行时,几乎所有普通进程都集中在 100--139 这一段优先级范围内,
调度器在这一范围内通过时间片(O(1))或 vruntime(CFS)来实现相对公平的 CPU 分配。
O(1)调度
"活跃进程 / 过期进程 + 队列翻转 + O(1) 调度"
👉 指的是 Linux 早期的 O(1) 调度器,而不是 CFS:
active / expired 队列 ≠ CFS ,它们属于 Linux 2.6 早期(~2.6.23 之前) 的经典设计,后来被 CFS 完整取代
但理解它 非常有价值,因为:能看清 Linux 调度设计的"进化逻辑"很多教材 / 面试题 / 老文章还在讲它
下面我会完整讲 O(1) 调度器的机制 ,并在最后告诉你 它为什么被 CFS 干掉。
一、O(1) 调度器要解决什么问题?
在 O(1) 调度器之前(2.4 内核):
调度要遍历进程列表,时间复杂度:O(N),进程一多,调度就慢
👉 O(1) 调度器的目标:
无论有多少进程,选下一个进程的时间都是常数级 O(1)
二、核心设计:两套队列
O(1) 调度器的核心结构:
runqueue
├── active queue (活跃队列)
└── expired queue (过期队列)
每个队列里有什么?
每个队列本身不是一个队列,而是:
priority_array
├── queue[0] ← 最高优先级
├── queue[1]
├── ...
├── queue[139] ← 最低优先级
└── bitmap ← 哪些优先级非空
-
一共 **140 个优先级,**0--99:实时进程,100--139:普通进程
-
每个优先级一个 FIFO 链表
三、什么是"活跃进程"和"过期进程"?
1️⃣ 活跃进程(active)
还有时间片可以用的进程
特点:可以被立即调度,调度器只从 active 里选进程
2️⃣ 过期进程(expired)
时间片已经用完的进程
特点:暂时不会再被调度,等下一轮"翻转"才有机会
四、进程是如何插入 active 队列的?
1️⃣ 新进程(fork)
当一个普通进程创建:
-
根据静态优先级(由 nice 决定)
-
分配一个 时间片(timeslice)
-
插入 active 队列对应优先级链表
active[prio] → task
2️⃣ 被唤醒的进程(sleep → wakeup)
-
如果 还有时间片
- 👉 插入 active
-
如果 时间片已经用完
- 👉 插入 expired
五、调度时是如何做到 O(1) 的?
关键点:bitmap(位图)
每个 priority_array 有一个 bitmap:
bitmap[140]
-
1:该优先级有进程
-
0:没有
调度流程:
-
找 bitmap 中 最高优先级的 1
- 这是一个常数时间操作(位运算)
-
直接取该优先级链表的第一个进程
-
完成调度
👉 完全不依赖进程总数 → O(1)
六、进程什么时候进入 expired 队列?
当进程运行时:
-
每跑一个 tick,减少时间片
-
当时间片耗尽:
task.timeslice == 0
👉 进程被移出 active
👉 插入 expired 队列(同优先级)
七、active / expired 队列如何"翻转"?
这是 O(1) 调度器最经典的设计 🌟
翻转条件
当:
active queue 为空
说明:
-
当前这一"轮"所有进程
-
都已经用完时间片
翻转操作(非常简单)
swap(active, expired);
也就是:
旧 expired → 新 active
旧 active → 新 expired(空)
⚠️ 注意:
- 不是拷贝 ,只是交换指针,成本 O(1)
八、为什么这样就"公平"了?
用一句非常直观的话:
每个进程在一轮中最多跑一个时间片
- 跑完 → 进 expired,等别人也跑完,下一轮再一起开始
这是一种:
"轮次公平"
九、nice 值在 O(1) 调度器里的作用
nice 影响两件事:
-
静态优先级
- 决定在哪个优先级队列
-
时间片长度
-
nice 越小 → 时间片越长
-
nice 越大 → 时间片越短
-
👉 高优先级进程:跑得久,跑得早
十、一个完整例子(非常重要)
假设有 A、B、C:
| 进程 | nice | 优先级 | 时间片 |
|---|---|---|---|
| A | 0 | 120 | 100 |
| B | 0 | 120 | 100 |
| C | 0 | 120 | 100 |
第一轮:
-
A 用完 → expired
-
B 用完 → expired
-
C 用完 → expired
active 空了
翻转:
expired → active
第二轮开始
👉 所有进程重新"公平起跑"
十一、那为什么 O(1) 调度器被 CFS 取代?
这是关键的"历史转折"。
O(1) 的问题
-
**时间片是离散的,**不是真正"连续公平"
-
**交互进程体验不好,**刚唤醒的进程可能要等一整轮
-
**调参复杂,**时间片、优先级、启发式规则非常多
-
公平是"轮级"的,不是"时间级"的
CFS 的改进思路
| O(1) | CFS |
|---|---|
| 时间片 | vruntime |
| 轮次公平 | 连续公平 |
| active/expired | 红黑树 |
| 静态策略多 | 动态计算 |
👉 CFS 用"欠账模型"替代了"轮次模型"
十二、总结
O(1) 调度器通过 active / expired 两套优先级队列和位图结构,使进程调度、插入和翻转都能在常数时间内完成,从而实现 O(1) 复杂度;
但由于其公平性是基于"时间片轮次"而非"实际运行时间",在交互性和精细公平性上存在不足,最终被 CFS 的 vruntime + 红黑树模型所取代。