news 2026/9/4 6:38:20

Re:Linux 系统篇(十六):进程篇(五):O (1) 调度算法深度解析 —— 优先级数组、位图优化与活跃 / 过期双队列

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Re:Linux 系统篇(十六):进程篇(五):O (1) 调度算法深度解析 —— 优先级数组、位图优化与活跃 / 过期双队列

观众老爷们大家好 这里是邪修KING的独家频道
本文属于系列Linux系统篇 ——操作指令
一起学Linux的小伙伴可订阅专栏: Linux系统篇

上一篇我们讲透了进程上下文切换的完整流程。切换谁、选哪个进程上 CPU,这件事由调度器决定。

早期 Linux 调度器很简单:所有就绪进程放一个链表,每次调度遍历整个链表找优先级最高的。进程少还好,进程多了,每次调度都要遍历一遍,复杂度 O (n),调度越来越慢。

于是经典的O (1) 调度算法横空出世:无论系统里有多少个进程,选择下一个进程的时间永远是固定的,不随进程数增长而变慢。
本篇我们从 O (n) 的痛点讲起,一步步拆解位图优化、优先级数组、活跃 / 过期双队列设计,彻底搞懂 O (1) 的核心精髓。


一、进程调度的核心诉求与优先级数组

1.1 调度的核心工作

调度器的核心任务只有一个:

从就绪队列里,选出下一个应该上 CPU 运行的进程。

评判调度器好不好,两个关键指标:

  1. 调度速度:选下一个进程要多久
  2. 公平性:会不会有进程饿死,响应及不及时

1.2 为什么要有进程优先级?

进程不是人人平等的。
比如交互程序(点击鼠标、打字)需要快速响应,优先级要高;后台编译、下载可以慢一点,优先级低。
Linux 给每个进程分配了优先级,调度器优先选高优先级的进程。

1.2.1 区分:优先级与权限

很多人混淆优先级和权限:

  • 优先级:调度层面的,决定进程能不能抢到 CPU 时间
  • 权限:系统资源层面的,决定能不能访问文件、能不能执行特权操作

root 用户的进程权限高,但不代表优先级一定高;普通用户也可以把自己进程的优先级调高调低(在允许范围内)。

1.2.2 UID 与优先级无关

UID 是用户 ID,是权限标识,和调度优先级是两个维度。
root 可以调整进程优先级到更高范围,但不是 root 进程就一定优先级高。

1.3 进程优先级的正面作用

  • 高优先级进程获得更多 CPU 时间,响应更快
  • 重要任务优先执行,保证核心业务体验
  • 低优先级后台任务闲时运行,充分利用 CPU

1.4 Linux 下查看进程优先级

1.4.1 使用 ps -l 命令查看
ps -l

输出里两个核心指标:

  • PRI:进程最终优先级,数值越小优先级越高
  • NI:Nice 值,用户可以调整的友好值,范围 - 20 到 19,越小优先级越高
1.4.2 核心指标:PRI 与 NI

最终优先级 = 基础优先级 + Nice 值偏移。
Nice 值是用户能控制的部分:

  • Nice=-20:最高优先级加成
  • Nice=0:默认
  • Nice=19:最低优先级

1.5 进程优先级的计算公式

Linux 优先级是动态计算的,不是固定值。
调度器会根据进程的行为动态调整:

  • 总是睡觉的 IO 密集型进程:自动提升优先级,因为它平时不占 CPU,响应要快
  • 一直死循环的 CPU 密集型进程:自动降低优先级,因为它一直占 CPU

Nice 值是用户设定的基准偏移,在动态计算的基础上叠加。

1.6 Nice 值的取值范围与限制原因

Nice 值范围是-20 到 19,共 40 档。
为什么范围这么小?
因为优先级分档太多的话,调度复杂度上升,且差别太小没意义。40 档足够区分不同类型的进程了。

普通用户只能调大 Nice 值(降低优先级),不能调小(提升优先级),防止普通用户把自己进程调最高霸占 CPU。
root 用户可以调整全范围。

