You are on page 1of 15

Linux 进程调度算法分析

摘 要 : 基于 X86 平台 Linux2.6.26 内核进程调度部分代码,刨析 Linux 进程调度算法,对

算法的原理,实现和复杂度进行了分析并提出了算法改进措施。

关键字:Linux 内核 进程调度 算法

1. Linux 进程调度概述

Linux 系统支持用户态进程和内核线程,需要说明的是,Linux 没有提供用户态线程支

持,实现用户态线程需要引入第三方线程库。

操作系统进程调度是整个操作系统理论的核心,在设计进程调动机制需要考虑的具体

问题主要有:

1)调度的时机:在什么情况下,什么时候进行调度。

2)调度的“政策”(policy):根据什么准则挑选下一个进入运行的进程。

3)调度的方式:是“可剥夺”(preemptive)还是“不可剥夺”(nonpreemptive)。

图 1.2.1 给出了 Linux 进程状态转换关系:

fork ()

TASK _RUNNING
收到信号 就绪 等待资源到位
SIG _CONT wake _up()
wake _up() 等待资源到位 或收到信号
wake _up () wake _up_interruptible

TASK _UNINTERRUPTIBLE TASK _INTERRUPTIBLE


深度睡眠 浅度睡眠

schedule () 时间片到
等待资源
sleep _on () 等待资源
schedule () 占有 CPU
sleep _on_interruptible ()
执行
schedule ()

TASK _STOPPED TASK _ZOMBIE


暂停 死亡但户口未注销
图 1 Linux 进程状态转换图

Linux 进程调度分为自愿调度和强制调度两种。1)在内核空间,一个进程可以通过

schedule() 启 动 一 次 调 度 , 也 可 以 在 调 用 schedule() 之 前 , 将 本 进 程 状 态 设 置 为

TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE,暂时放弃运行而进入睡眠。这通

常发生在来自用户空间的系统调用被阻塞。在用户空间,用户进程可以通过系统调用

nanosleep()达到目的。2)调度还可以是非自愿的。在一定条件下,内核会强制性剥夺当前进

程运行而调度其他进程进入运行。

Linux 调度政策基础是时间片轮转+优先级抢占的结合,为了满足不同应用的需要,内

核提供了三种调度方法: 1)SCHED_FIFO 实时调度策略,先到先服务 2)SCHED_RR 实

时 调 度 策 略 , 时 间 片 轮 转 3 ) SCHED_NORMAL 分 时 调 度 策 略 ( 在 2.6 内 核 以 前 为

SCHED_OTHER)。用户进程可以通过系统调用 sched_setscheduler()设定自己的调度策略。

SCHED_FIFO 和 SCHED_RR 的区别是,前者只有在就绪队列中有优先级更高的进程,或

进程被阻塞,或自愿调用阻塞原语(如 sleep_on_interruptible)的情况下,才会放弃 CPU,

而如果调度策略是后者,当前进程与就绪队列里其他进程按 Round Robin 方式共享 CPU。

2. Linux 进程调度原理

基本的操作系统进程调度算法包括先来先服务( first come first serve),时间片轮转

(round robin),多级反馈轮转法(round robin with multiple feedback),优先级法(静态

优 先 级 法 / 动 态 优 先 级 法 ) , 短 作 业 优 先 法 ( shortest job first ) , 最 高 响 应 比 优 先 法

(highest response_ratio next)。不同调度算法应用场合不同,某些调度算法可能仅具有研究

价值,实际中鲜有应用;而某些调度算法需要互补以完成设计需求。但是,无论哪种进程调

度算法,都要面对以下实际问题:

1)调度器对实时进程的响应;

2)调度器的调度开销,以及系统进程负载对调度的影响;

3)在 SMP 环境下,当前 CPU 调度对其他 CPU 的影响;

Linux2.6.x 内核进程调度算法为解决上述问题,设计了全新的数据结构和调度算法,

但其基本策路仍是以优先级为基础的抢占式调度,与 2.6 以前内核版本不同,内核抢占可

能发生在内核态(因此,2.6 版本的内核代码必须考虑到重入问题)。

2.6 版本的内核调度也是几度变迁,其基本思想是 1)提高实时进程调度相应比 2)普


通进程调度体现“完全公平这个思想”。从 2.6 早期版本 SD(Staircase Schedule)调度器,到

2.6.23 版本的 RSDL(The Rotating Staircase Deadline Schedule)调度器,再至 2.6.26 版本

CFS(Complete Fair Schedule)调度器,调度机制不断完善。2.6.26 内核进程调度吸收了前

期版本的精华,通过全新设计数据结构和算法,为实时进程(SCHED_FIFO/SCHED_RR)

提供 O(1)时间复杂度的调度算法,同时,为了兼顾“完全公平”这一设计思路,设计了

CFS 调度器,为普通进程提供满足公平性为原则的 O(lgn)时间复杂度的调度算法。因此,

准确地说,2.6.23 以后的版本进程调度是基于 O(1)+ O(lgn)时间复杂度的调度。基于这

两部分的设计和 Linux 内核代码实现将在本文给与介绍。

2.1 基于实时进程调度
Linux2.4 内核维护双向循环队列 runqueue,一旦调度时机触发,内核重新计算当前队

列中所有进程运行权值,并从中挑选出权值最高的进程作为当前进程投入运行。其弊端是显

而易见的:

1)调度时机触发,重新计算 runqueue 中每个进程运行权值,复杂度为 O(n), 且调度

性能与内核负载相关。

2)runqueue 同时管理着实时进程与非实时进程(普通进程),内核通过进程属性,如

实时或非实时、实时进程优先级、用户进程或内核线程相关因素来计算运行权值 count,灵活

性低,且不便于理解和维护。

从 Linux2.6 早期版本开始,内核进程对实时进程调度重新设计了 O(1)调度器——

SD/RSDL,RSDL 调度器是在 SD 调度器基础上的改进。Linux2.6.26 内核在早期 2.6 内核基

础上简化了 RSDL 调度器,把就绪进程队列和过期进程队列合并为就绪队列。下面结合内核

代码,给与实时进程 O(1)调度器的实现(限于篇幅,本文给出核心数据结构关键成员的

注释)。

1)就绪进程队列 struct rq

struct rq {

/* ...... */

/* runqueue lock: */

spinlock_t lock;

/* 就绪队列中进程个数 */

unsigned long nr_running;


/* ...... */

/* 普通进程就绪队列 */

struct cfs_rq cfs;

/* 实时进程就绪队列 */

struct rt_rq rt;

/* ...... */

/* 就绪队列工作时间 */

u64 clock;

/* ...... */

/* used by load_balance */

struct task_struct *migration_thread;

struct list_head migration_queue;

/* ...... */

内核为系统中每个 CPU 维护独立的 struct rq 数据结构,在 SMP 环境下,CPU 之间互

不影响。实时进程调度的核心数据结构是 struct rt_rq,定义如下:

2)实时进程就绪队列 struct rt_rq

struct rt_rq {

/* 实时进程优先级队列 */

struct rt_prio_array active;

/* 实时进程个数 */

unsigned long rt_nr_running;

/* ... */

/* 实时进程队列工作时间 */

u64 rt_time;

/* ... */

};

rt_rq 中关键的数据结构在于 prio_array_active,定义如下:

3)优先级队列 struct rt_prio_array

struct rt_prio_array {

/* 优先级位图 */

DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1);
/* 优先级队列 */

struct list_head queue[MAX_RT_PRIO];

};

4)进程运行信息结构 sched_info

struct sched_info {

/* cumulative counters */

unsigned long pcount; /* # of times run on this cpu */

unsigned long long cpu_time, /* time spent on the cpu */

run_delay; /* time spent waiting on a runqueue */

/* timestamps */

unsigned long long last_arrival,/* when we last ran on a cpu */

last_queued; /* when we were last queued to run */

};

sched_info 维护进程运行时的实时信息,代码作者的注释已比较详细,该结构数据在

schedule 进程切换发生时被更新。

struct rt_prio_array 成员 bitmap 是进程优先级队列位图,其大小是 MAX_RT_PRIO +

1,如果某优先级就绪进程队列不空,那么 bitmap 相应的位置 1,否则为 0。queue 为进程优

先级队列数组,每个进程优先级队列用双端循环链表来描述。内核寻找优先级最高的任务需

要两个简单的 BSFS 汇编指令,查询优先级队列位图,然后从优先级队列数组中取出对应

的优先级队列的对头所指向的进程,即为下一个投入运行的进程。当进程用完了自己的时间

片后,被加入 active 数组优先级队列的末尾,调度任务从当前实时任务优先级队列中取出

队首任务投入运行。实时进程调度核心数据结构之间的关系如图 2:
struct cfs _rq tasks _timeline ;
rb_leftmost ;
curr ;
next ;

struct sched _info


struct rq
pcount ;
task _struct cpu _time ;
cfs ;
se ; run _delay ;
rt _rq;
rt; last _arrival ;
last _queued ;

struct rt _rq
struct rt _prio _array

active ;
bitmap ;
rt _se;
que ue [MAX _RT _PRIO ];

图2

2.2 基于普通进程调度
Linux2.6.23 内核进程调度支持 CFS 调度器,它从 RSDL/SD 中吸取了完全公平的思想,

不再跟踪进程的睡眠时间,也不再企图区分交互式进程。它将所有的进程(普通进程)都统

一 对 待 , 这 就 是 公 平 的 含 义 。 CFS 调 度 器 使 用 红 黑 树 管 理 就 绪 进 程 , 所 有 状 态 为

TASK_RUNNING 的进程都被插入红黑树。在每个调度点,CFS 调度器都会选择红黑树的最

左边的叶子节点作为下一个将获得 CPU 的进程。由于红黑树是平衡树,因此采用 CFS 调度

器调度时间复杂度是 O(lgn)。在 CFS 中,tick 中断首先更新调度信息。然后调整当前进程在

红黑树中的位置。调整完成后如果发现当前进程不再是最左边的叶子,就标记 need_resched

标志,中断返回时就会调用 scheduler()完成进程切换。否则当前进程继续占用 CPU。从这里

可以看到 CFS 调度器带来的两点变换:1)抛弃了传统的时间片概念,进程运行权值的计

算分散到 tick 中断发生时。tick 中断只需更新红黑树,以前的所有调度器都在 tick 中断中递

减时间片,当时间片或者配额被用完时才触发优先级调整并重新调度(参见函数

update_curr()调用时机)。2)CFS 为内核抢占调度提供完美支持。

理解 CFS 的关键就是了解红黑树键值的计算方法。该键值由三个因子计算而得:一是

进程已经占用的 CPU 时间;二是当前进程的 nice 值;三是当前的 cpu 负载。CFS 调度器维

护 CPU 级 变 量 min_vruntime ; 同 时 , 每 个 进 程 维 护 进 程 级 变 量 vruntime 。 其 中 ,

min_vruntime = max (min_vruntime, vruntime),即调度前 min_vruntime 的数值和备选进程运


行时间权值的大者。进程插入红黑树的键值为 vruntime - min_vruntime。它们的差值代表了一

个进程的公平程度。该值越大,代表当前进程相对于其它进程越不公平。因此该值越大,键

值越大,从而使得当前进程向红黑树的右侧移动,越晚被选中。

以上,介绍了 Linux2.6.26 内核两种主流调度器,2.6.26 内核还为 idle 进程提供了专门

的调度器(idle_sched_class)。需要指出的是,进程调度首先选择实时进程调度器,即进程

总是以保证实时进程最高运行权限,如果系统中没有实时进程,那么才会选择 CFS 调度器

进行进程调度。

Additions notes:

1)2.6.26 内核对实时进程队列运行时间进行了限制,如果某一实时进程队列运行时间

超过最大限制,内核将限制此实时进程队列继续占用 CPU(详见 update_curr_rt 函数)。

2)2.6.26 内核进程运行时间权值的分散计算点:

a.定时器中断调用调度器计算进程调度权值

b.schedule 中进程调度点

2.6 内核对 SMP 支持的研究有待研究!

3. Linux 进程调度实现代码分析

Linux2.6.26 进程调度部分核心数据结构已在上文介绍,限于篇幅,本节对进程调度其

他核心数据结构及函数给予简要介绍。

3.1 调度器 struct sched_class


2.6.26 内核引入了调度器 struct sched_class 数据结构,提供了多种调度策略,包括:

/* fair_sched_class */

static const struct sched_class fair_sched_class;

/* rt_sched_class */

static const struct sched_class rt_sched_class;

/* idle_sched_class */

static const struct sched_class idle_sched_class;

struct sched_class 定义如下:

struct sched_class {

/* 内核支持调度器单链表 */

const struct sched_class *next;


/* ...... */

/* 根据本调度策略,选择下一个投入运行的进程 */

struct task_struct * (*pick_next_task) (struct rq *rq);

/* ...... */

/* 定时器中断调用,更新进程运行权值 */

void (*task_tick) (struct rq *rq, struct task_struct *p, int queued);

/* ...... */

/* 进程切换时对进程与本调度策略相关成员计算 */

void (*switched_to) (struct rq *this_rq, struct task_struct *task,

int running);

/* ...... */

/* 修改进程优先级,针对实时进程 */

void (*prio_changed) (struct rq *this_rq, struct task_struct *task,

int oldprio, int running);

};

3.2 进程控制结构 task_struct


struct task_struct {

/* ...... */

/* prio 动态优先级,static_prio 静态优先级,normal_prio 非实时进程优先级 */

int prio, static_prio, normal_prio;

/* 进程所属调度器 */

const struct sched_class *sched_class;

/* 进程通过 sche_entity,sched_rt_entity 链入各自的优先级进程队列 */

struct sched_entity se;

struct sched_rt_entity rt;

/* ...... */

/* 本进程调度策略 */

unsigned int policy;

/* ...... */

/* 进程调度信息 */

struct sched_info sched_info;

/* ...... */
};

3.3 实时进程运行入口 struct sched_rt_entity


struct sched_rt_entity {

/* ...... */

/* 实时进程运行时间片,用于调度策略是 SCHED_RR */

unsigned int time_slice;

/* ...... */

/* rq on which this entity is (to be) queued: */

struct rt_rq *rt_rq;

/* ..... */

};

3.4 普通进程运行入口 struct sched_entity


struct sched_entity {

struct load_weight load; /* for load-balancing */

/* 指向对应红黑树的结点 */

struct rb_node run_node;

struct list_head group_node;

unsigned int on_rq;

/* 进程调度开始运行时间 */

u64 exec_start;

/* 进程创建开始运行时间 */

u64 sum_exec_runtime;

/* CFS 考虑运行权值 */

u64 vruntime;

u64 prev_sum_exec_runtime;

/* 进程最后一次唤醒时间 */

u64 last_wakeup;

/* 进程被调度出的平均时间 */

u64 avg_overlap;

/* ..... */

/* rq on which this entity is (to be) queued: */

struct cfs_rq *cfs_rq;


/* ...... */

};

3.5 调度函数 schedule


