Linux的进程调度及内核实现
本文主要介绍Linux进程调度及内核实现,顺序如下:进程调度的关键数据结构->调度的不同方式->调度触发方式和进程间切换过程
一、进程管理的关键数据结构
1.1 task_struct
task_struct是内核描述进程的结构体,包括了调度策略、优先级等信息
struct task_struct {// 1. 调度策略:决定进程属于哪种调度类(实时/公平/空闲)unsigned int policy; // 如 SCHED_NORMAL(公平)、SCHED_RR(实时)// 2. 优先级相关:静态/动态/实时优先级int static_prio; // 静态优先级(用户通过nice设置,100-139)int normal_prio; // 常规优先级(由static_prio和policy计算)int rt_prio; // 实时优先级(0-99,值越小优先级越高)int prio; // 当前动态优先级(可能因优先级继承临时提升)// 3. 调度类:指向进程所属的调度类(如fair_sched_class)const struct sched_class *sched_class;// 4. 调度实体:封装进程的调度信息(如vruntime、权重)struct sched_entity se; // 公平调度(CFS)的实体struct sched_rt_entity rt; // 实时调度的实体// 5. 运行队列关联:进程当前归属的CPU运行队列struct rq *on_rq; // 非NULL表示进程在就绪队列中// 6. 抢占标志:是否需要调度(由内核设置,schedule()检查)unsigned int flags; // 含 TIF_NEED_RESCHED(需调度)// ... 其他字段(内存、文件、信号等)
};
优先级规则:实时进程>普通进程
实时进程(rt_prio)值越小优先级越高
普通优先级(static_prio)值越小优先级越高
动态优先级(prio):默认等于normal_prio,但会因优先级继承临时提升,避免 “优先级反转”
1.2 sched_class调度类
Linux通过调度类实现对不同调度策略的封装,本质是一个含函数指针的结构体,定义调度的一些核心操作(入队、出队等)
struct sched_class {// 1. 调度类名称(如"fair"、"rt")const char *name;// 2. 入队:将进程加入就绪队列(如进程唤醒后)void (*enqueue_task)(struct rq *rq, struct task_struct *p, int flags);// 3. 出队:将进程从就绪队列移除(如进程阻塞、退出)void (*dequeue_task)(struct rq *rq, struct task_struct *p, int flags);// 4. 选择下一个要运行的进程(调度器核心逻辑)struct task_struct *(*pick_next_task)(struct rq *rq);// 5. 切换前准备:将当前进程放回就绪队列(如时间片用完)void (*put_prev_task)(struct rq *rq, struct task_struct *p);// 6. 检查是否需要抢占当前进程void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags);// ... 其他函数(如任务创建、优先级修改)
};
调度类按优先级从高到低排列如下
stop_sched_class | 用于停止CPU |
dl_sched_class | 用于调度截止时间策略的任务 |
rt_sched_class | 用于调度实时策略的任务 |
fair_sched_class | 用于调度公平调度策略的任务 |
idle_sched_class | 每个CPU上有一个空闲任务,无其他任务运行时会运行该空闲任务 |
1.3进程状态
二、核心调度策略与算法实现
2.1公平调度(CFS)
CFS是linux默认调度器,核心思想是假设有N个进程,每个进程都获得1/N的CPU时间,实际实现是通过vruntime计算进程虚拟运行时间,优先调度vruntime最小的进程,确保每个进程的vruntime增长速率与权重匹配。
计算公式为vruntime+=实际运行时间*N/进程权重(N表示系统CPU数量),
CFS调度器采用动态时间片(每个进程运行的时间),时间片长度由调度延迟和进程权重决定
时间片长度=(进程权重/总权重)*调度延迟
调度延迟指所有就绪进程轮询一次的总时间
例如:
单核CPU上有两个权重(weight)相同的进程,总权重为2weight,调度延迟=6ms。
那么每个进程的时间片=(weight/2weight)*6ms=3ms.
CFS采用红黑树数据结构组织可运行的调度实体,键为调度进程的vruntime,最左节点为整棵树中vruntime值最小的节点,即下一个应该被运行的任务。
当任务被唤醒或状态改变时,会被插入到红黑树中,当任务被调度运行或因各种事件离开时,会从树中被移除。
为什么要用红黑树管理调度任务,而不直接用二叉搜索树,是因为红黑树在频繁的动态插入和删除过程中,通过自身规则自动保持树的近似平衡,防止退化成链表,从而保证操作的时间复杂度为O(logn),用二叉搜索树的话,在极端情况下退化成链表,导致操作的时间复杂度变为O(n),使性能不稳定。
2.2实时调度器(RT)
负责调度SCHED_FIFO和SCHED_RR策略的任务
SCHED_FIFO采用先进先出实时调度,无时间片概念,进程一旦获得CPU会一直占用直到自己主动放弃或被更高优先级抢占。
SCHED_RR采用时间片轮转调度,运行时间片(默认 100ms)后,若有同优先级的就绪进程,会被放到该优先级链表末尾,让下一个同优先级进程运行,仍支持被更高优先级的实时进程抢占。
实时调度器为每个优先级维护了一个队列数组,还使用了位图来表示对应优先级的队列是否非空(每个比特位bit表示空或非空二元状态),调度器通过查找位图,可以快速地找到最高优先级的非空队列,然后从该队列的头部取出任务运行。
2.3截止时间调度
每个进程需指定三个参数:
runtime(每次周期内需要的CPU时间,如10ms)、period(进程的运行周期,如100ms表示每100ms需要10msCPU运行时间)、deadline(截止时间,如周期结束前10ms,值就等于100ms-10ms)。
优先调度截止时间最早的进程,确保进程在截止时间前完成。
三、调度触发方式与进程间切换
调度器不会主动运行,需要通过触发事件触发schedule()(调度主函数)。
3.1主动调度-进程主动放弃CPU
进程阻塞 | 调用sleep()、wait()、poll()等 | 进入可中断睡眠态或不可中断睡眠态,内核在进程阻塞前调用schedule() |
主动让出CPU | 调用sched_yield() | 进程仍为就绪态,但会重新被插入就绪队列尾部,触发调度 |
进程退出 | 调用exit() | 内核调用schedule选择新进程 |
3.2被动调度-内核强制触发调度
(1)时钟中断
Linux内核每隔1/HZ秒触发一次时钟中断(例如HZ=1000时,每隔1ms触发一次),时钟中断处理函数timer_interrupt会调用scheduler_tick()
TIF_NEED_RESCHED是一个标志表示当前进程需要进行调度。
(2)进程唤醒
当进程从睡眠态被唤醒,调用wake_up_process():
将进程状态设为TASK_RUNNING->调用enqueue_task()将进程加入就绪队列->调用check_preempt_curr()(若唤醒的进程优先级高于当前运行进程或CFS中vruntime更小,设置TIF_NEED_RESCHED)。
(3)优先级变化
用户通过nice()、renice()、sched_setscheduler()修改进程优先级时,内核会调用resched_curr(),设置TIF_NEED_RESCHED。
3.3调度主函数_schedule()
_schedule()是调度器的核心函数,主要逻辑如下:
(1)获取当前CPU的运行队列(rq)和当前正在运行的任务(prev)
(2)根据是主动调度还是被动抢占,更新 上下文切换计数(prev->nvcsw或prev->nivcsw)
(3)调用pick_next_task()函数,按照调度类的优先级顺序(stop>dl>rt>fair>idle)选择下一个要运行的进程
(4)如果选出的next任务和当前运行的prev任务不同,则调用context_switch()执行上下文切换
(5)切换完成后,重新开启抢占
3.4上下文切换(context_switch())
简而言之上下文切换是把当前运行进程的信息保存好,把下一进程的信息拿出来的过程。
上下切换:
页表切换,切换进程的地址空间(CR3寄存器,指向页全局目录PGD),确保CPU访问的是下一个进程的内存
寄存器切换,保存当前进程的栈指针、程序计数器等寄存器到task_struct,恢复下一进程的寄存器,完成CPU使用权的交换。