UCORE实验6
实验目的
理解操作系统的调度管理机制;
熟悉 ucore 的系统调度器框架,以及缺省的Round-Robin调度算法;
基于调度器框架实现一个(Stride Scheduling)调度算法来替换缺省的调度算法。
实验内容
实验五完成了用户进程的管理,可在用户态运行多个进程。但到目前为止,采用的调度策略是很简单的FIFO调度策略。本次实验,主要是熟悉ucore的系统调度器框架,以及基于此框架的Round-Robin(RR) 调度算法。然后参考RR调度算法的实现,完成Stride Scheduling调度算法。
练习0:填写已有实验
本实验依赖实验1/2/3/4/5。请把你做的实验2/3/4/5的代码填入本实验中代码中有“LAB1”/“LAB2”/“LAB3”/“LAB4”“LAB5”的注释相应部分。并确保编译通过。注意:为了能够正确执行lab6的测试应用程序,可能需对已完成的实验1/2/3/4/5的代码进行进一步改进。
分析
alloc_proc在lab6中新增6个变量需要初始化:
// alloc_proc - alloc a proc_struct and init all fields of proc_struct
static struct proc_struct *
alloc_proc(void) {
struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
if (proc != NULL) {
//LAB4:EXERCISE1 YOUR CODE
/*
* below fields in proc_struct need to be initialized
* enum proc_state state; // Process state
* int pid; // Process ID
* int runs; // the running times of Proces
* uintptr_t kstack; // Process kernel stack
* volatile bool need_resched; // bool value: need to be rescheduled to release CPU?
* struct proc_struct *parent; // the parent process
* struct mm_struct *mm; // Process's memory management field
* struct context context; // Switch here to run process
* struct trapframe *tf; // Trap frame for current interrupt
* uintptr_t cr3; // CR3 register: the base addr of Page Directroy Table(PDT)
* uint32_t flags; // Process flag
* char name[PROC_NAME_LEN + 1]; // Process name
*/
//LAB5 YOUR CODE : (update LAB4 steps)
/*
* below fields(add in LAB5) in proc_struct need to be initialized
* uint32_t wait_state; // waiting state
* struct proc_struct *cptr, *yptr, *optr; // relations between processes
*/
//LAB6 YOUR CODE : (update LAB5 steps)
/*
* below fields(add in LAB6) in proc_struct need to be initialized
* struct run_queue *rq; // running queue contains Process
* list_entry_t run_link; // the entry linked in run queue
* int time_slice; // time slice for occupying the CPU
* skew_heap_entry_t lab6_run_pool; // FOR LAB6 ONLY: the entry in the run pool
* uint32_t lab6_stride; // FOR LAB6 ONLY: the current stride of the process
* uint32_t lab6_priority; // FOR LAB6 ONLY: the priority of process, set by lab6_set_priority(uint32_t)
*/
proc->state = PROC_UNINIT;
proc->pid = -1;
proc->runs = 0;
proc->kstack = 0;
proc->need_resched = 0;
proc->parent = NULL;
proc->mm = NULL;
memset(&(proc->context), 0, sizeof(struct context));
proc->tf = NULL;
proc->cr3 = boot_cr3;
proc->flags = 0;
memset(proc->name, 0, PROC_NAME_LEN);
proc->wait_state = 0;
proc->cptr = proc->optr = proc->yptr = NULL;
//新增lab6 code
proc->rq = NULL; //进程运行队列
list_init(&(proc->run_link)); //该进程的调度链表结构,该结构内部的连接组成了运行队列列表
proc->time_slice = 0; //该进程剩余的时间片,只对当前进程有效
proc->lab6_run_pool.left = proc->lab6_run_pool.right = proc->lab6_run_pool.parent = NULL; //该进程在优先队列中的节点,仅在 LAB6 使用
proc->lab6_stride = 0; //该进程的调度步进值,仅在 LAB6 使用
proc->lab6_priority = 0; //该进程的调度优先级,仅在 LAB6 使用
}
return proc;
}
系统中断里的时钟中断也需要进行修改:
case IRQ_OFFSET + IRQ_TIMER:
ticks++;
assert(current != NULL);
sched_class_proc_tick(current); //时钟中断的处理逻辑中主动调用了调度器的proc_tick函数,使得调度器能感知到时钟中断的产生并调整调度相关的数据结构。
break;
练习1: 使用 Round Robin 调度算法(不需要编码)
完成练习0后,建议大家比较一下(可用kdiff3等文件比较软件)个人完成的lab5和练习0完成后的刚修改的lab6之间的区别,分析了解lab6采用RR调度算法后的执行过程。执行make grade,大部分测试用例应该通过。但执行priority.c应该过不去。
请在实验报告中完成:
请理解并分析sched_calss中各个函数指针的用法,并结合Round Robin调度算法描述ucore的调度执行过程。
请在实验报告中简要说明如何设计实现”多级反馈队列调度算法“,给出概要设计,鼓励给出详细设计。
分析
理解并分析sched_calss中各个函数指针的用法,并结合Round Robin调度算法描述ucore的调度执行过程
①背景相关
我们先来看看和调度框架有关,但又和具体调度算法无关的三个函数:wakup_proc、shedule、run_timer_list。
wakeup_proc函数其实完成了把一个就绪进程放入到就绪进程队列中的工作,调用了sched_class_enqueue。
schedule函数完成了与调度框架和调度算法相关三件事情:把当前继续占用CPU执行的运行进程放放入到就绪进程队列中,从就绪进程队列中选择一个“合适”就绪进程,把这个“合适”的就绪进程从就绪进程队列中摘除,调用了sched_class_enqueue、sched_class_pick_next、sched_class_enqueue三个调度类接口函数。
run_timer_list函数在每次timer中断处理过程中被调用,从而可用来调用调度算法所需的timer时间事件感知操作,调整相关进程的进程调度相关的属性值,调用了sched_class_proc_tick函数。
这里涉及了一系列调度类接口函数:
sched_class_enqueue
sched_class_dequeue
sched_class_pick_next
sched_class_proc_tick
而这4个函数的实现其实就是调用某基于sched_class数据结构的特定调度算法实现的4个指针函数。因此我们可以把sched_calss看做是一个调度类的框架,如果我们需要实现一个新的调度算法,则我们需要定义一个针对此算法的调度类的实例,一个就绪进程队列的组织结构描述就行了,其他的事情都可交给调度类框架来完成。
②理解并分析sched_calss中各个函数指针的用法
sched_class如下:
// The introduction of scheduling classes is borrrowed from Linux, and makes the
// core scheduler quite extensible. These classes (the scheduler modules) encapsulate
// the scheduling policies.
struct sched_class {
//调度器的名字
const char *name;
//初始化运行队列
void (*init)(struct run_queue *rq);
//将进程 p 插入队列 rq
void (*enqueue)(struct run_queue *rq, struct proc_struct *proc);
//将进程 p 从队列 rq 中删除
void (*dequeue)(struct run_queue *rq, struct proc_struct *proc);
//返回 运行队列 中下一个可执行的进程
struct proc_struct *(*pick_next)(struct run_queue *rq);
timetick 处理函数
void (*proc_tick)(struct run_queue *rq, struct proc_struct *proc);
/* for SMP support in the future
* load_balance
* void (*load_balance)(struct rq* rq);
* get some proc from this rq, used in load_balance,
* return value is the num of gotten proc
* int (*get_proc)(struct rq* rq, struct proc* procs_moved[]);
*/
};
我们在Round Robin中来具体看待这些函数指针的用法。
void (*init)(struct run_queue *rq)用于初始化传入的就绪队列。RR算法中只初始化了对应run_queue的run_list成员,且初始化后要调度的进程数目为0。
static void
RR_init(struct run_queue *rq) {
list_init(&(rq->run_list));
rq->proc_num = 0;
}
void (*enqueue)(struct run_queue *rq, struct proc_struct *proc)用于将某个进程添加进传入的队列中。RR算法除了把当前进程加入到运行队列的最后,还将当前进程的时间片时间重置为最大,更新队列中进程数目。
static void
RR_enqueue(struct run_queue *rq, struct proc_struct *proc) {
assert(list_empty(&(proc->run_link)));
list_add_before(&(rq->run_list), &(proc->run_link));
if (proc->time_slice == 0 || proc->time_slice > rq->max_time_slice) {
proc->time_slice = rq->max_time_slice;
}
proc->rq = rq;
rq->proc_num ++;
}
void (*dequeue)(struct run_queue *rq, struct proc_struct *proc)用于将某个进程从传入的队列中移除。RR算法把就绪进程队列rq的进程控制块指针的队列元素删除,并把表示就绪进程个数的proc_num减一。
static void
RR_dequeue(struct run_queue *rq, struct proc_struct *proc) {
assert(!list_empty(&(proc->run_link)) && proc->rq == rq);
list_del_init(&(proc->run_link));
rq->proc_num --;
}
struct proc_struct *(*pick_next)(struct run_queue *rq)用于在传入的就绪队列中选择出一个最适合运行的进程(选择进程但不将从队列中移除)。在RR算法中每次都只选择队列最前面那个进程,并把队列元素转换成进程控制块指针。
static struct proc_struct *
RR_pick_next(struct run_queue *rq) {
list_entry_t *le = list_next(&(rq->run_list));
if (le != &(rq->run_list)) {
return le2proc(le, run_link);
}
return NULL;
}
void (*proc_tick)(struct run_queue *rq, struct proc_struct *proc)。每次timer到时后,trap函数将会间接调用此函数来把当前执行进程的时间片time_slice减一。如果time_slice降到零,则设置此进程成员变量need_resched标识为1,这样在下一次中断来后执行trap函数时,会由于当前进程程成员变量need_resched标识为1而执行schedule函数,从而把当前执行进程放回就绪队列末尾,而从就绪队列头取出在就绪队列上等待时间最久的那个就绪进程执行。
static void
RR_proc_tick(struct run_queue *rq, struct proc_struct *proc) {
if (proc->time_slice > 0) {
proc->time_slice --;
}
if (proc->time_slice == 0) {
proc->need_resched = 1;
}
}
③结合Round Robin调度算法描述ucore的调度执行过程
首先,uCore调用sched_init函数用于初始化相关的就绪队列。
之后在proc_init函数中,建立第一个内核进程,并将其添加至就绪队列中。
当所有的初始化完成后,uCore执行cpu_idle函数,并在其内部的schedule函数中,调用sched_class_enqueue将当前进程添加进就绪队列中(因为当前进程要被切换出CPU了)。
然后,调用sched_class_pick_next获取就绪队列中可被轮换至CPU的进程。如果存在可用的进程,则调用sched_class_dequeue函数,将该进程移出就绪队列,并在之后执行proc_run函数进行进程上下文切换。
需要注意的是,每次时间中断都会调用函数sched_class_proc_tick,该函数会减少当前运行进程的剩余时间片。如果时间片减小为0,则设置need_resched为1,并在时间中断例程完成后,在trap函数的剩余代码中进行进程切换。(即练习0中修改的trap.c)
实现“多级反馈队列调度算法”的概要设计
我们知道,调度要考虑到周转时间和响应时间,而MLFQ能同时满足各种工作的需求,它有多级队列,并利用反馈信息决定某个工作的优先级。对于短时间运行的交互型工作,获得类似于SJF/STCF的很好的全局性能,同时对长时间运行的CPU密集型负载也可以公平地、不断地稳步向前。
书上给出的MLFQ的五大规则:
规则 1:如果 A 的优先级 > B 的优先级,运行 A(不运行 B)。
规则 2:如果 A 的优先级 = B 的优先级,轮转运行 A 和 B。
规则 3:工作进入系统时,放在最高优先级(最上层队列)。
规则 4:一旦工作用完了其在某一层中的时间配额(无论中间主动放弃了多少次CPU),就降低其优先级(移入低一级队列)。
规则 5:经过一段时间 S,就将系统中所有工作重新加入最高优先级队列。
基于此,我们类似的给出概要设计:
①设置多个run_queue,而且这些run_queue的max_time_slice需要按照优先级依次递减。
②在MLFQ_init函数中,程序先初始化这些run_queue,并依次从大到小设置max_time_slice。比如分为4个优先级,优先级可以通过proc->priority来表示,最高优先级时间片长度为10,最低为2等等。
③在MLFQ_enqueue函数中,先判断当前进程是否是新建立的进程。如果是,则将其添加至最高优先级(即时间片最大)的队列。然后再判断proc进程的时间片proc -> time_slice是否为0,如果为0,则proc -> priority += 1,否则不变。根据proc加入到对应优先级的列表中去(如果原先的队列已经是最低优先级的队列了,则重新添加至该队列)。
③MLFQ_dequeue函数,将proc进程从相应的优先级运行队列中删除。
④MLFQ_pick_next函数,对相同优先级的进程进行随机挑选,高优先级的进程率先挑选,低优先级的次挑选。为了避免低优先级的进程一直饥饿,所以每隔一段时间将系统中的所有工作重新加入最高优先级队列。
练习2: 实现 Stride Scheduling 调度算法(需要编码)
首先需要换掉RR调度器的实现,即用default_sched_stride_c覆盖default_sched.c。然后根据此文件和后续文档对Stride度器的相关描述,完成Stride调度算法的实现。
后面的实验文档部分给出了Stride调度算法的大体描述。这里给出Stride调度算法的一些相关的资料(目前网上中文的资料比较欠缺):
strid-shed paper location1
strid-shed paper location2
也可GOOGLE “Stride Scheduling” 来查找相关资料
执行:make grade。如果所显示的应用程序检测都输出ok,则基本正确。如果只是priority.c过不去,可执行 make run-priority 命令来单独调试它。大致执行结果可看附录。( 使用的是 qemu-1.0.1 )。
请在实验报告中简要说明你的设计实现过程。
分析
对Stride Scheduling调度算法的介绍
对于round-robin调度算法,在假设所有进程都充分使用了其拥有的CPU时间资源的情况下,所有进程得到的 CPU 时间应该是相等的。但我们希望调度器能够更智能地为每个进程分配合理的CPU资源,每个进程得到的时间资源与他们的优先级成正比关系,Stride调度是基于这种想法的一个较为典型和简单的算法(可以简单将这个算法理解为书上步长调度算法的近似版)。除了简单易于实现以外,它还有如下的特点:
可控性:如我们之前所希望的,可以证明Stride Scheduling对进程的调度次数正比于其优先级。
确定性:在不考虑计时器事件的情况下,整个调度机制都是可预知和重现的。
而其基本思想为:
①为每个runnable的进程设置一个当前状态stride,表示该进程当前的调度权。另外定义其对应的pass值,表示对应进程在调度后,stride需要进行的累加值。
②每次需要调度时,从当前 runnable 态的进程中选择stride最小的进程调度。
③对于获得调度的进程P,将对应的stride加上其对应的步长pass(pass只与进程的优先权有关系)。
④在一段固定的时间之后,回到步骤②,重新调度当前stride最小的进程。
⑤可以证明,如果令 P.pass =BigStride / P.priority 其中 P.priority 表示进程的优先权(大于1),而 BigStride 表示一个预先定义的大常数,则该调度方案为每个进程分配的时间将与其优先级成正比。
注意,一开始我们并没有考虑 stride 的数值范围,但这个值在理论上是不断增加的,在stride溢出以后,基于stride的比较可能会出现错误。如此时stride属性采用16位无符号整数进行存储,进程A、B如下:
此时A调度,调度一轮后:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mr38VHu9-1679660986540)(https://objectkuan.gitbooks.io/ucore-docs/content/lab6_figs/image002.png)]
可以看到由于溢出的出现,进程间stride的理论比较和实际比较结果出现了偏差。下一轮应该B进程调度,因为其理论stride更下,但如果不对算法进行修改,调度器将继续调度A进程。
这里有一个结论:对每次Stride调度器的调度步骤中,有其最大的步进值STRIDE_MAX和最小的步进值STRIDE_MIN之差,STRIDE_MAX – STRIDE_MIN <= PASS_MAX == BIG_STRIDE / 1 (注意最小的Priority为1)。所以我们只要将BIG_STRIDE限制在某个范围内,即可保证任意两个stride之差都会在机器整数表示的范围之内。
如何理解上文之意呢,我们举个例子:
#include <iostream>
using namespace std;
typedef unsigned int uint32_t;
int main(){
uint32_t a = ((uint32_t) -1); // 此时a为uint32_t的最大值
uint32_t b = 10;
cout << b - a; // 输出 11, 即 10 > ((uint32_t) -1)
return 0;
}
回到上文进程AB,由此不难看出,哪怕溢出之后98-65535在数值上是65535大于98,但是由于计算机的规则限定,计算机上真实情况是98大于65535,因此调度器会正确调度进程B。
所以,我们只需将BIG_STRIDE的值限制在一个uint32_t所能表示的范围(uint32_t为uCore所设置的stride值的类型),这样就可避开stride的溢出。即
#define BIG_STRIDE ((uint32_t) -1)
对Stride Scheduling调度算法的实现
①stride_init
该函数用于初始化run-queue rq,对成员变量进行正确赋值,run_list初始化后应该是一个空列表,lab6_run_pool(进程池)初始后为空,进程数目为0。
static void
stride_init(struct run_queue *rq) {
list_init(&(rq->run_list)); //init the ready process list: rq->run_list;
rq->lab6_run_pool = NULL; //init the run pool: rq->lab6_run_pool
rq->proc_num = 0; //set number of process: rq->proc_num to 0
}
②stride_enqueue
stride_enqueue将进程’‘proc’‘插入到运行队列’‘rq’‘中。这个过程应该验证/初始化’‘proc’‘的相关成员,然后将’‘lab6_run_pool’‘节点放入队列(因为我们在这里使用优先级队列)。过程还应该更新’‘rq’'结构中的元日期。 proc->time_slice表示进程的时间片分配,应该设置为rq->max_time_slice。
static void
stride_enqueue(struct run_queue *rq, struct proc_struct *proc) {
rq->lab6_run_pool = skew_heap_insert(rq->lab6_run_pool, &(proc->lab6_run_pool), proc_stride_comp_f); //(1) insert the proc into rq correctly
if (proc->time_slice == 0 || proc->time_slice > rq->max_time_slice) {
proc->time_slice = rq->max_time_slice; //(2) recalculate proc->time_slice
}
proc->rq = rq; //(3) set proc->rq pointer to rq
rq->proc_num ++; //(4) increase rq->proc_num
}
③stride_dequeue
stride_dequeue从run-queue’‘rq’‘中删除进程’‘proc’‘,该操作将由skew_heap_remove操作完成。记住要更新’‘rq’'结构。
static void
stride_dequeue(struct run_queue *rq, struct proc_struct *proc) {
rq->lab6_run_pool = skew_heap_remove(rq->lab6_run_pool, &(proc->lab6_run_pool), proc_stride_comp_f);
rq->proc_num --;
}
④proc_stride_comp_f
由于Stride Scheduling算法涉及到大量的查找,故使用斜堆skew_heap数据结构来提高算法效率,stride值最小的进程在斜堆的最顶端。proc_stride_comp_f为两个skew_heap_node_t和相应的procs的比较函数:
static int
proc_stride_comp_f(void *a, void *b)
{
struct proc_struct *p = le2proc(a, lab6_run_pool);
struct proc_struct *q = le2proc(b, lab6_run_pool);
int32_t c = p->lab6_stride - q->lab6_stride;
if (c > 0) return 1;
else if (c == 0) return 0;
else return -1;
}
⑤stride_pick_next
stride_pick_next从"run-queue"中以最小的stride值挑选元素,并返回相应的进程指针。进程指针将由宏le2proc计算,定义见kern/process/proc.h。如果队列中没有进程,则返回NULL。当一个proc结构被选中时,记得要更新proc的stride属性。(stride += BIG_STRIDE / priority)
static struct proc_struct *
stride_pick_next(struct run_queue *rq) {
/* LAB6: YOUR CODE
* (1) get a proc_struct pointer p with the minimum value of stride
(1.1) If using skew_heap, we can use le2proc get the p from rq->lab6_run_poll
(1.2) If using list, we have to search list to find the p with minimum stride value
* (2) update p;s stride value: p->lab6_stride
* (3) return p
*/
skew_heap_entry_t* she = rq->lab6_run_pool; //(1) get a proc_struct pointer p with the minimum value of stride
if (she != NULL) {
struct proc_struct* p = le2proc(she, lab6_run_pool); //(1.1) If using skew_heap, we can use le2proc get the p from rq->lab6_run_poll
if(p->lab6_priority == 0) p->lab6_stride += BIG_STRIDE;
else p->lab6_stride += BIG_STRIDE / p->lab6_priority; //(2) update p;s stride value: p->lab6_stride
return p;
}
return NULL;
}
⑥stride_proc_tick
与RR一致。
static void
stride_proc_tick(struct run_queue *rq, struct proc_struct *proc) {
if (proc->time_slice > 0) {
proc->time_slice --;
}
if (proc->time_slice == 0) {
proc->need_resched = 1;
}
}
实验心得
在实验五,创建了用户进程,并让它们正确运行。这中间也实现了FIFO调度策略。而本实验实现调度器框架,并且允许其能在单独文件中涉及具体的调度算法。再上一个实验中,当系统没有进程可以执行的时候,它会把所有cpu时间用在搜索进程池,以实现idle的目的。而我们需要一个更智能的调度,避免这种浪费CPU且复杂的调度。所以我们有了RR调度,MLFQ调度,Stride调度等等。如果上个实验是着重用户进程的创建,那么这个实验则着重于不同进程间的管理,如何更好的平衡各个进程,更好发挥它们。