天天看点

Ucore Lab6 操作系统实验LAB6实验报告

LAB6实验报告

实验相关知识

(主要从教学ppt、gitbook、学堂在线上了解掌握并根据CSDN查询了解更加详细的信息。同时结合自己的理论课笔记,实际上是对理论知识的复习)

1.处理机调度的相关概念

本章主要关于处理机调度,即从就绪队列中挑选下一次执行的进程,也就是给进程CPU资源使用。

CPU调度的两种模式:

  • 抢占式调度。采用非抢占调度,一旦CPU分配给一个进程,那么该进程会一直使用CPU直到进程终止或切换到等待状态
  • 非抢占式调度。系统可以在一个进程仍在使用CPU将其中断并切换调度下一个进程使用

内核执行调度程序的条件:

  1. 当一个进程从运行状态切换到等待状态 running->waiting 例如:I/O请求
  2. 当一个进程从运行状态切换到就绪状态 running->ready 例如:发生中断
  3. 当一个进程从等待状态切换到就绪状态 waiting->ready 例如:I/O请求完成
  4. 当一个进程终止时

需要强调的是,非抢占式调度策略发生的条件为 1 和 4 .抢占式调度策略发生条件为以上均可。

**调度准则:**简单来说,我们希望实现的调度机制能够达到的目标是:CPU使用率尽可能大、吞吐量(单位时间内完成的进程数)尽可能大、周转时间越少越好、等待时间越少越好、响应时间越少越好。

2.调度算法

在介绍具体调度算法之前,需要了解一个事实,那就是进程的CPU使用时间大量地集中于0~8ms这个区间,了解这个事实才可以设计出更好更优秀的算法:

Ucore Lab6 操作系统实验LAB6实验报告

主要介绍FCFS、SJF、RR、Priority四种调度策略

  • 先到先服务算法 FCFS

    先请求的CPU的进程先分配到CPU。维护一个就绪队列,该就绪队列依据进程进入就绪状态的先后顺序排列。当进程进入等待或结束状态时,就绪队列中的下一个进程获得CPU时间进入运行状态。

    Ucore Lab6 操作系统实验LAB6实验报告
    • FCFS算法的优点:简单、易于实现
    FCFS算法的缺点:I/O资源和CPU资源利用率较低(CPU密集型进程可能会导致I/O设备长时间闲置),平均等待时间波动较大,可能导致护航现象(长进程在短进程之前执行导致执行时间大大增加)

最短作业优先算法 SJF

预期占用最短CPU时间的进程最先分配到CPU。维护一个就绪队列,该就绪队列依据进程的CPU时间区间由小到大排列,当进程进入等待或结束状态时,就绪队列中的下一个进程获得CPU时间进入运行状态。需要强调的是,一个进程将占用的CPU时间是不可预知的,故这是理论层面算法。该算法有抢占式和非抢占式两种。 非抢占式:每次都给最短的时间,待进程结束后再为当前最短时间分配。抢占式则是一旦有更短的CPU时间的进程,则采取调度策略将其换入运行状态。需要注意的是,抢占式SJF算法可能导致频繁的上下文切换。

Ucore Lab6 操作系统实验LAB6实验报告

显然SJF有较好的平均等待时间,实际上有最优的平均等待时间。但存在饥饿现象(由于不断调度短进程导致长进程无法获得CPU时间而一直处于就绪队列不能执行)

时间片轮转算法 RR

**设置一个时间片,执行过程中,如果进程只需要小于时间片的CPU区间,则进程完成后释放CPU。否则定时器中断并产生系统中断,进行上下文切换,将进程加入到就绪队列的尾部。**可以令每个时间片结束时采用FCFS切换到下一个进程。每个进程最长等待时间不超过(n-1)*q时间。

一个设置时间片为20的RR算法示例:

Ucore Lab6 操作系统实验LAB6实验报告
  • **RR算法的时间片设置较为重要,如果设置时间片过大,则接近甚至成为FCFS算法,等待时间将较大;如果设置时间片较小,则产生过多上下文切换,影响系统的吞吐量。**因此,由上面给出的大量进程CPU时间位于8ms以内这个事实,我们可以参考设置RR为8ms左右。

    RR算法的等待时间基本处于FCFS和SJF之间,但具有良好的响应时间。还有需要强调的是RR算法是公平的,交互性较好,等待时间性能较差

  • 优先级调度策略 Priority

    **为每个进程设置一个优先级,优先级高的进程会优先分配到CPU。**实现时可以维护多个队列,每个队列表示一种优先级,优先处理高优先级队列,同优先级采取FCFS即可。优先级调度策略可以采用抢占式和非抢占式两种,实际上SJF可以看作将CPU时间由小到大设为优先级从高到低的策略。可能导致低优先级进程处于饥饿现象,解决办法:老化技术(逐步提升等待时间较长的进程的优先级)

个人认为理解掌握这四种算法核心后基本对其他衍生或者更复杂的算法应该也能很快读懂。

实验过程

本次实验,主要是熟悉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的代码进行进一步改进。

答:和上个lab一样,这次练习0对前面的代码有一些改进,故在此说明一下。(做练习0必须细心加认真呀)

这次除了将之前的代码copy到框架中之外,还对trap.c和proc.c中部分函数进行了些改动:

trap.c中的

trap_dispatch

函数:在时钟产生的地方需要对定时器做初始化,用于调度算法

static void trap_dispatch(struct trapframe *tf)
{
   .....
   case IRQ_OFFSET + IRQ_TIMER:	
 		ticks ++;
        assert(current != NULL);
      //更新定时器,并根据参数调用调度算法  
        run_timer_list();
        break;
   .....
}
           

proc.c中的

proc_struct

结构体:增加了一些用户调度算法的成员变量

// 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) 
    {
        proc->state = PROC_UNINIT;//设置进程为未初始化状态
        proc->pid = -1;          //未初始化的进程id=-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;//进程相关指针初始化  
        //*cptr-->children | *yptr-->younger | *optr-->older
        proc->rq = NULL; //初始化运行队列为空
//----------------LAB6的补充:---------------------------------        
        list_init(&(proc->run_link)); //初始化运行队列的指针
        proc->time_slice = 0; //初始化时间片
        proc->lab6_run_pool.left = NULL;//初始化各类指针为空,包括父进程等待
        proc->lab6_run_pool.right = NULL;
        proc->lab6_run_pool.parent = NULL;  
        proc->lab6_stride = 0; //步数初始化
        proc->lab6_priority = 0; //初始化优先级
    }
    return proc;
}

           

基于此,即可开始本次实验的主要内容。

练习1: 使用 Round Robin 调度算法(不需要编码)

完成练习0后,建议大家比较一下(可用kdiff3等文件比较软件)个人完成的lab5和练习0完成后的刚修改的lab6之间的区别,分析了解lab6采用RR调度算法后的执行过程。执行make grade,大部分测试用例应该通过。但执行priority.c应该过不去。

请在实验报告中完成:

  • 请理解并分析sched_calss中各个函数指针的用法,并接合Round Robin 调度算法描ucore的调度执行过程
  • 请在实验报告中简要说明如何设计实现”多级反馈队列调度算法“,给出概要设计,鼓励给出详细设计

答:先执行make grade,检验一下练习0是否无误:

Ucore Lab6 操作系统实验LAB6实验报告

因此可知符合预期,只有priority无法通过,与题目描述一致。因此可知练习0是正确的。

接下来分析ucore是如何实现RR算法的:(在"实验相关知识"的"调度算法"内容中由具体的RR算法介绍)

RR算法的核心是设置一个时间片,执行过程中,如果进程只需要小于时间片的CPU区间,则进程完成后释放CPU。否则定时器中断并产生系统中断,进行上下文切换,将进程加入到就绪队列的尾部,然后从其头部取出进程进行调度。

