Linux Kernel 26进程调度的分析.docx
- 文档编号:8787608
- 上传时间:2023-05-15
- 格式:DOCX
- 页数:28
- 大小:33.70KB
Linux Kernel 26进程调度的分析.docx
《Linux Kernel 26进程调度的分析.docx》由会员分享,可在线阅读,更多相关《Linux Kernel 26进程调度的分析.docx(28页珍藏版)》请在冰点文库上搜索。
LinuxKernel26进程调度的分析
LinuxKernel2.6进程调度的分析
第一章 Kernel 2.4存在的不足
根据对2.4进程调度的分析,我们总结出看出2.4内核总的特点就是:
内核调度简单有效
内核不可抢占
但是经过对2.4内核的分析,我们也明显看到了它的缺点:
1.调度算法复杂度是O(n),与系统负荷关系较大。
而且调度算法在设计上也有缺陷
,比如:
(1) 2.4进程调度只设置了一个进程就绪队列,这样有的进程用完了自己时间片以后
还要呆在就绪进程队列里面。
这样这个进程虽然在这一轮调度循环里面已经无法取得
CPU的使用权,但是还要参与goodness()值的计算,这样就白白浪费了时间。
(2) 就绪进程队列是一个全局数据结构,多个CPU只有一个就绪队列runqueue,因而
调度器对它的所有操作都会因全局自旋锁而导致系统各个处理机之间的等待,使得就
绪队列成为一个明显的瓶颈。
2.调度算法在内核态不可抢占。
如果某个进程一旦进了内核态那么再高优先级的进
程都无法剥夺,只有等进程返回内核态的时候才可以进行调度。
缺乏对实时进程的支持。
第二章Kernel 2.6进程调度分析
一、基本思想
Kernel2.6调度算法仍然是基于优先级的调度,它的算法复杂度为O
(1),也就是说是
调度器的开销是恒定的,与系统当前的负载没有关系。
1. 就绪队列的改进
每个CPU有两个按优先级排序的数组:
一个是active array;一个是expired array。
Active array是当前CPU可能选择执行的运行进程队列,队列中的每个进程都有时间
片剩下。
Expired array是那些用户时间片的就绪进程队列。
一旦active array里面
某个普通进程的时间片用完了,调度器将重新计算进程的时间片、优先级,将它从active
array中删除,插入到expired array中相应得优先级队列中。
Active array和expired
array是通过两个指向每个CPU运行队列的指针来访问的。
所以当active array中所
有的进程都用完时间片,只需将两个指针切换一下就可以了,这比Kernel 2.4的切换
要改进了很多。
2. 快速查找应该执行的进程
系统中往往有很多的就绪进程,如何快速找到CPU即将运行的进程就成了关系到系统
性能的一个重要因素。
针对2.4的缺点,Kernel 2.6进行了重新设计:
引进了一个64bit的bitmap作为进程队列的索引,用bitmap来记载某个优先级的进程
队列上有无进程,如果有则为1。
这样使得寻找优先级最高的任务只需要两个BSFL命
令。
3. 引进"load estimator"
在一个负载很重的系统上有一个很好的交互感是一件很困难的事情,设计者经过研究
发现一味的激励(boost)交互任务并不够,还需惩罚(punish)那些需求大于可获
得CPU时间的进程。
调度器通过对用户睡眠时间和运行时间的纪录来判断进程是否是
交互进程,一旦被认为是交互进程,调度器会给进程很多"奖励"(bonus)。
4. 内核可抢占
内核可抢占可以说是2.6内核调度器优于2.4内核的一个很重要的原因。
当内核进程没有访问内核的关键数据,也就是内核没有被加锁,此时内核代码是可重
入的,因此更高优先级的进程可以在此时中断正在执行的进程,从而达到抢占的目的
。
5. 调度器相关的负载均衡
负载均衡有两种策略,一种是从别的CPU上将进程迁移过来,称为"pull";一种是将
本CPU上的进程迁移出去,称为"push"。
二、数据结构
1. 进程优先级的划分
Kernel 2.6将进程优先级作了以下规定:
进程优先级范围是从0 ~ MAX_PRIO-1,其中实时进程的优先级的范围是0 ~ MAX_RT_PRIO
-1,普通进程的优先级是MAX_RT_PRIO ~ MAX_PRIO-1。
数值越小优先级越高。
2. 就绪队列runqueue(kernel/sched.c)
struct runqueue是2.6调度器中一个非常重要的数据结构,它主要用于存放每个CPU
的就绪队列信息。
限于篇幅,这里只介绍其中相对重要的部分:
(1) prio_array_t *active, *expired, arrays[2]
这是runqueue中最重要的部分。
每个CPU的就绪队列都是一个数组,按照时间片是否
用完将就绪队列分为两个部分,分别用指针active和expired来指向数组的两个下标
。
prio_array_t的结构如下:
struct prio_array {
int nr_active; /*本进程组中进程个数*/
struct list_head queue[MAX_PRIO]; /*每个优先级的进程队列*/
unsigned long bitmap[BITMAP_SIZE]; /*上述进程队列的索引位图*/
};
数组queue[MAX_PRIO]里面存放的是优先级为i(MAX_PRIO>i>=0)的进程队列的链表
头,即task_struct:
:
runlist(通过runnlist即可找到task_struct)。
那么调度器在执行调度的任务时是怎么找到优先级最高的进程呢?
在结构体struct prio_array中有一个重要的数据unsigned long bitmap[BITMAP_SIZE
],这个数据是用来作为进程队列queue[MAX_PRIO]的索引位图,bitmap的每一位(bit
)都与queue[i]对应。
当queue[i]的进程队列不为空时,bitmap的相应位就为1;否
则就为0。
这样我们只需要通过汇编指令从进程优先级由高到低的方向找到第一个为
1的位置idx即为当前就绪队列中最高的优先级(函数sched_find_first_bit()就是用
来完成这一工作的),那么queue[i]->next就是我们要找的task_struct:
:
runlist。
当一个普通进程的时间片用完以后将重新计算进程的时间片和优先级,将该进程从active
array中删除,添加到expired array中相应优先级的进程队列中。
当active array
中没有进程时,则将active和expired指针调换一下就完成了切换工作。
而在2.4内核
中重新计算时间片是在所有就绪进程的时间片都用完以后才统一进行的,因而进程时
间片的计算非常耗时,而在2.6中计算时间片是分散的,而且通过以上的方法来实现
时间片的轮转,这也是2.6调度器一个亮点。
另外,程序将struct runqueue定义在sched.c里面而没有定义在sched.h里面是为了
让抽象调度器部分的代码,使得内核的其他部分使用调度器提供的接口即可。
(2) spinlock_t lock
runqueue的自旋锁,当对runqueue进行操作的时候,需要对其加锁。
由于每个CPU都
有一个runqueue,这样会大大减少竞争的机会。
(3) task_t *curr
CPU当前运行的进程。
在程序中还有一个全局变量current也是CPU当前运行的进程,它在通常情况下和runqueue
的curr指针是相同的,但是当调度器进行调度的时,如果已经找到最高优先级的进程
,则此时做rq->curr = next;可见在进行任务切换之前,rq->curr和current的值是
不同的。
当唤醒一个进程的时候,很明显将唤醒进程与rq->curr的优先级进行比较更
有意义。
(4) unsigned long expired_timestamp
此变量是用来记录active array中最早用完时间片的时间(赋值jiffies)。
因此,
用这个量就可以记录expired array中等时间最长的进程的等待时间。
这个值的主要
用处是用于宏EXPIRED_STARVING()(这个宏主要是用来判断expired array中的进程
是否已经等待了足够长的时间,详见"进程调度的生与死"一节中"scheduler_tick()
"函数的介绍)。
(5) unsigned long nr_running, nr_switches, nr_uninterruptible,
timestamp_last_tick
用来记录该CPU进程相关数据。
具体作用如下
用来记录该CPU进程相关数据。
具体作用如下
nr_running
记录该CPU上就绪进程总数,是active array和expired array进程总数和
nr_switches
记录该CPU运行以来发生的进程切换次数
nr_uninterruptible
记录该CPU不可中断状态进程的个数
timestamp_last_tick
记录就绪进程队列上次发生调度的时间,用于负载均衡
(6) struct list_head migration_queue
这个是存放希望迁移到其他CPU上的进程队列,实际迁移的数据类型是migration_req_t
,这里是通过将migration_req_t:
:
list连接起来。
详见"负载均衡"中"push"一节。
3. 进程标识task_struct(include/linux/sched.h)
Linux是一个多任务的操作系统,在多任务操作系统中每一个进程都由一个PCB程序控
制块来标识在Linux中PCB实际上是一个名为task_struct的结构体。
task_struct有上百个域,主要包括了10个方面的信息:
1.进程状态;2.调度信息,
如调度策略,优先级,时间片,交互值等;3.进程的通讯状况;4.进程树中的父子兄
弟的指针;5.时间信息,如睡眠时间,上一次发生调度时间等;6.标号,决定该进程
归属;7.打开的一些文件信息;8.进程上下文和内核上下文;9.处理器上下文;10.
内存信息。
由于task_struct结构体比较复杂,因此我们只注意它与进程调度相关的重要部分。
(1) volatile long state
进程所处的状态。
在include/linux/sched.h中包含6种状态:
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_STOPPED 4
#define TASK_ZOMBIE 8
#define TASK_DEAD 16
新增的TASK_DEAD是表示已经退出且不需父进程回收的进程的状态。
(2) struct thread_info *thread_info
当前进程运行的一些环境信息。
其中有两个结构成员非常重要,与调度密切相关:
__s32 preempt_count;
unsigned long flags;
preempt_count是用来表示内核能否被抢占的使能成员。
如果它大于0,表示内核不能
被抢占;如果等于0,则表示内核处于安全状态(即没有加锁),可以抢占。
flags里面有一个TIF_NEED_RESCHED位,它和Kernel 2.4中need_resched作用一样。
如果此标志位为1,则表示应该尽快启动调度器。
(3) int prio, static_prio
prio是进程的动态优先级,相当于Kernel2.4中用goodness()函数计算出来的结果;
在Kernel2.6 中不再是由调度器统一计算,而是独立计算;prio的计算和许多因素有
关,详见"进程优先级的计算"一节。
static_prio则是进程的静态优先级,与nice意义相同。
nice的取值仍然是-20 ~ 19
,数值越小,进程优先级越高。
kernel/sched.c中定义了两个宏来完成将nice转换到prio的取值区间和将prioity转
换到nice取值区间。
#define NICE_TO_PRIO(nice) (MAX_RT_PRIO + (nice) + 20)
#define PRIO_TO_NICE(prio) ((prio) - MAX_RT_PRIO - 20)
可见prioity和nice的关系是:
priority = MAX_RT_PRIO+nice+20
(4) struct list_head run_list
前面提到过,就绪进程都是按照优先级进行排列,prio_array中的queue[MAX_PRIO]
存放的是指向每个优先级队列的链头list_head;而同一优先级的进程则是通过run_list
链接在一起。
include/linux/list.h定义了一种抽象的双向链表struct list_head,通过它可以将
任意类型的结构体链接到一起。
task_struct也是通过这种方式链接起来的。
(5) prio_array_t *array
指向当前CPU的active array的指针。
在进程控制块里面又加了一个指向active array
的指针,看似重复,其实不然。
比如说对于下面的代码(kernel/sched.c):
array = next->array;
dequeue_task(next, array);
recalc_task_prio(next, next->timestamp + delta);
enqueue_task(next, array);
对于单处理器(UP)的情况,我们确实可以通过runqueue:
:
active直接得到当前的active
array;但是对于SMP,就不是这样了,需要引用next的thread_info,再依靠thread_info
中的cpu找到next所在的处理器,找到以后再找到这个cpu上的runqueue,最后得到active
。
对于schedule这样频繁调用的函数,这种浪费是不能容忍的。
(6) unsigned long sleep_avg
进程的平均等待时间,单位是纳秒(nanosecond),在0 ~ NS_MAX_SLEEP_AVG范围内
。
它的实质是进程等待时间和运行时间的差值。
当进程处于等待或者睡眠状态时,该
值变大;当进程运行时,该值变小。
sleep_avg是Kernel 2.6中衡量进程的一个关键指标,它既可以用来衡量进程的交互
程度,也可以用来衡量进程的紧急程度。
具体内容将在"平均等待时间sleep_avg"一
节作详细介绍。
(7) long interactive_credit
表示进程交互程度,取值范围在-CREDIT_LIMIT ~ CREDIT_LIMIT+1之间。
进程创建的
时候值为1,以后根据不同的情况进行不同的增1、减1;如果一个进程的
interactive_credit
超过CREDIT_LIMIT之后,这个进程就会被认为是交互式进程,同时interactive_credit
的值也就不再改变了(恒为CREDIT_LIMIT+1)。
下面将在"交互进程优化"一节详细介
绍。
(8) unsigned long long timestamp
进程发生调度的时间,单位和sleep_avg一样,也是纳秒。
它负责纪录以下四种情况
的时间:
a. 进程被唤醒的时间:
在activate_task()(kernel/sched.c)中记录(p->timestamp = now)。
b. 进程被切换到expired array的时间:
在schedule()(kernel/sched.c)中记录,当准备进行进程切换的时候,记录下该进
程被切换到expired array的时间(prev->timestamp = now)。
c. 进程被切换到active array的时间:
在schedule()(kernel/sched.c)中记录,进行进程切换的开始,记录下下一个进程
被切换到active array的时间(next->timestamp = now)。
d. 负载均衡相关的赋值
在进行负载均衡的时候,当把一个进程从其他CPU上pull过来的时候需要将该进程的
timestamp设成sched_clock() - (src_rq->timestamp_last_tick - p->timestamp)
,即相对于本CPU被切换下来的时间。
(9) int activated
表示该进程被唤醒的类别:
actived=-1 表示该进程并非自愿sleep,其先前状态是TASK_UNINTERRUPTIBLE。
在
try_to_wake_up
()中设置。
actived=0 缺省值,表示进程本来就是处于就绪状态。
actived=1 进程先前状态是TASK_INTERRUPTIBLE,但是不是由中断唤醒;这
样的进
程在第一次运行时有credit,以后就没有了。
在activate_task()中设置。
actived=2 进程先前状态是TASK_INTERRUPTIBLE,进程被中断唤醒。
这样的
进程非
常像交互式进程。
在activate_task()中设置。
(10) unsigned long policy
进程的调度策略和2.4一样,有以下几种:
SCHED_FIFO 先进先出式调度,除非有更高优先级进程申请运行,否则该进程
将保持运行至退出才让出CPU
SCHED_RR 轮转式调度,该进程被调度下来后将被置于运行队列的末尾,以
保证其他实时进程有机会运行)
SCHED_OTHER 常规的分时调度策略
(11) unsigned int time_slice, first_time_slice
ime_slice是进程剩余的时间片,相当于Kernel 2.4里面counter,但是时间片不再影
响进程的优先级。
first_time_slice用来记录时间片是否是第一次分配(进程创建时),如果值不为0
,进程退出时将时间片交还给父进程。
三、调度策略
1. 进程优先级
(1) 优先级的计算
前面已经说过,优先级由两部分构成,一是静态优先级static_prio,一是动态优先
级prio。
静态优先级在进程创建的时候就被赋值,并且不变(除非用系统调用改变进
程的nice值);而进程的动态优先级则是跟static_prio和sleep_avg有关。
对于实时
进程的优先级在创建的时候就确定了,而且一旦确定以后就不再改变,所以下面部分
仅对于非实时进程而言。
具体的计算由函数effecitve_prio()(kernel/sched.c)完
成。
函数将进程的sleep_avg映射成范围是-MAX_BONUS/2 ~ MAX_BONUS/2的变量bonus,而
MAX_BONUS是等于 ,可见sleep_avg仅能影响的优先级范围在-5 ~ 5之间。
具体的映
射是由以下规则完成的:
那么进程的动态优先级就等于:
(当然必须在MAX_RT_PRIO和MAX_PRIO-1之间
)。
可见,sleep_avg和bonus是一个线性关系。
进程的sleep_avg越大,bonus越大,
从而进程的动态优先级也就越高。
(2) 何时计算优先级
计算进程的动态优先级一般调用两个函数,一个是effective_prio(),一个是
recalc_task_prio()。
函数recalc_task_prio ()先要根据进程被唤醒前的状态
(即actived)、interactive_credit等来计算进程的sleep_avg
(详见"平均等待时间sleep_avg"一节),在最后调用effective_prio()来计算函数
的动态优先级。
总的来说,有以下几种情况需要计算进程的优先级:
a. 创建新进程,使用函数effective_prio()(因为此时进程尚未进行调度,没有
sleep_avg和interactive_credit可言);
b. 唤醒等待进程时,使用函数recalc_task_prio ()来计算进程动态优先级。
c. 进程用完时间片以后,被重新插入到active array或者expired array的时候需要
重新计算动态优先级,以便将进程插入到队列的相应位置。
此时,使用函数
effective_prio();
d. 其他情况,如IDLE进程初始化等时候。
2. 进程时间片
(1) 时间片的计算
进程的时间片time_slice是基于进程静态优先级的,静态优先级越高(值越小),时
间片就越大。
计算时间片是同过函数task_timeslice()(kernel/sched.c)来完成的
MAX_BONUS是等于 ,可见sleep_avg仅能影响的优先级范围在-5 ~ 5之间。
具体的映
射是由以下规则完成的:
那么进程的动态优先级就等于:
(当然必须在MAX_RT_PRIO和MAX_PRIO-1之间
)。
可见,sleep_avg和bonus是一个线性关系。
进程的sleep_avg越大,bonus越大,
从而进程的动态优先级也就越高。
(2) 何时计算优先级
计算进程的动态优先级一般调用两个函数,一个是effective_prio(),一个是
recalc_task_prio()。
函数recalc_task_prio ()先要根据进程被唤醒前的状态
(即actived)、interactive_credit等来计算进程的sleep_avg
(详见"平均等待时间sleep_avg"一节),在最后调用effective_prio()来计算函数
的动态优先级。
总的来说,有以下几种情况需要计算进程的优先级:
a. 创建新进程,使用函数effective_prio()(因为此时进程尚未进行调度,没有
sleep_avg和interactive_credit可言);
b. 唤醒等待进程时,使用函数recalc_task_prio ()来计算进程动态优先级。
c. 进程用完时间片以后,被重新插入到active array或者expired array的时候需要
重新计算动态优先级,以便将进程插入到队列的相应位置。
此时,使用函数
effective_prio();
d. 其他情况,如IDLE进程初始化等时候。
2. 进程时间片
(1) 时间片的计算
进程的时间片time_slice是基于进程静态优先级的,静态优先级越高(值越小),时
间片就越大。
计算时间片是同过函数task_timeslice()(kernel/sched.c)来完成的
。
该函数也是使用线性映射的方法,将进程优先级
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Linux Kernel 26进程调度的分析 26 进程 调度 分析