
目录
[2. 操作系统的调度算法](#2. 操作系统的调度算法)
[2.1 运行队列的结构](#2.1 运行队列的结构)
[2.2 active和expired的比较](#2.2 active和expired的比较)
[2.3 O(1)调度器的弊端](#2.3 O(1)调度器的弊端)
1.进程优先级
进程优先级是调度器决定下一个谁运行的核心数据,数值越小,优先级越高。
实时进程的优先级最高,数值为0-99。
普通进程可以通过nice renice调整优先级。对于普通用户nice的范围是0-19,超级用户还可以设置-20至-1
cs
创建新进程时设置优先级
nice -n -5 ./my_server
nice -n 19 ./backup.sh
renice用于修改已存在进程的nice值
cs
# 按 PID 调整
renice -n -10 -p 1234 # 将 PID=1234 的进程 NI 改为 -10
# 按用户/进程组批量调整
renice -n 5 -u username # 该用户所有进程 NI +5
renice -n 0 -g 5678 # 进程组 GID=5678 的所有进程 NI 设为 0
nice值在操作系统内部存储时还要加上默认值,通常为120,最后的范围是100-139
在 Linux 内核头文件 include/linux/sched/prio.h 中,有如下宏定义:
cs
#define MAX_RT_PRIO 100 // 实时优先级最大值(不含)
#define MAX_PRIO 140 // 总优先级数量
#define DEFAULT_PRIO (MAX_RT_PRIO + NICE_WIDTH / 2) // 即 100 + 40/2 = 120
#define NICE_WIDTH (MAX_PRIO - MAX_RT_PRIO) // 即 140 - 100 = 40
2. 操作系统的调度算法
2.1 运行队列的结构
一个CPU,一个运行队列(struct runqueue),核心有两个优先级数组 active和expired,数组的结构如下
cs
struct prio_array {
unsigned int nr_active; // 该数组中的进程总数
unsigned int bitmap[BITMAP_SIZE]; // 优先级位图
struct list_head queue[MAX_PRIO]; // 140个优先级链表
};
unsigned int类型一共有32个比特位,数组大小通常为5,共有160个比特位,正好对应140个优先级,每个位为1代表这个位置有进程
nr_active是一个计数器,用于计算进程数,判断系统负载
struct list_head queue是140个优先级链表,每个优先级对应一个双向链表头,挂载该优先级上的所有进程
抽象图如下

通过位图的方法能够快速找到下一个等待执行的进程,从而达到了O(1)级别。
2.2 active和expired的比较
调度器永远只从 active 数组中选取下一个要运行的进程。
expired不参与调度选择,它只是一个**等待区。**存放那些本轮时间片已耗尽、但尚未获得新一轮时间片的普通进程
当nr_active变成0时,交换两个数组的指针,expired变成active继续被调度。上一轮的active变成expired自动刷新进入新一轮
nr_active变成0意味着当前 CPU 的 active 数组中没有任何处于可运行状态的进程
通常由四种场景触发
1.正常耗尽,时间片用完被移入expired
2.主动阻塞,进程进入睡眠/等待状态,直接移出运行队列
3.进程终止,调用exit()或被信号杀死,移出运行队列
4.在多核系统中负载均衡机制,进入其他cpu的运行队列
其中第二种情况最为常见
完整的生命周期
cs
新进程 ──→ active
│
├── 被调度执行 → 时间片耗尽(非交互) ──→ expired
├── 被调度执行 → 时间片耗尽(交互式) ──→ 重回 active
├── 未轮到执行 → 主动睡眠/阻塞 ──→ 移出运行队列(不进expired)
├── 未轮到执行 → 被迁移到其他CPU ──→ 移出本CPU active
└── 被更高优先级抢占 → 留在 active 原位(保留剩余时间片)
当且仅当 active->nr_active == 0 时:
swap(active, expired)
// 原 expired 中所有进程自动获得新一轮时间片
// 注意:这些进程可能在上轮也没全部跑过!
创建两个优先级数组也是一种空间换时间的方法。
2.3 O(1)调度器的弊端
如果一直fork创建新进程,那nr_active永远无法变为0,expired中的老进程就会被饿死。如果一直有高优先级抢占进程,且没有进程阻塞和退出,那理论上也会存在进程饥饿的问题。
为了解决,Linux 2.6.23 引入CFS时,彻底废弃了 active/expired 双数组结构,从根本上消除了"指针交换"这个前提条件:
- 没有"批次"概念,每个进程独立按 vruntime 排序
- 新创建的进程继承父进程的 vruntime,并加上一个小的偏移量(防止 fork 轰炸获得不公平优势)
- 无论有多少新进程涌入,红黑树中最左节点(vruntime 最小的进程)总是会被选中
- 老进程的 vruntime 不会因为"没轮到交换"而停滞,它们只是在树中位置相对靠右,但仍然有确定的、可计算的调度机会
终