通过数据结构 struct run_queue 来描述完整的 run_queue( 运行队列) 。 它的主要结构如下:(运行队列存储的是当前可以调度的进程, 所以, 只有状态为runnable的进程才能够进入运行队列。 当前正在运行的进程并不会在运行队列中)

struct run_queue 
{
    list_entry_t run_list;//运行队列的哨兵结构,看作是队列的头和尾
    unsigned int proc_num;//内部的进程数目 
    int max_time_slice; //每个进程一轮最多占用时间
    //优先队列形式的容器,
    skew_heap_entry_t *lab6_run_pool;
};
           

RR算法的框架:

//sched_class类似于之前对物理内存、虚拟内存管理时的结构体,是调度算法的总体框架
struct sched_class default_sched_class = {
    .name = "RR_scheduler", 
    .init = RR_init,   //初始化进程
    .enqueue = RR_enqueue, //将进程p加入到运行队列q中
    .dequeue = RR_dequeue,  //将进程p从运行队列q中删除
    .pick_next = RR_pick_next,  //返回运行队列中下一个可执行的进程
    .proc_tick = RR_proc_tick,  //用于处理时间片
};
           

RR算法的具体实现主要在

default_sched.c

//对就绪队列进行初始化,并初始化proc_num为0
static void
RR_init(struct run_queue *rq) 
{
    list_init(&(rq->run_list));
    rq->proc_num = 0;
}
//把当前进程加入到就绪队列的最后,将当前进程的时间片时间重置为最大,更新队列中进程数目
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)); 
    //如果进程时间片用完,将其重置为max_time_slice
    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的进程控制块指针的队列元素删除,并把表示就绪进程个数的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)); //将proc删除
    rq->proc_num --;
}
//选择运行队列中的第一个进程交给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 NULL;
}
/*
RR_proc_tick的函数实现如下表所示。即每次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;
    }
}
           
  • 请理解并分析sched_calss中各个函数指针的用法,并接合Round Robin 调度算法描ucore的调度执行过程
    struct sched_class {
        //调度器的名字
        const char *name;
        //初始化运行队列
        void (*init)(struct run_queue *rq);
        // 将进程p插入到运行队列q中
        void (*enqueue)(struct run_queue *rq, struct proc_struct *proc);
        // 将进程p从运行队列q中删除
        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);
    };
               

    sched_class

    实现了调度算法的一个框架,将具体的实现转换为具体的接口。

    ucore通过schedule()函数进行调度,我们细看一下schedule函数的框架:

    /*
    如果一个进程是RUNNABLE状态,则通过`sched_class_enqueue()`函数将其加入运行队列,然后通过`sched_class_pick_next()`函数选择下一个要执行的线程并将其从队列中删除。具体到RR算法也就是将新的进程加入到运行队列,然后为其分配一个时间片,之后通过`pick_next()`选中新的进程时,先将其从队列中删除,并给它CPU时间,如果CPU时间到该进程仍需执行,则继续将其加入到运行队列
    */
    void
    schedule(void) {
        bool intr_flag;
        struct proc_struct *next;
        local_intr_save(intr_flag);
        {
            current->need_resched = 0;
            //插入运行队列
            if (current->state == PROC_RUNNABLE) {
                sched_class_enqueue(current);
            }
            //选取下一个运行的进程并将其从队列中删去
            if ((next = sched_class_pick_next()) != NULL) {
                sched_class_dequeue(next);
            }
            if (next == NULL) {
                next = idleproc;
            }
            next->runs ++;
            if (next != current) {
                proc_run(next);
            }
        }
        local_intr_restore(intr_flag);
    }
               
    在内核初始化时调用

    sched_init

    函数选择调度器并对调度器初始化。每当进程被唤醒则调用

    enqueue

    函数将其加入调度器等待调度的进程队列。每当发生进程切换时,首先调用

    enqueue

    将当前正在运行的进程加入等待调度的进程队列,然后调用

    pick_next

    函数获取接下来将执行的进程,并调用

    dequeue

    将被选中即将执行的进程从调度队列中移除。每当产生时钟中断时,需要调用

    proc_tick

    函数来更新调度器的时钟信息。
  • 请在实验报告中简要说明如何设计实现”多级反馈队列调度算法“,给出概要设计,鼓励给出详细设计

    理论课知识:多级反馈调度是一种采用多种调度队列的调度方式,它与多级队列调度最大的区别在于进程可以在不同的调度队列间移动,也就是说可以制定一些策略用以决定进程是否可以升级到优先级更高的队列或者降级到优先级更低的队列。通过该算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。通常实现时设计的多级反馈队列核心思想是:时间片大小随优先级级别增加而增加。同时,进程在当前时间片没有完成 则降到下一优先级。实现时可以维护多个队列,每个新的进程加入第一个队列,当需要选择一个进程调入执行时,从第一个队列开始向后查找,遇到某个队列非空,那么从这个队列中取出一个进程调入执行。如果从某个队列调入的进程在时间片用完之后仍然没有结束,则将这个进程加入其调入时所在队列之后的一个队列,并且时间片加倍。

    自己设计该算法的概要:

    • 维护三个优先级队列,可以将之前的run_list用run_list[3]来维护,其中run_list[0]表示最高优先级队列,run_list[2]表示最低优先级队列。将进程的初始态也划分为3个优先级,初始化加入队列时加入相应的优先级,通常进程可以直接加入最高优先级,随着降级逐渐降至低优先级。
    • 初始化时,将每个队列置空即可。并将每个队列对应的

      proc_num

      置0。
    • enqueue

      时,判断进程的时间片是否为0,如果是0,说明该进程应当降级,将其

      priority

      +1,否则的话不需要改变。然后将该进程根据其优先级加入到对应的队列中。
    • dequeue

      时,将进程从相应的优先级队列中删去即可,没有太大变化。
    • pick_next

      时,通过算法选出下一个要执行的进程。在这里为了避免出现低优先级进程饥饿现象,可以设置为高优先级每处理100个单位时间后,低优先级处理30个单位时间 类似的思想。用这个方法来避免低优先级进程饥饿现象。
    • proc_tick

      时钟中断所使用,每次时钟中断,减少当前进程的时间片。若为0,则将进程标记为需要调度。
    • 具体实现时,将run_list[0]队列采用时间片为8ms的RR算法(因为绝大多数进程所需的CPU时间是小于8ms的),将run_list[1]队列采用时间片为16ms的RR算法,将run_list[2]队列采用FCFS算法。
    • 每次选取执行的队列时,可以用概率或者时分的方法将优先级进行区分。
    • Ucore Lab6 操作系统实验LAB6实验报告
    • 练习2: 实现 Stride Scheduling 调度算法(需要编码)

