Professional Documents
Culture Documents
算法的原理,实现和复杂度进行了分析并提出了算法改进措施。
关键字:Linux 内核 进程调度 算法
1. Linux 进程调度概述
持,实现用户态线程需要引入第三方线程库。
操作系统进程调度是整个操作系统理论的核心,在设计进程调动机制需要考虑的具体
问题主要有:
1)调度的时机:在什么情况下,什么时候进行调度。
2)调度的“政策”(policy):根据什么准则挑选下一个进入运行的进程。
3)调度的方式:是“可剥夺”(preemptive)还是“不可剥夺”(nonpreemptive)。
fork ()
TASK _RUNNING
收到信号 就绪 等待资源到位
SIG _CONT wake _up()
wake _up() 等待资源到位 或收到信号
wake _up () wake _up_interruptible
schedule () 时间片到
等待资源
sleep _on () 等待资源
schedule () 占有 CPU
sleep _on_interruptible ()
执行
schedule ()
Linux 进程调度分为自愿调度和强制调度两种。1)在内核空间,一个进程可以通过
schedule() 启 动 一 次 调 度 , 也 可 以 在 调 用 schedule() 之 前 , 将 本 进 程 状 态 设 置 为
TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE,暂时放弃运行而进入睡眠。这通
常发生在来自用户空间的系统调用被阻塞。在用户空间,用户进程可以通过系统调用
nanosleep()达到目的。2)调度还可以是非自愿的。在一定条件下,内核会强制性剥夺当前进
程运行而调度其他进程进入运行。
Linux 调度政策基础是时间片轮转+优先级抢占的结合,为了满足不同应用的需要,内
时 调 度 策 略 , 时 间 片 轮 转 3 ) SCHED_NORMAL 分 时 调 度 策 略 ( 在 2.6 内 核 以 前 为
SCHED_OTHER)。用户进程可以通过系统调用 sched_setscheduler()设定自己的调度策略。
2. Linux 进程调度原理
价值,实际中鲜有应用;而某些调度算法需要互补以完成设计需求。但是,无论哪种进程调
度算法,都要面对以下实际问题:
1)调度器对实时进程的响应;
2)调度器的调度开销,以及系统进程负载对调度的影响;
Linux2.6.x 内核进程调度算法为解决上述问题,设计了全新的数据结构和调度算法,
能发生在内核态(因此,2.6 版本的内核代码必须考虑到重入问题)。
期版本的精华,通过全新设计数据结构和算法,为实时进程(SCHED_FIFO/SCHED_RR)
提供 O(1)时间复杂度的调度算法,同时,为了兼顾“完全公平”这一设计思路,设计了
2.1 基于实时进程调度
Linux2.4 内核维护双向循环队列 runqueue,一旦调度时机触发,内核重新计算当前队
列中所有进程运行权值,并从中挑选出权值最高的进程作为当前进程投入运行。其弊端是显
而易见的:
性能与内核负载相关。
2)runqueue 同时管理着实时进程与非实时进程(普通进程),内核通过进程属性,如
实时或非实时、实时进程优先级、用户进程或内核线程相关因素来计算运行权值 count,灵活
性低,且不便于理解和维护。
代码,给与实时进程 O(1)调度器的实现(限于篇幅,本文给出核心数据结构关键成员的
注释)。
1)就绪进程队列 struct rq
struct rq {
/* ...... */
/* runqueue lock: */
spinlock_t lock;
/* 就绪队列中进程个数 */
/* 普通进程就绪队列 */
/* 实时进程就绪队列 */
/* ...... */
/* 就绪队列工作时间 */
u64 clock;
/* ...... */
/* used by load_balance */
/* ...... */
struct rt_rq {
/* 实时进程优先级队列 */
/* 实时进程个数 */
/* ... */
/* 实时进程队列工作时间 */
u64 rt_time;
/* ... */
};
struct rt_prio_array {
/* 优先级位图 */
DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1);
/* 优先级队列 */
};
4)进程运行信息结构 sched_info
struct sched_info {
/* cumulative counters */
/* timestamps */
};
sched_info 维护进程运行时的实时信息,代码作者的注释已比较详细,该结构数据在
schedule 进程切换发生时被更新。
先级队列数组,每个进程优先级队列用双端循环链表来描述。内核寻找优先级最高的任务需
的优先级队列的对头所指向的进程,即为下一个投入运行的进程。当进程用完了自己的时间
队首任务投入运行。实时进程调度核心数据结构之间的关系如图 2:
struct cfs _rq tasks _timeline ;
rb_leftmost ;
curr ;
next ;
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 调 度 器 使 用 红 黑 树 管 理 就 绪 进 程 , 所 有 状 态 为
红黑树中的位置。调整完成后如果发现当前进程不再是最左边的叶子,就标记 need_resched
减时间片,当时间片或者配额被用完时才触发优先级调整并重新调度(参见函数
update_curr()调用时机)。2)CFS 为内核抢占调度提供完美支持。
理解 CFS 的关键就是了解红黑树键值的计算方法。该键值由三个因子计算而得:一是
个进程的公平程度。该值越大,代表当前进程相对于其它进程越不公平。因此该值越大,键
值越大,从而使得当前进程向红黑树的右侧移动,越晚被选中。
的调度器(idle_sched_class)。需要指出的是,进程调度首先选择实时进程调度器,即进程
进行进程调度。
Additions notes:
1)2.6.26 内核对实时进程队列运行时间进行了限制,如果某一实时进程队列运行时间
2)2.6.26 内核进程运行时间权值的分散计算点:
a.定时器中断调用调度器计算进程调度权值
b.schedule 中进程调度点
3. Linux 进程调度实现代码分析
Linux2.6.26 进程调度部分核心数据结构已在上文介绍,限于篇幅,本节对进程调度其
他核心数据结构及函数给予简要介绍。
/* fair_sched_class */
/* rt_sched_class */
/* idle_sched_class */
struct sched_class {
/* 内核支持调度器单链表 */
/* 根据本调度策略,选择下一个投入运行的进程 */
/* ...... */
/* 定时器中断调用,更新进程运行权值 */
/* ...... */
/* 进程切换时对进程与本调度策略相关成员计算 */
int running);
/* ...... */
/* 修改进程优先级,针对实时进程 */
};
/* ...... */
/* 进程所属调度器 */
/* ...... */
/* 本进程调度策略 */
/* ...... */
/* 进程调度信息 */
/* ...... */
};
/* ...... */
/* 实时进程运行时间片,用于调度策略是 SCHED_RR */
/* ...... */
/* ..... */
};
/* 指向对应红黑树的结点 */
/* 进程调度开始运行时间 */
u64 exec_start;
/* 进程创建开始运行时间 */
u64 sum_exec_runtime;
/* CFS 考虑运行权值 */
u64 vruntime;
u64 prev_sum_exec_runtime;
/* 进程最后一次唤醒时间 */
u64 last_wakeup;
/* 进程被调度出的平均时间 */
u64 avg_overlap;
/* ..... */
};
need_resched:
/* 关闭内核抢占,因为此时要对内核的一些重要数据结构进行操作, 所以必须将内核
抢占关闭 */
preempt_disable();
cpu = smp_processor_id();
rcu_qsctr_inc(cpu);
prev = rq->curr;
switch_count = &prev->nivcsw;
release_kernel_lock(prev);
need_resched_nonpreemptible:
/* ...... */
/* ...... */
态不是 TASK_RUNNING,且关闭内核抢占 */
else
switch_count = &prev->nvcsw;
if (prev->sched_class->pre_schedule)
prev->sched_class->pre_schedule(rq, prev);
#endif
在 CPU 之间平衡负载 */
idle_balance(cpu, rq);
/* 调用 put_prev_task 选择下一个可调度进程 */
prev->sched_class->put_prev_task(rq, prev);
sched_info_switch(prev, next);
rq->nr_switches++;
rq->curr = next;
++*switch_count;
/*
* the context switch might have flipped the stack from under
*/
cpu = smp_processor_id();
rq = cpu_rq(cpu);
} else
spin_unlock_irq(&rq->lock);
/* ...... */
3.6 update_process_times
/* update_process_times 时钟中断调用该函数更新当前进程时间片,并根据结果进行相应处
理 */
/* 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);
/*
*/
if (p->policy != SCHED_RR)
return;
/* 递减当前进程运行时间片 */
if (--p->rt.time_slice)
return;
p->rt.time_slice = DEF_TIMESLICE;
/*
* 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 调度
核重新分配默认时间片大小 DEF_TIMESLICE,然后将当前进程插入到所在的优先级队列
队尾,并设置请求调度标志。
值的计算。调用堆栈如下:
task_tick_fair() ->entity_tick()->update_curr()->__update_curr()
/*
* Update the current task's runtime statistics. Skip current tasks that
*/
/* 累加 sum_exec_runtime:进程创建开始运行时间 */
curr->sum_exec_runtime += delta_exec;
delta_exec_weighted = delta_exec;
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
/*
*/
static void
resched_task(rq_of(cfs_rq)->curr);
(sysctl_sched_latency / sysctl_sched_min_granularity) 。 这 表 示 , 当 可 运 行 任 务 数 大 于
latency_nr 时,将线性延长调度周期。
断是否应该剥夺进行运行权。
4.参考文献
1.《Linux 内核原代码情景分析》
2.《Linux 内核设计与实现 第二版》
3.《计算机操作系统教程》
4.http://www.ibm.com/developerworks/cn/linux/
6.www.google.com
7.www.chinaunix.net