asmlinkage void __sched schedule(void) {

need_resched:

/* 关闭内核抢占,因为此时要对内核的一些重要数据结构进行操作, 所以必须将内核

抢占关闭 */

preempt_disable();

cpu = smp_processor_id();

rq = cpu_rq(cpu); /* 获取 CPU 的进程就绪队列 */

rcu_qsctr_inc(cpu);

prev = rq->curr;

switch_count = &prev->nivcsw;

release_kernel_lock(prev);

need_resched_nonpreemptible:

/* ...... */

spin_lock(&rq->lock); /* 加锁 CPU 就绪进程队列 */

/* ...... */

if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {/* 当前进程状

态不是 TASK_RUNNING,且关闭内核抢占 */

if (unlikely(signal_pending_state(prev->state, prev))) /* 有悬挂信号待处理 */

prev->state = TASK_RUNNING; /* 重置进程状态为 TASK_RUNNING*/

else

deactivate_task(rq, prev, 1); /* 否则,当前进程退出就绪队列,递减就绪进程数,

并重新计算 CPU 的负载 */

switch_count = &prev->nvcsw;

#ifdef CONFIG_SMP /* SMP 相关处理代码,暂跳过 */

if (prev->sched_class->pre_schedule)
prev->sched_class->pre_schedule(rq, prev);

#endif

if (unlikely(!rq->nr_running)) /* 如果没有就绪进程,调用 idle_balance

在 CPU 之间平衡负载 */

idle_balance(cpu, rq);

/* 调用 put_prev_task 选择下一个可调度进程 */

prev->sched_class->put_prev_task(rq, prev);

next = pick_next_task(rq, prev);

if (likely(prev != next)) { /* 当前进程与选取进程是否是同一个 */

sched_info_switch(prev, next);

rq->nr_switches++;

rq->curr = next;

++*switch_count;

context_switch(rq, prev, next); /* unlocks the rq */

/*

* the context switch might have flipped the stack from under

* us, hence refresh the local variables.

*/

cpu = smp_processor_id();

rq = cpu_rq(cpu);

} else

spin_unlock_irq(&rq->lock);

/* ...... */

3.6 update_process_times
/* update_process_times 时钟中断调用该函数更新当前进程时间片,并根据结果进行相应处

理 */

void update_process_times(int user_tick) {


/* ... */

/* scheduler_tick */

scheduler_tick();

3.7 task_tick
/* 调度器根据调度策略实现 task_tick 函数,计算实时进程调度和普通进程调度调度权值 */

3.7.1 实时进程(SCHED_RR)时间权值计算
static void task_tick_rt(struct rq *rq, struct task_struct *p, int queued)

update_curr_rt(rq);

watchdog(rq, p);

/*

* RR tasks need a special form of timeslice management.

* FIFO tasks have no timeslices.

*/

if (p->policy != SCHED_RR)

return;

/* 递减当前进程运行时间片 */

if (--p->rt.time_slice)

return;

p->rt.time_slice = DEF_TIMESLICE;

/*

* Requeue to the end of queue if we are not the only element

* on the queue:

*/

if (p->rt.run_list.prev != p->rt.run_list.next) {

requeue_task_rt(rq, p);
set_tsk_need_resched(p);

task_tick_rt 函 数 在 定 时 器 中 断 处 理 程 序 程 序 中 调 用 , 用 于 计 算 实 时 进 程

(SCHED_FIFO/SCHED_RR)的运行时间权值。从程序我们不难发现:SCHED_FIFO 调度

策略的进程忽略运行时间权值;而 SCHED_RR 调度策略的进程,递减时间片直至到 0,内

核重新分配默认时间片大小 DEF_TIMESLICE,然后将当前进程插入到所在的优先级队列

队尾,并设置请求调度标志。

3.7.2 普通进程运行权值计算(CFS 调度器实现)


1)对比实时进程运行时间权值的计算点,分析定时器中断服务程序中普通进程运行时间权

值的计算。调用堆栈如下:

task_tick_fair() ->entity_tick()->update_curr()->__update_curr()

/*

* Update the current task's runtime statistics. Skip current tasks that

* are not in our scheduling class.

*/

static inline void

__update_curr(struct cfs_rq *cfs_rq, struct sched_entity *curr,

unsigned long delta_exec)

unsigned long delta_exec_weighted;

schedstat_set(curr->exec_max, max((u64)delta_exec, curr->exec_max));

/* 累加 sum_exec_runtime:进程创建开始运行时间 */

curr->sum_exec_runtime += delta_exec;

schedstat_add(cfs_rq, exec_clock, delta_exec);

delta_exec_weighted = delta_exec;

/* 函数 calc_delta_fair()根据 CPU 负载根据 cpu 负载,对 delta_exec_weighted 进行修正,


并将结果保存到 delta_exec_weighted 中 */
if (unlikely(curr->load.weight != NICE_0_LOAD)) {

delta_exec_weighted = calc_delta_fair(delta_exec_weighted,
&curr->load);

/* 累加 vruntime:CFS 调度器进程运行权值 */

curr->vruntime += delta_exec_weighted;

2)CFS 调度器调度时机,调用堆栈如下:

task_tick_fair() ->entity_tick()->check_preempt_tick

/*

* Preempt the current task with a newly woken task if needed:

*/

static void

check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)

unsigned long ideal_runtime, delta_exec;

ideal_runtime = sched_slice(cfs_rq, curr);

delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;

if (delta_exec > ideal_runtime)

resched_task(rq_of(cfs_rq)->curr);

函数 sched_slice()根据进程运行时间权值和 CPU 负载,计算进程理想运行时间权值,

其 公 式 为 : sched_period = (nr_running > sched_nr_latency) ? sysctl_sched_latency :

((nr_running * sysctl_sched_latency) / sched_nr_latency) , 其 中 sched_nr_latency =

(sysctl_sched_latency / sysctl_sched_min_granularity) 。 这 表 示 , 当 可 运 行 任 务 数 大 于

latency_nr 时,将线性延长调度周期。

通过比较 delta_exec(进程运行时间权值)和 ideal_runtime(进程理想时间权值),判

断是否应该剥夺进行运行权。

4.参考文献

1.《Linux 内核原代码情景分析》
2.《Linux 内核设计与实现 第二版》
3.《计算机操作系统教程》
4.http://www.ibm.com/developerworks/cn/linux/

6.www.google.com
7.www.chinaunix.net

You might also like