首先需要换掉RR调度器的实现,即用default_sched_stride_c覆盖default_sched.c。然后根据此文件和后续文档对Stride度器的相关描述,完成Stride调度算法的实现。

答:Stride算法是结合时间片的一种优先级调度策略。每一个时间片结束时,选择就绪状态的进程中Stride值最小的进程分配一个时间片,在一个时间段中进程所获得的时间片数量和进程的优先级大致成正比。该算法较为重要的几个变量:

  • stride:步幅,表示该进程当前的调度权。每次需要调度时优先选择stride值最小的进程进行调度。每次调度后stride加上pass。
  • pass:表示每次前进的步数。该值仅与优先级相关。可以令

    P.pass =BigStride / P.priority

    ,因此pass与优先级成反比。为每个进程分配的时间将与其优先级成正比,因为pass越小通常被调度次数就会越多。

该调度算法的基本策略:

  • 为每个runnable的进程设置一个当前状态stride并定义其对应的pass值
  • 每次需要调度时,从当前 runnable 态的进程中选择 stride最小的进程调度。对于获得调度的进程P,将对应的stride加上其对应的步长pass。
  • 在一段固定的时间之后,回到步骤2,重新执行调度。
  • 可以证明的是,如果令P.pass = BigStride / P.priority其中P.priority表示进程的优先权(大于1),而BigStride表示一个预先定义的大常数,则该调度方案为每个进程分配的时间将与其优先级成正比。

由于每次都需要选择最小的stride进程,因此用优先队列数据结构将使得该算法复杂度降低。本次实验提供了

libs/skew_heap.h

作为优先队列的一个实现,对优先队列的实现进行说明:

//初始化一个队列节点
static inline void
skew_heap_init(skew_heap_entry_t *a)
{
     //将节点的子节点、父节点均初始化为null
     a->left = a->right = a->parent = NULL;
}
将a与b两个堆进行合并
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 a<b,说明a应当做堆头
     if (comp(a, b) == -1)
     { 
          //令a为最终的堆的头,将b与a的子节点进行合并后的结果作为a的子节点即可
          r = a->left;
          l = skew_heap_merge(a->right, b, comp);    
          a->left = l;
          a->right = r;
          if (l) l->parent = a;
          return a;
     }
     else
     { //令b为最终的堆的头,将a与b的子节点进行合并后的结果作为b的子节点即可
          r = b->left;
          l = skew_heap_merge(a, b->right, comp);
          
          b->left = l;
          b->right = r;
          if (l) l->parent = b;

          return b;
     }
}
// 将节点 b 插入至以节点 a 为队列头的队列中去,返回插入后的队列
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);
     return skew_heap_merge(a, b, comp);
}
//将节点 b 从以节点 a 为队列头的队列中删去,返回删除后的队列
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;
     //将b的左儿子和右儿子堆进行合并并插入到原来b的位置即可
     skew_heap_entry_t *rep = skew_heap_merge(b->left, b->right, comp);
     if (rep) rep->parent = p;
     //p不为空说明b不是根节点,因此将b子节点对应的堆合并起来并且插入,然后返回a即可
     if (p)
     {
          if (p->left == b)
               p->left = rep;
          else p->right = rep;
          return a;
     }
     //如果b是根节点,则将合并后的堆返回
     else return rep;
}
           

掌握好已定义的堆的用法后将使得之后的任务变得轻松高效。

之后根据

default_sched_stride_c

所给的提示进行编程,在这里我将

default_sched_stride_c

中的代码复制到了

default_sched.c

中,完成编程并测试运行

首先定义BIG_STRIDE为

(((uint32_t)-1) / 2)

,因为我们希望该值尽可能地较大,这样使得对于不同的优先级pass值有较好的区分度,同时,我们又希望在比较时采用有符号比较,避免出现减法后溢出的现象,这就需要保留一位作为符号位,因此在这两个限制条件下的最优BIG_STRIDE为

0x7FFFFFFF

.

我们可以看到已经定义好的函数

proc_stride_comp_f

,用来比较stride,为有符号比较

//比较a和b的stride
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;
}
           

之后进行初始化,初始化运行队列,并初始化当前的运行队,然后设置当前运行队列内进程数目为0。根据提示非常容易完成。代码如下:

static void
stride_init(struct run_queue *rq) 
{
      list_init(&rq->run_list); //初始化run_list
      rq->lab6_run_pool=NULL; //初始化运行堆
      rq->proc_num=0;   //初始化当前队列运行进程数目为0
}
           

stride_enqueue

初始化刚进入运行队列的进程 proc 的stride属性,然后比较队头元素与当前进程的步数大小,选择步数最小的运行,即将其插入放入运行队列中去,这里并未放置在队列头部。最后初始化时间片,然后将运行队列进程数目加一。

static void
stride_enqueue(struct run_queue *rq, struct proc_struct *proc) {
  //将proc对应的堆插入到rq中
  rq->lab6_run_pool = skew_heap_insert(rq->lab6_run_pool, &(proc->lab6_run_pool), proc_stride_comp_f);
  //更新一下proc的时间片
    if (proc->time_slice == 0 || proc->time_slice > rq->max_time_slice)
        proc->time_slice = rq->max_time_slice;
   //将proc的rq指向总的rq,rq中的进程数+1
    proc->rq = rq;
    rq->proc_num++;
}
           

stride_dequeue

将进程从运行队列移走时,需要将进程从斜堆中删除,并将运行队列的进程计数减一。

static void
stride_dequeue(struct run_queue *rq, struct proc_struct *proc) {
   //将proc从堆中删除
    rq->lab6_run_pool = skew_heap_remove(rq->lab6_run_pool, &(proc->lab6_run_pool), proc_stride_comp_f);
    //将rq的进程数减1
    rq->proc_num--;
}
           

stride_pick_next

选择下一个要调度的进程,只需要选择stride值最小的进程即可,因此将堆顶进程取出,并将堆恢复即可。

static struct proc_struct *
stride_pick_next(struct run_queue *rq)
{
    if (rq->lab6_run_pool == NULL)
    {
           return NULL;
     }
    //得到堆顶元素,并将其堆恢复
    struct proc_struct *p = le2proc(rq->lab6_run_pool, lab6_run_pool);
    //将p对应的进程的stride加上pass值
    p->lab6_stride += BIG_STRIDE / p->lab6_priority;
    return p;
}

           

stride_proc_tick

函数用于处理时钟,和RR算法类似,如果time slice大于0,则将值减一。否则认为其时间片用完,需要调度。

static void
stride_proc_tick(struct run_queue *rq, struct proc_struct *proc) 
{
    if (proc->time_slice == 0)
    {
     proc->need_resched = 1;
    } 
    else 
    {
      --proc->time_slice;
    }
}
           

完成函数后,将原来的

default_sched_class

注释。用stride算法的调度类:

struct sched_class default_sched_class = {
     .name = "stride_scheduler",
     .init = stride_init,
     .enqueue = stride_enqueue,
     .dequeue = stride_dequeue,
     .pick_next = stride_pick_next,
     .proc_tick = stride_proc_tick,
};
           

至此,该练习基本完成。执行make qemu以及make grade查看具体的运行结果。

make qemu结果:

Ucore Lab6 操作系统实验LAB6实验报告

make grade结果:

Ucore Lab6 操作系统实验LAB6实验报告

故运行无误。

但是这里需要说明的是,可能是虚拟机等原因,会导致相同代码运行可能出现不同的结果,grade也可能只有163.具体原因暂不清楚,但可以确定这不是代码导致的。如果make grade没有出现170可能需要make clean后再尝试一次,make qemu有时也会运行name=“exit”而不是"priority"的进程。

扩展练习 Challenge :实现 Linux 的 CFS 调度算法

在ucore的调度器框架下实现下Linux的CFS调度算法。可阅读相关Linux内核书籍或查询网上资料,可了解CFS的细节,然后大致实现在ucore中。

答:CFS算法是想要让每个进程的运行时间尽可能相同,那么记录每个进程已经运行的时间即可。在进程控制块结构中增加一个属性表示运行时间。每次需要调度的时候,选择已经运行时间最少的进程调入CPU执行。我在google上查询的关于CFS算法的实现时普遍采用了红黑树,但由于红黑树实现起来过于复杂且堆已经实现并且可以满足要求,故在此我将采用堆来实现。

run_queue

中增添一个堆,练习2中已经用了

skew_heap_entry_t * lab6_run_pool;

在此继续使用。

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;
};
           

在proc_struct中增添了几个辅助完成该算法的变量:

struct proc_struct {
    ....
      //---------------用于CFS算法--------------------------------------------------
    int fair_run_time;                          //虚拟运行时间
    int fair_priority;                          //优先级系数:从1开始,数值越大,时间过得越快
    skew_heap_entry_t fair_run_pool;            // 运行进程池
}
           

将proc初始化时将增添的三个变量一并初始化:

skew_heap_init(&(proc->fair_run_pool));
    proc->fair_run_time = 0;
    proc->fair_priority = 1;
           

具体实现算法:

比较函数:

proc_fair_comp_f

,实际上该函数和练习2的stride算法中的comp函数思想一致。

//类似于stride算法,这里需要比较的是两个进程的fair_run_time
static int proc_fair_comp_f(void *a, void *b)
{
     struct proc_struct *p = le2proc(a, fair_run_pool);
     struct proc_struct *q = le2proc(b, fair_run_pool);
     int c = p->fair_run_time - q->fair_run_time;
     if (c > 0) return 1;
     else if (c == 0) return 0;
     else return -1;
}
           

初始化:

//init函数
static void fair_init(struct run_queue *rq) {
    //进程池清空,进程数为0
    rq->lab6_run_pool = NULL;
    rq->proc_num = 0;
}
           

加入队列:

//类似于stride调度
static void fair_enqueue(struct run_queue *rq, struct proc_struct *proc)  
{
  //将proc对应的堆插入到rq中
    rq->lab6_run_pool = skew_heap_insert(rq->lab6_run_pool, &(proc->fair_run_pool), proc_fair_comp_f);
    //更新一下proc的时间片
    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 ++;
}
           

移出队列:

static void fair_dequeue(struct run_queue *rq, struct proc_struct *proc) {
    rq->lab6_run_pool = skew_heap_remove(rq->lab6_run_pool, &(proc->fair_run_pool), proc_fair_comp_f);
    rq->proc_num --;
}
           

选取下一个进程:

//pick_next,选择堆顶元素即可
static struct proc_struct * fair_pick_next(struct run_queue *rq) {
    if (rq->lab6_run_pool == NULL)
        return NULL;
    skew_heap_entry_t *le = rq->lab6_run_pool;
    struct proc_struct * p = le2proc(le, fair_run_pool);
    return p;
}
           

需要更新虚拟运行时,增加的量为优先级系数

//每次更新时,将虚拟运行时间更新为优先级相关的系数
static void
fair_proc_tick(struct run_queue *rq, struct proc_struct *proc) {
    if (proc->time_slice > 0) {
        proc->time_slice --;
        proc->fair_run_time += proc->fair_priority;
    }
    if (proc->time_slice == 0) {
        proc->need_resched = 1;
    }
}
           

基本该算法的实现如此,为了通过测试可能需要一些小调整。

具体运行截图:

Ucore Lab6 操作系统实验LAB6实验报告

必须承认,这个CFS调度的性能相较于LINUX上还是较差的。主要原因是LINUX实现该算法时采用了红黑树,红黑树对于维护树的平衡性以及每次取出节点时的恢复性能较好。

堆的结构可能导致的问题在于很可能导致树的退化,这时候性能将会明显降低。

但由于时间有限,故可能会在假期再尝试用红黑树实现更为完美的CFS。

参考资料

gitbook上相关内容,其中对很多知识点的解释非常详细,认真看收获非常大,对完成实验内容帮助很大

清华大学学堂在线操作系统教学视频,该视频对理论进行了很好、很详细的讲解,并对实验部分进行了大致介绍

CSDN上与进程调度算法、堆的实现、红黑树等相关的内容

google查询相关的Stride scheduling以及CFS算法的介绍

操作系统理论课书籍

关于Stride Scheduling的论文介绍:strid-shed paper location2

如果给你带来了帮助,可点击关注,博主将继续努力推出好文。

继续阅读