1、我的博客从lab2之后,如果没有特殊说明,所有标注的代码行数位置,以labcodes_answer(答案包)里的文件为准!!!因为你以后会发现做实验用meld软件比较费时费力,对于咱们学校的验收不如直接对着答案来;
2、感谢网上的各路前辈大佬们,本人在这学期初次完成实验的过程中,各位前辈们的博客给了我很多有用的指导;本人的博客内容在现有的内容上,做了不少细节的增补内容,有些地方属个人理解,如果有错在所难免,还请各位大佬们批评指正;
3、所有实验的思考题,我把它规整到了文章最后;
4、所有实验均默认不做challenge,对实验评分无影响;
5、湖南大学的实验顺序为1 4 5 6 7 2 3 8,在实验4-7过程中涉及到实验二三的页表虚存问题,当做黑盒处理,没有过多探索。
实验五完成了用户进程的管理,可在用户态运行多个进程。但到目前为止,采用的调度策略是很简单的FIFO调度策略。本次实验,主要是熟悉ucore的系统调度器框架, 以及基于此框架的Round-Robin(RR) 调度算法。然后参考RR调度算法的实现,完成Stride Scheduling调度算法。
理解操作系统的调度管理机制
熟悉 ucore 的系统调度器框架,以及缺省的Round-Robin 调度算法
基于调度器框架实现一个(Stride Scheduling)调度算法来替换缺省的调度算法
本实验依赖实验1/2/3/4/5。请把你做的实验2/3/4/5的代码填入本实验中代码中有“LAB1”/“LAB2”/“LAB3”/“LAB4”“LAB5”的注释相应部分。并确保编译通过。注意:为了能够正确执行lab6的测试应用程序,可能需对已完成的实验1/2/3/4/5的代码进行进一步改进。
本次实验的主要内容是完成进程调度。在之前的实验中,ucore遍历进程队列(进程池),找到一个处在Runnable状态的进程并将它调度,但是本次实验中需要用到RR算法和Stride算法。
使用meld比较软件,发现以下文件需要更改:
proc.c
default_pmm.c
pmm.c
swap_fifo.c
vmm.c
trap.c
需要修改的部分如下:
1、alloc_proc函数需要新增成员(kern/process/proc.c,86——128行)
更改部分为120——125行:
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; //进程运行进度初始化(针对于stride调度算法,下同)
proc->lab6_priority = 0; //初始化优先级
相关的解释和定义可以在(kern/process/proc.h,61——66行)找到:
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)
参数的解释是:
rq:当前的进程在队列中的指针;
run_link:运行队列的指针;
time_slice:时间片;
lab6_stride:代表现在执行到了什么地方(stride调度算法,下同);
lab6_priority:进程优先级。
2、trap_dispatch函数(kern/trap/trap.c,234——237行),需要更改对于定时器做初始化,修改的部分如下:
ticks ++;
assert(current != NULL);
run_timer_list(); //调用该函数更新定时器,并根据参数调用调度算法
break;
这个run_timer_list函数被定义在(kern/schedule/sched.c,146+行)
void run_timer_list(void) {
bool intr_flag;
local_intr_save(intr_flag);
{
list_entry_t *le = list_next(&timer_list);
if (le != &timer_list) {
timer_t *timer = le2timer(le, timer_link);
assert(timer->expires != 0);
timer->expires --;
while (timer->expires == 0) {
le = list_next(le);
struct proc_struct *proc = timer->proc;
if (proc->wait_state != 0) {
assert(proc->wait_state & WT_INTERRUPTED);
}
else {
warn("process %d's wait_state == 0.\n", proc->pid);
}
wakeup_proc(proc);
del_timer(timer);
if (le == &timer_list) {
break;
}
timer = le2timer(le, timer_link);
}
}
sched_class_proc_tick(current);
}
local_intr_restore(intr_flag);
}
Round Robin调度算法(简称RR,轮转调度)的调度思想是让所有 runnable 态的进程分时轮流使用 CPU 时间。调度器维护当前 runnable进程的有序运行队列。当前进程的时间片用完之后,调度器将当前进程放置到运行队列的尾部,再从其头部取出进程进行调度。
首先,本实验中的调度都是基于调度类的成员函数实现的,相关定义在
(kern/schedule/sched.h,35——55行),定义了几个成员函数。
struct sched_class {
// the name of sched_class
const char *name;
// Init the run queue
void (*init)(struct run_queue *rq);
// put the proc into runqueue, and this function must be called with rq_lock
void (*enqueue)(struct run_queue *rq, struct proc_struct *proc);
// get the proc out runqueue, and this function must be called with rq_lock
void (*dequeue)(struct run_queue *rq, struct proc_struct *proc);
// choose the next runnable task
struct proc_struct *(*pick_next)(struct run_queue *rq);
// dealer of the time-tick
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[]);
*/
};
其中有初始化,入队,出队等操作。
在实验六中,实现一个调度算法,必须具有这五个函数,才能满足调度类。
RR算法的主要实现在(kern/schedule/default_sched.c)中定义:下面是其分析:
static void RR_init(struct run_queue *rq) {
list_init(&(rq->run_list)); //初始化运行队列
rq->proc_num = 0;
}
第一部分Init:是初始化环节,初始化rq的进程队列,并将其进程数量置零。
其中,struct run_queue的定义如下:(kern/schedule/sched.h,57——63行)
struct run_queue {
list_entry_t run_list; //进程队列
unsigned int proc_num; //进程数量
int max_time_slice; //最大时间片长度(RR)
skew_heap_entry_t *lab6_run_pool;
//在stride调度算法中,为了“斜堆”数据结构创建的一种特殊进程队列,本质就是进程队列。
};
第二部分enqueue:是一个进程入队的操作:进程队列是一个双向链表,一个进程加入队列的时候,会将其加入到队列的第一位,并给它初始数量的时间片;并更新队列的进程数量。
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 ++;
}
第三部分dequeue:从就绪队列中取出这个进程,并将其调用list_del_init删除。同时,进程数量减一。
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 --;
}
第四部分pick_next:通过list_next函数的调用,会从队尾选择一个进程,代表当前应该去执行的那个进程。如果选不出来有处在就绪状态的进程,那么返回NULL,并将执行权交给内核线程idle,idle的功能是不断调用schedule,直到整个系统出现下一个可以执行的进程。
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返回的是一个进程
}
return NULL;
}
第五部分:proc_tick:产生时钟中断的时候,会触发tick函数的调用,对应于上图中调度点的第六种情况。
每次产生了时钟中断,代表时间片数量减一(因为中断和时间片的关系,在练习0的中断处理函数中修改,变得相关联)。
一旦时间片用完了,那么就需要把该进程PCB中的need_resched置为1,代表它必须放弃对于CPU的占有,需要将别的进程调度进来执行,而当前进程需要等待了。
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;
}
}
最后一步:在schedule初始化的时候,需要填写一个初始化信息,那么这里就填上我们所实现的类函数,那么系统就可以按照这个方式去执行了。
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,
};
调度初始化的函数sched_init被定义在(kern/schedule/sched.c,45——56行):
如下所示,将sched_class设置为刚刚定义的类名,就可以完成初始化绑定。
void sched_init(void) {
list_init(&timer_list);
sched_class = &default_sched_class;
rq = &__rq;
rq->max_time_slice = MAX_TIME_SLICE;
sched_class->init(rq);
cprintf("sched class: %s\n", sched_class->name);
}
先用default_sched_stride_c覆盖default_sched.c,即覆盖掉Round Robin调度算法的实现,覆盖掉之后需要在该框架上实现Stride Scheduling调度算法。
考察 round-robin 调度器,在假设所有进程都充分使用了其拥有的 CPU 时间资源的情况下,所有进程得到的 CPU 时间应该是相等的。但是有时候我们希望调度器能够更智能地为每个进程分配合理的 CPU 资源。假设我们为不同的进程分配不同的优先级,则我们有可能希望每个进程得到的时间资源与他们的优先级成正比关系。Stride调度是基于这种想法的一个较为典型和简单的算法。除了简单易于实现以外,它还有如下的特点:
可控性:如我们之前所希望的,可以证明 Stride Scheduling对进程的调度次数正比于其优先级。
确定性:在不考虑计时器事件的情况下,整个调度机制都是可预知和重现的。该算法的基本思想可以考虑如下:
1. 为每个runnable的进程设置一个当前状态stride(执行进度),表示该进程当前的调度权。另外定义其对应的pass(步长)值,表示对应进程在调度后,stride 需要进行的累加值。
2. 每次需要调度时,从当前 runnable 态的进程中选择 stride最小的进程调度。
3. 对于获得调度的进程P,将对应的stride加上其对应的步长pass(只与进程的优先权有关系)。
4. 在一段固定的时间之后,回到 2.步骤,重新调度当前stride最小的进程。
比如,对于上述过程,现在我们就需要选择调度stride最小的P1,P1执行一个步长16,此时stride为116,接下来会选择stride最小的P3(112)去执行。
谁的pass值越小,谁被调度的次数就越多。
其中,在练习0的过程中,我们做过一个对于PCB的初始化,其中就包含两条信息:
proc->lab6_stride = 0; //步长初始化(针对于stride调度算法,下同)
proc->lab6_priority = 0; //初始化优先级
这两个参数对应于上图。其中,priority与pass呈反比,因为谁的pass越小,为了进度能够“跟上”,那么调度的优先级就会更高。
计算公式是:priority = BIG_NUM / pass,其中BIG可以是一个很大的数字,实验中实际使用的是MAX INT。
为了比较各个进程之间的stride值,我们使用了“斜堆”这样的数据结构。斜堆是一种二叉树结构,又被称作为“自适应堆”,其特点是根结点的值最小。由此,我们可以用程序中已经定义好的数据结构完成对于当前调度进程的选择。
Stride算法的实现如下:
第一步:比较
既然需要调度当前stride最小的进程去执行,那么必须要有比较部分:
实现为comp函数(kern/schedule/default_sched.c,15——24行)
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;
}
其中,a和b是两个进程的指针,通过指针指向的地点从队列中调出这两个进程并拷贝给p个q,使用p和q直接比较他们的stride值,并根据返回值,调整斜堆。
第二步:正式调度
正式调度需要用到练习1中所提到的schedule类,它包含一个五元组:
init、enqueue、dequeue、pick_next、tick。
2.1:init初始化(kern/schedule/default_sched.c,37——43行)
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;
}
这里的处理和之前的RR无区别。
唯一的不同在于,初始化进程队列的时候是对于lab6_run_pool(实验六进程池)进行操作,因为使用了斜堆的数据结构,代码中,为这个变量已经建立好了相应的结构,因此需要这样做。如果还是初始化rq,那么由于rq是基于双向链表实现的,会出现一些错误。
2.2:enqueue入队(kern/schedule/default_sched.c,58——73行)
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 ++;
}
如果你使用了斜堆数据结构,那么就应该调用的是斜堆的插入函数,这个库类似于前面提到的list.h,属于linux内核部分,这里用到的也是其改进版本,具体定义在lib/skew_heap.h中。
skew_heap_entry_t *skew_heap_insert函数实现在(lib/skew_heap.h,16——18行)
static inline skew_heap_entry_t *skew_heap_insert(
skew_heap_entry_t *a, skew_heap_entry_t *b,
compare_f comp) __attribute__((always_inline));
第一和第二参数都是堆中的元素,第三是比较法则,因为斜堆数据结构是自组织的,可以对自身进行排序,因此插入进去之后就需要排序了。
其他处理,和RR调度算法相同,取得处于队首的进程进行调度,并为其分配时间片。
2.3:dequeue出队(kern/schedule/default_sched.c,83——94行)
static void
stride_dequeue(struct run_queue *rq, struct proc_struct *proc) {
/* LAB6: YOUR CODE */
#if USE_SKEW_HEAP
rq->lab6_run_pool =
skew_heap_remove(rq->lab6_run_pool, &(proc->lab6_run_pool), proc_stride_comp_f);
#else
assert(!list_empty(&(proc->run_link)) && proc->rq == rq);
list_del_init(&(proc->run_link));
#endif
rq->proc_num --;
}
和enqueue一样,使用了斜堆的数据结构就必须配套使用其相关函数。
skew_heap_entry_t *skew_heap_remove函数实现在(lib/skew_heap.h,16——18行)
static inline skew_heap_entry_t *skew_heap_remove(
skew_heap_entry_t *a, skew_heap_entry_t *b,
compare_f comp) __attribute__((always_inline));
和之前的insert一样,具体实现是在linux内核,但是也需要传入一个比较方法函数,用于自组织,否则删除一个结点会对于数据结构产生很大的影响。
其他处理方式,也和RR一样,在进程队列中删除这个进程就结束了。
2.4:pick_next(kern/schedule/default_sched.c,108——134行)
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; //如果调度队列为空,那么返回NULL,表示没有进程能够被调度,此时idpe内核线程会执行,不停调用schedule函数,直到找到一个符合条件的进程被调度上去。
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) //核心部分是,先扫描整个运行队列,返回其中stride值最小的对应进程,然后更新对应进程的stride值,将步长设置为优先级的倒数,如果为0则设置为最大的步长。
{
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)
p->lab6_stride += BIG_STRIDE;
else p->lab6_stride += BIG_STRIDE / p->lab6_priority;
return p;
}
2.5:tick(kern/schedule/default_sched.c,145——153行)
stride_proc_tick(struct run_queue *rq, struct proc_struct *proc) {
/* LAB6: YOUR CODE */
if (proc->time_slice > 0) {
proc->time_slice --;
}
if (proc->time_slice == 0) {
proc->need_resched = 1;
}
}
主要工作是检测当前进程是否已用完分配的时间片。如果时间片用完,应该正确设置进程结构的相关标记来引起进程切换。和RR没有任何区别。
1、调度算法的实现基于调度类五元组:初始化、入队、出队、选择下一个、中断处理。
2、RR和Stride代码主要区别在于Stride的基于比较步长和进程执行进度的思想,要求频繁比较Stride值,因此选用了适应斜堆的函数,就代码而言,差别不大。
思考题一:请理解并分析sched_calss中各个函数指针的用法,并接合Round Robin 调度算法描ucore的调度执行过程。
调度类的代码定义如下:
struct sched_class {
const char *name;//调度类名
void (*init)(struct run_queue *rq);//调度队列初始化
void (*enqueue)(struct run_queue *rq, struct proc_struct *proc);//入队
void (*dequeue)(struct run_queue *rq, struct proc_struct *proc);//出队
struct proc_struct *(*pick_next)(struct run_queue *rq);//切换
void (*proc_tick)(struct run_queue *rq, struct proc_struct *proc);//触发
};
调度点会触发关于调度相关的工作。相关的工作如下:
Proc.c:do_exit:用户线程执行结束,主动放弃CPU;
Proc.c:do_wait:父进程等待子进程结束,主动放弃CPU;
Proc.c:init_main:Initproc内核线程等待所有用户进程结束并回收系统资源;
Proc.c:cpu_idle:选取内核线程中处于就绪态的进程,并进行切换;
Sync.h::lock:进程如果无法得到锁,则主动放弃CPU;
Trap.c::trap:修改当前进程时间片,若时间片用完,则设置need_resched为1,让当前进程放弃CPU。
思考题二:请在实验报告中简要说明如何设计实现”多级反馈队列调度算法“,给出概要设计,鼓励给出详细设计
假设进程一共有4个调度优先级,分别为0、1、2、3,其中0位最高优先级,3位最低优先级。为了支持4个不同的优先级,在运行队列中开4个队列,分别命名为rq -> run_list[0..3]。除此之外,在proc_struct中加入priority成员表示该进程现在所处的优先级,初始化为0。
1、MLFQ_init:进程队列初始化,和RR算法实现一样,不同之处在于需要初始化4个队列,分别对应0、1、2、3。
2、MLFQ_enqueue(struct run_queue *rq, struct proc_struct *proc):判断proc进程的时间片proc -> time_slice是否为0,如果为0,则proc -> priority += 1,否则不变。根据proc加入到对应优先级的列表中去。时间片的长度也和优先级有关,低优先级的时间片长度设置为高优先级的两倍。
3、MLFQ_dequeue(struct run_queue *rq, struct proc_struct *proc):将proc进程从相应的优先级运行队列中删除。
4、MLFQ_pick_next(struct run_queue *rq):为了避免优先级较低的进程出现饥饿现象,对每个优先级设置一定的选中概率,高优先级是低优先级选中概率的两倍,然后选出一个优先级,找到这个优先级中的第一个进程返回。
5、MLFQ_proc_tick(struct run_queue *rq, struct proc_struct *proc):和RR算法相似。
如果未做challenge,得分应当是84 / 165,如果make grade无法满分,尝试注释掉tools/grade.sh的221行到233行(在前面加上“#”)。