1.7 优先级修改方法

1.7.1 命令行工具
  • nice:启动程序时指定 Nice 值
nice -n -5 ./myprocess # 启动时设置Nice为-5
  • renice:修改已经运行的进程的 Nice 值
renice -10 -p 进程PID
1.7.2 系统调用

程序里可以用nice()系统调用修改自己的优先级。

1.7.3 通过 top 命令交互式修改

top 里按r,输入 PID,再输入 Nice 值,即可修改。


二、O (n) 调度的痛点:引出位图优化

2.1 传统的顺序遍历困境

最早的调度器:所有就绪进程串成一个链表。
每次调度:从头遍历整个链表,找出优先级最高的进程。

  • 10 个进程:遍历 10 次,很快
  • 1000 个进程:遍历 1000 次,调度变慢
  • 10000 个进程:遍历 10000 次,调度开销大到不可接受

时间复杂度 O (n),进程越多,调度越慢。服务器上几百上千进程很常见,这种调度器性能跟不上。

2.2 性能破局:引出位图技术

核心思路:按优先级分组排队
优先级一共就几十档,我们给每个优先级单独建一个队列。同优先级的进程,放在同一个队列里。

然后用一个位图(bitmap)来标记:哪个优先级队列里有进程。

  • 比如第 0 位是 1,代表优先级 0 的队列有进程
  • 第 5 位是 0,代表优先级 5 的队列是空的

找最高优先级,就变成了:找位图里第一个 1 的位置

2.3 位图是什么?

位图就是用整数的每一位来表示一个状态。
比如一个 32 位整数,可以表示 32 个优先级的空 / 非空状态。
Linux 有 140 个优先级,用两个 64 位整数就能全部表示。

2.4 为什么找第一个 1 是 O (1)?

CPU 硬件有专门的指令:bsf(Bit Scan Forward,位扫描向前)。
一条 CPU 指令,就能直接返回位图里第一个 1 的位置,不管位图多大,都是一条指令搞定,时间固定。

所以:

  • 找最高优先级 = 一次 bsf 指令 → O (1)
  • 找到对应优先级队列,取第一个进程 → O (1)

整个调度过程,无论多少进程,时间都是固定的,这就是 O (1) 名字的由来。


三、活跃队列与过期队列

3.1 为什么要两个队列?

只有一个优先级队列会有问题:
高优先级进程源源不断,低优先级进程永远轮不到,直接饿死。
比如一直有高优先级的 IO 进程醒来,CPU 永远被它们占着,低优先级的后台计算进程永远跑不到。

所以 O (1) 调度设计了两个数组:活跃数组(Active Array)和过期数组(Expired Array)。

3.2 活跃队列(Active Array)

当前这一轮,所有时间片没用完的进程,都在活跃数组里。
调度器每次都从活跃数组里选进程。
进程时间片用完了,就从活跃数组里拿出来,放到过期数组里。

3.3 过期队列(Expired Array)

已经用完时间片的进程,都放在过期数组,等待下一轮。
当活跃数组里所有进程都跑完了,空了,就把两个数组交换:

  • 过期数组 变成 新的活跃数组
  • 原来的活跃数组(空了)变成 新的过期数组

然后开始新一轮调度。

3.4 指针交换:O (1) 的互换

两个数组交换,不是把所有进程挪来挪去,那又变成 O (n) 了。
Linux 的做法很巧妙:交换指针
两个数组各有一个指针指向自己,交换的时候,只交换两个指针的值,O (1) 操作,瞬间完成。

💡 类比理解:
两个篮子,A 篮装当前轮的乒乓球,B 篮装打完的。
A 篮空了,不用把球一个个从 B 搬到 A,直接把两个篮子的标签互换,A 变 B,B 变 A,开始下一轮。

3.5 完整流转梳理

  1. 初始:所有进程分配好时间片,全部放入活跃数组
  2. 调度器从活跃数组选最高优先级进程,运行
  3. 进程时间片用完,移出活跃数组,加入过期数组
  4. 活跃数组不为空,回到步骤 2 继续
  5. 活跃数组空了,交换活跃和过期数组指针,开始新一轮

3.6 设计优势

  • 保证公平:每一轮所有进程都跑完,才开始下一轮,不会有进程永远轮不到
  • 性能恒定:所有操作都是 O (1),不随进程数增加变慢
  • 支持动态优先级:每一轮重新计算时间片和优先级,灵活调整

四、周边问题

4.1 新进程来了怎么办?

新创建的进程,直接放到过期数组里,等当前轮结束,下一轮再参与调度。
好处:不打乱当前轮的秩序,保证当前轮所有进程公平跑完;新进程不会一进来就抢占,避免调度抖动。

4.2 调度队列其他元素

每个 CPU 都有自己独立的调度队列(runqueue)。
多核系统里,每个 CPU 自己调度自己的队列,不用全局抢锁,减少竞争,提升多核性能。

每个 runqueue 里就是:

  • 一个活跃优先级数组
  • 一个过期优先级数组
  • 对应的位图
  • 调度相关统计信息

4.3 优先级与调度算法的关系

  • 优先级决定了进程在哪个优先级队列,决定了被选中的先后
  • 调度算法(O (1))决定了怎么高效选出最高优先级的进程

优先级是规则,调度算法是高效执行规则的方法。


五、O (1) 调度的核心设计总结

表格

设计点作用复杂度
按优先级分队列同优先级排队,优先级之间独立-
位图 bitmap标记哪个优先级有进程,bsf 指令快速找最高优先级O(1)
活跃 + 过期双数组保证每轮公平,防止饥饿O (1) 指针交换
每个 CPU 独立 runqueue减少多核锁竞争,提升并行性-

O (1) 调度的精髓就是:用空间换时间,用分组、位图、双队列的设计,把调度操作从 O (n) 降到 O (1),让 Linux 在大负载、多进程场景下,调度性能依然稳定。

补充:O (1) 是 Linux 2.6 内核的经典调度器。后来的 CFS 完全公平调度是更现代的调度器,但 O (1) 里的位图、双队列、每 CPU 队列等设计思想,依然是调度算法的经典,也是理解现代调度的基础。


全文总结

  1. 调度核心:从就绪进程里选下一个上 CPU,核心是速度和公平。
  2. 优先级:进程调度的权重,Nice 值用户可调,最终优先级动态计算。
  3. O (n) 痛点:遍历链表选最高优先级,进程越多越慢。
  4. 位图优化:按优先级分队列,位图标记非空队列,硬件 bsf 指令找最高优先级,O (1) 选出。
  5. 活跃 / 过期双队列:每轮跑完交换指针,保证公平,防止饥饿,交换 O (1)。
  6. 每 CPU 队列:多核独立调度,减少锁竞争,提升性能。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 6:38:15

手机随拍实战:从港口船舶记录到专业纪实短片制作全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 6:35:11

Simulink同步发电机励磁与调速控制系统仿真实践指南

简介:本资源是一套面向电气工程专业本科生课程设计与毕业设计的同步发电机控制系统实践方案,聚焦励磁调节与原动机调速两大核心控制问题,帮助学习者掌握电力系统动态建模、反馈控制设计及Simulink工程实现能力。压缩包共19个文件,…

作者头像 李华
网站建设 2026/9/4 6:35:10

EviAnn v2.0.5 安装与使用--生信工具111

EviAnn—— 基于证据的真核生物基因组注释软件 EviAnn(Evidence Annotation)是一款全新的基因组注释软件,完全基于证据驱动。EviAnn 利用 RNA‑seq 数据、转录本序列以及近缘物种蛋白比对结果,完成蛋白编码基因与长非编码 RNA 的…

作者头像 李华
网站建设 2026/9/4 6:34:42

Claude访问真实系统事件复盘:权限边界与沙箱隔离

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 6:34:02

行情源异常监控告警系统实践:第一时间发现价格源故障

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 6:32:41

Claude Code 实战指南:从零构建一个 AI 编程街机厅

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华