练习0:填写已有实验
本实验依赖实验1/2/3/4/5。请把你做的实验2/3/4/5的代码填入本实验中代码中有“LAB1”/“LAB2”/“LAB3”/“LAB4”“LAB5”的注释相应部分。并确保编译通过。注意:为了能够正确执行lab6的测试应用程序,可能需对已完成的实验1/2/3/4/5的代码进行进一步改进。
1 改进proc_struct:
struct proc_struct {
// . . .
// 该进程是否需要调度,只对当前进程有效
volatile bool need_resched;
// 该进程的调度链表结构,该结构内部的连接组成了 运行队列 列表
list_entry_t run_link;
// 该进程剩余的时间片,只对当前进程有效
int time_slice;
// round-robin 调度器并不会用到以下成员
// 该进程在优先队列中的节点,仅在 LAB6 使用
skew_heap_entry_t lab6_run_pool;
// 该进程的调度优先级,仅在 LAB6 使用
uint32_t lab6_priority;
// 该进程的调度步进值,仅在 LAB6 使用
uint32_t lab6_stride;
};
2 改进alloc_proc函数:
static struct proc_struct *
alloc_proc(void) {
....
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;
proc->lab6_stride = 0; //设置步长为0
proc->lab6_priority = 0; //设置优先级为0
}
3 改进trap_dispatch函数:
static void
trap_dispatch(struct trapframe *tf) {
......
......
ticks ++;
assert(current != NULL);
run_timer_list(); //更新定时器,并根据参数调用调度算法
break;
......
......
}
练习1: 使用 Round Robin 调度算法
完成练习0后,建议大家比较一下(可用kdiff3等文件比较软件)个人完成的lab5和练习0完成后的刚修改的lab6之间的区别,分析了解lab6采用RR调度算法后的执行过程。执行make grade,大部分测试用例应该通过。但执行priority.c应该过不去。
请在实验报告中完成:
请理解并分析sched_calss中各个函数指针的用法,并接合Round Robin 调度算法描ucore的调度执行过程。
请在实验报告中简要说明如何设计实现”多级反馈队列调度算法“,给出概要设计,鼓励给出详细设计。
1 初始化进程队列(RR_init函数)
static void
RR_init(struct run_queue *rq) {
list_init(&(rq->run_list)); //初始化运行队列
rq->proc_num = 0; //初始化进程数为0
}
其中的run_queue结构体:
struct run_queue {
//其运行队列的哨兵结构,可以看作是队列头和尾
list_entry_t run_list;
//内部进程总数
unsigned int proc_num;
//每个进程一轮占用的最多时间片
int max_time_slice;
// For LAB6 ONLY
//优先队列形式的进程容器
skew_heap_entry_t *lab6_run_pool;
};
结构体中的skew_heap_entry结构体
struct skew_heap_entry {
//树形结构的进程容器
struct skew_heap_entry *parent, *left, *right;
};
typedef struct skew_heap_entry skew_heap_entry_t;
从代码可以看出RR_init
函数,函数比较简单,完成了对进程队列的初始化。
2 将进程加入就绪队列(RR_enqueue函数)
static void RR_enqueue(struct run_queue *rq, struct proc_struct *proc) {
assert(list_empty(&(proc->run_link))); //进程控制块指针非空
//把进程的进程控制块指针放入到rq队列末尾
list_add_before(&(rq->run_list), &(proc->run_link));
//进程控制块的时间片为0或者进程的时间片大于分配给进程的最大时间片
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 ++; //就绪进程数加一
}
看代码,首先,它把进程的进程控制块指针放入到rq队列末尾,且如果进程控制块的时间片为0或者进程的时间片大于分配给进程的最大时间片,则需要把它重置为max_time_slice
。然后在依次调整rq和rq的进程数目加一。
3 将进程从就绪队列中移除(RR_dequeue函数)
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 --; //就绪进程数减一
}
先确定当前进程控制块指针非空并且进程在就绪队列中,然后将进程控制块指针从就绪队列中删除,最后将就绪进程数减一。
4 选择下一调度进程(RR_pick_next函数)
static struct proc_struct *RR_pick_next(struct run_queue *rq) {
//选取就绪进程队列rq中的队头队列元素
list_entry_t *le = list_next(&(rq->run_list));
if (le != &(rq->run_list)) { //取得就绪进程
return le2proc(le, run_link);//返回进程控制块指针
}
return NULL;
}
先选取就绪进程队列rq中的队头队列元素,并把队列元素转换成进程控制块指针。最后返回就绪进程。
5 时间片(RR_proc_tick函数)
static void RR_proc_tick(struct run_queue *rq, struct proc_struct *proc) {
if (proc->time_slice > 0) { //到达时间片
proc->time_slice --; //执行进程的时间片time_slice减一
}
if (proc->time_slice == 0) { //时间片为0
//设置此进程成员变量need_resched标识为1,进程需要调度
proc->need_resched = 1;
}
}
即每一次时间片到时的时候,当前执行进程的时间片time_slice
便减一。如果time_slice
降到零,则设置此进程成员变量need_resched
标识为1,这样在下一次中断来后执行trap函数时,会由于当前进程程成员变量need_resched
标识为1而执行schedule函数,从而把当前执行进程放回就绪队列末尾,而从就绪队列头取出在就绪队列上等待时间最久的那个就绪进程执行。
6 sched_class
struct sched_class default_sched_class = {
.name = "RR_scheduler",
.init = RR_init,
.enqueue = RR_enqueue,
.dequeue = RR_dequeue,
.pick_next = RR_pick_next,
.proc_tick = RR_proc_tick,
};
定义一个c语言类的实现,提供调度算法的切换接口。
7 问题:
请理解并分析sched_calss中各个函数指针的用法,并接合Round Robin 调度算法描ucore的调度执行过程。
首先查看sched_calss类
struct sched_class {
// 调度器的名字
const char *name;
// 初始化运行队列
void (*init) (struct run_queue *rq);
// 将进程 p 插入队列 rq
void (*enqueue) (struct run_queue *rq, struct proc_struct *p);
// 将进程 p 从队列 rq 中删除
void (*dequeue) (struct run_queue *rq, struct proc_struct *p);
// 返回 运行队列 中下一个可执行的进程
struct proc_struct* (*pick_next) (struct run_queue *rq);
// timetick 处理函数
void (*proc_tick)(struct run_queue* rq, struct proc_struct* p);
};
调度执行过程:
RR调度算法的就绪队列在组织结构上也是一个双向链表,只是增加了一个成员变量,表明在此就绪进程队列中的最大执行时间片。而且在进程控制块proc_struct中增加了一个成员变量time_slice,用来记录进程当前的可运行时间片段。这是由于RR调度算法需要考虑执行进程的运行时间不能太长。在每个timer到时的时候,操作系统会递减当前执行进程的time_slice,当time_slice为0时,就意味着这个进程运行了一段时间(这个时间片段称为进程的时间片),需要把CPU让给其他进程执行,于是操作系统就需要让此进程重新回到rq的队列尾,且重置此进程的时间片为就绪队列的成员变量最大时间片max_time_slice值,然后再从rq的队列头取出一个新的进程执行。
请在实验报告中简要说明如何设计实现”多级反馈队列调度算法“,给出概要设计,鼓励给出详细设计。
- 首先调度优先级高的队列中的进程。若高优先级中队列中已没有调度的进程,则调度次优先级队列中的进程
- 对于同一个队列中的各个进程,按照时间片轮转法调度。比如Q1队列的时间片为N,那么Q1中的作业在经历了N个时间片后若还没有完成,则进入Q2队列等待,若Q2的时间片用完后作业还不能完成,一直进入下一级队列,直至完成。
- 在低优先级的队列中的进程在运行时,又有新到达的作业,那么在运行完这个时间片后,CPU马上分配给新到达的作业。
练习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 runpriority命令来单独调试它。大致执行结果可看附录。( 使用的是 qemu-1.0.1 )。
请在实验报告中简要说明你的设计实现过程。
相比于RR调度,Stride Scheduling函数定义了一个比较器:
static int
proc_stride_comp_f(void *a, void *b)
{
//通过进程控制块指针取得进程a
struct proc_struct *p = le2proc(a, lab6_run_pool);
//通过进程控制块指针取得进程b
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;
}
1 初始化运行队列(stride_init函数)
static void
stride_init(struct run_queue *rq) {
/* LAB6: YOUR CODE */
list_init(&(rq->run_list)); //初始化调度器类
rq->lab6_run_pool = NULL; //初始化当前进程运行队列为空
rq->proc_num = 0; //设置运行队列为空
}
初始化函数stride_init
。 开始初始化运行队列,并初始化当前的运行队,然后设置当前运行队列内进程数目为0。
2 将进程加入就绪队列(stride_enqueue函数)
static void
stride_enqueue(struct run_queue *rq, struct proc_struct *proc) {
/* LAB6: YOUR CODE */
#if USE_SKEW_HEAP
//将进程加入就绪队列
rq->lab6_run_pool =skew_heap_insert(rq->lab6_run_pool, &(proc->lab6_run_pool), proc_stride_comp_f);
#else
assert(list_empty(&(proc->run_link)));
list_add_before(&(rq->run_list), &(proc->run_link));
#endif
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 ++; //进程数加一
}
里面有一个条件编译:
#if USE_SKEW_HEAP
...
#else
...
#endif
在ucore中 USE_SKEW_HEAP 定义为1 ,因此# else 与 # endif之间的代码将会被忽略。
其中的 skew_heap_insert 函数:
static inline skew_heap_entry_t *
skew_heap_insert(skew_heap_entry_t *a, skew_heap_entry_t *b,
compare_f comp)
{
skew_heap_init(b); //初始化进程b
return skew_heap_merge(a, b, comp);//返回a与b进程结合的结果
}
函数中的skew_heap_init函数
static inline void
skew_heap_init(skew_heap_entry_t *a)
{
a->left = a->right = a->parent = NULL; //初始化相关指针
}
函数中的skew_heap_merge函数
static inline skew_heap_entry_t *
skew_heap_merge(skew_heap_entry_t *a, skew_heap_entry_t *b,
compare_f comp)
{
if (a == NULL) return b;
else if (b == NULL) return a;
skew_heap_entry_t *l, *r;
if (comp(a, b) == -1) //a进程的步长小于b进程
{
r = a->left; //a的左指针为r
l = skew_heap_merge(a->right, b, comp);
a->left = l;
a->right = r;
if (l) l->parent = a;
return a;
}
else
{
r = b->left;
l = skew_heap_merge(a, b->right, comp);
b->left = l;
b->right = r;
if (l) l->parent = b;
return b;
}
}
根据之前对该调度算法的分析,这里函数主要是初始化刚进入运行队列的进程 proc 的stride
属性,然后比较队头元素与当前进程的步数大小,选择步数最小的运行,即将其插入放入运行队列中去,这里并未放置在队列头部。最后初始化时间片,然后将运行队列进程数目加一。
3 将进程从就绪队列中移除(stride_dequeue函数)
static void
stride_dequeue(struct run_queue *rq, struct proc_struct *proc) {
/* LAB6: YOUR CODE */
rq->lab6_run_pool =
skew_heap_remove(rq->lab6_run_pool, &(proc->lab6_run_pool), proc_stride_comp_f);
rq->proc_num --;
}
里面的代码比较简单,只有一个主要函数 :skew_heap_remove
static inline skew_heap_entry_t *
skew_heap_remove(skew_heap_entry_t *a, skew_heap_entry_t *b,
compare_f comp)
{
skew_heap_entry_t *p = b->parent;
skew_heap_entry_t *rep = skew_heap_merge(b->left, b->right, comp);
if (rep) rep->parent = p;
if (p)
{
if (p->left == b)
p->left = rep;
else p->right = rep;
return a;
}
else return rep;
}
完成将一个进程从队列中移除的功能,这里使用了优先队列。最后运行队列数目减一。
4 选择进程调度(stride_pick_next函数)
static struct proc_struct *
stride_pick_next(struct run_queue *rq) {
/* LAB6: YOUR CODE */
#if USE_SKEW_HEAP
if (rq->lab6_run_pool == NULL) return NULL;
struct proc_struct *p = le2proc(rq->lab6_run_pool, lab6_run_pool);
#else
list_entry_t *le = list_next(&(rq->run_list));
if (le == &rq->run_list)
return NULL;
struct proc_struct *p = le2proc(le, run_link);
le = list_next(le);
while (le != &rq->run_list)
{
struct proc_struct *q = le2proc(le, run_link);
if ((int32_t)(p->lab6_stride - q->lab6_stride) > 0)
p = q;
le = list_next(le);
}
#endif
if (p->lab6_priority == 0) //优先级为0
p->lab6_stride += BIG_STRIDE; //步长设置为最大值
//步长设置为优先级的倒数
else p->lab6_stride += BIG_STRIDE / p->lab6_priority;
return p;
}
先扫描整个运行队列,返回其中stride值最小的对应进程,然后更新对应进程的stride值,将步长设置为优先级的倒数,如果为0则设置为最大的步长。
5 时间片部分(stride_proc_tick函数)
static void
stride_proc_tick(struct run_queue *rq, struct proc_struct *proc) {
/* LAB6: YOUR CODE */
if (proc->time_slice > 0) { //到达时间片
proc->time_slice --; //执行进程的时间片time_slice减一
}
if (proc->time_slice == 0) { //时间片为0
//设置此进程成员变量need_resched标识为1,进程需要调度
proc->need_resched = 1;
}
}
主要工作是检测当前进程是否已用完分配的时间片。如果时间片用完,应该正确设置进程结构的相关标记来引起进程切换。
总结一下过程:
首先是初始化函数stride_init
。 开始初始化运行队列,并初始化当前的运行队,然后设置当前运行队列内进程数目为0。然后是入队函数stride_enqueue
,根据之前对该调度算法的分析,这里函数主要是初始化刚进入运行队列的进程 proc 的stride
属性,然后比较队头元素与当前进程的步数大小,选择步数最小的运行,即将其插入放入运行队列中去,这里并未放置在队列头部。最后初始化时间片,然后将运行队列进程数目加一。然后是出队函数stride_dequeue
,即完成将一个进程从队列中移除的功能,这里使用了优先队列。最后运行队列数目减一。接下来就是进程的调度函数stride_pick_next
,观察代码,它的核心是先扫描整个运行队列,返回其中stride值最小的对应进程,然后更新对应进程的stride值,将步长设置为优先级的倒数,如果为0则设置为最大的步长。最后是时间片函数stride_proc_tick
,主要工作是检测当前进程是否已用完分配的时间片。如果时间片用完,应该正确设置进程结构的相关标记来引起进程切换。
运行结果: