当前位置:网站首页>ucore lab 6

ucore lab 6

2022-07-06 09:25:00 湖大金胜宇

lab 6

实验目的

  • 理解操作系统的调度管理机制
  • 熟悉 ucore 的系统调度器框架,以及缺省的Round-Robin 调度算法
  • 基于调度器框架实现一个(Stride Scheduling)调度算法来替换缺省的调度算法

实验内容

实验五完成了用户进程的管理,可在用户态运行多个进程。但到目前为止,采用的调度策略是很简单的FIFO调度策略。本次实验,主要是熟悉ucore的系统调度器框架,以及基于此框架的Round-Robin(RR) 调度算法。然后参考RR调度算法的实现,完成Stride Scheduling调度算法。

练习

对实验报告的要求:

  • 基于markdown格式来完成,以文本方式为主
  • 填写各个基本练习中要求完成的报告内容
  • 完成实验后,请分析ucore_lab中提供的参考答案,并请在实验报告中说明你的实现与参考答案的区别
  • 列出你认为本实验中重要的知识点,以及与对应的OS原理中的知识点,并简要说明你对二者的含义,关系,差异等方面的理解(也可能出现实验中的知识点没有对应的原理知识点)
  • 列出你认为OS原理中很重要,但在实验中没有对应上的知识点

练习0:填写已有实验

本实验依赖实验1/2/3/4/5。请把你做的实验2/3/4/5的代码填入本实验中代码中有“LAB1”/“LAB2”/“LAB3”/“LAB4”“LAB5”的注释相应部分。并确保编译通过。注意:为了能够正确执行lab6的测试应用程序,可能需对已完成的实验1/2/3/4/5的代码进行进一步改进。

使用meld来比较目录,我们可以知道有如下文件不同:

在这里插入图片描述

  • proc.c
  • default_pmm.c
  • pmm.c
  • swap_fifo.c
  • vmm.c
  • trap.c

需要修改的部分如下:

1、alloc_proc函数需要新增成员

更改部分为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;		//初始化优先级

相关的解释和定义如下:

struct run_queue *rq;             	//当前的进程在队列中的指针;
list_entry_t run_link;              	// 运行队列的指针
int time_slice;                   	// 时间片
skew_heap_entry_t lab6_run_pool; 	// 代表现在执行到了什么地方(stride调度算法,下同)
uint32_t lab6_stride;            	// 进程优先级
uint32_t lab6_priority;           	
// FOR LAB6 ONLY: the priority of process, set by lab6_set_priority(uint32_t)

2、trap_dispatch函数,需要更改对于定时器做初始化,修改的部分如下:

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

run_timer_list函数的定义如下:

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

3、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
}

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

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

请在实验报告中完成:

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

RR算法的基本实现思路:

R之前没觉得,慢慢(睡完)我才发现我们真的不合适ound Robin调度算法(简称RR,轮转调度)的调度思想是让所有 runnable 态的进程分时轮流使用 CPU 时间。调度器维护当前 runnable进程的有序运行队列。当前进程的时间片用完之后,调度器将当前进程放置到运行队列的尾部,再从其头部取出进程进行调度。

本实验中的调度都是基于调度类的成员函数实现的,定义了几个成员函数。

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[]); */
};

一个调度算法的实现,必须具有这几个函数,才能满足调度类。

实现过程:
1.初始化进程队列(RR_init函数)
static void
RR_init(struct run_queue *rq) {
    
    list_init(&(rq->run_list)); //初始化运行队列
    rq->proc_num = 0;           //初始化进程数为0
}

这一部分主要是初始化环节,初始化rq的进程队列,并将其进程数量置零。

其中,struct run_queue的定义如下:

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调度算法中,为了“斜堆”数据结构创建的一种特殊进程队列,本质就是进程队列。
};

结构体中的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)));
    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 ++;
}

这一部分是一个进程入队的操作:进程队列是一个双向链表,一个进程加入队列的时候,会将其加入到队列的第一位,并给它初始数量的时间片;并更新队列的进程数量。

具体的实现如下:

看代码,首先,它把进程的进程控制块指针放入到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 --; //就绪进程数减一
}

从就绪队列中取出这个进程,并将其调用list_del_init删除。同时,进程数量减一。

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

这一部分是产生时钟中断的时候,会触发tick函数的调用,对应于上图中调度点的第六种情况。

每次产生了时钟中断,代表时间片数量减一(因为中断和时间片的关系,在练习0的中断处理函数中修改,变得相关联)。

一旦时间片用完了,那么就需要把该进程PCB中的need_resched置为1,代表它必须放弃对于CPU的占有,需要将别的进程调度进来执行,而当前进程需要等待了。

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

在schedule初始化的时候,需要填写一个初始化信息,那么这里就填上我们所实现的类函数,那么系统就可以按照这个方式去执行了。

7.调度初始化的函数
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);
}

如上所示,将sched_class设置为刚刚定义的类名,就可以完成初始化绑定。

回答问题:
请理解并分析sched_calss中各个函数指针的用法,并接合Round Robin 调度算法描ucore的调度执行过程。

调度类的代码定义如下:

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调度算法的一些相关的资料(目前网上中文的资料比较欠缺)。

执行:make grade。如果所显示的应用程序检测都输出ok,则基本正确。如果只是priority.c过不去,可执行 make run-priority 命令来单独调试它。大致执行结果可看附录。( 使用的是 qemu-1.0.1 )。

请在实验报告中简要说明你的设计实现过程。

Stride调度算法:

考察 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值越小,谁被调度的次数就越多。

实现过程:

相比于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;
}

其中,a和b是两个进程的指针,通过指针指向的地点从队列中调出这两个进程并拷贝给p个q,使用p和q直接比较他们的stride值,并根据返回值,调整斜堆。

1.正式调度

正式调度需要用到练习1中所提到的schedule类,它包含一个五元组:

init、enqueue、dequeue、pick_next、tick。

2.初始化运行队列(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。

3.将进程加入就绪队列(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之间的代码将会被忽略。

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进程结合的结果
}

如果你使用了斜堆数据结构,那么就应该调用的是斜堆的插入函数,这个库类似于前面提到的list.h,属于linux内核部分,这里用到的也是其改进版本,具体定义在lib/skew_heap.h中。

skew_heap_entry_t *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) __attribute__((always_inline));

第一和第二参数都是堆中的元素,第三是比较法则,因为斜堆数据结构是自组织的,可以对自身进行排序,因此插入进去之后就需要排序了。

其他处理,和RR调度算法相同,取得处于队首的进程进行调度,并为其分配时间片。

4.将进程从就绪队列中移除(stride_dequeue函数)
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_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;
}

完成将一个进程从队列中移除的功能,这里使用了优先队列。最后运行队列数目减一。

5.选择进程调度(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则设置为最大的步长。

6.时间片部分(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; 
    }  
}

主要工作是检测当前进程是否已用完分配的时间片。如果时间片用完,应该正确设置进程结构的相关标记来引起进程切换。和RR没有任何区别。

完成函数后,将原来的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的结果如下:

在这里插入图片描述

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

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

下面开始实验:

CFS算法是想要让每个进程的运行时间尽可能相同,那么记录每个进程已经运行的时间即可。在进程控制块结构中增加一个属性表示运行时间。每次需要调度的时候,选择已经运行时间最少的进程调入CPU执行。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;
    }
}

扩展练习 Challenge 2 :在ucore上实现尽可能多的各种基本调度算法(FIFO, SJF,…),并设计各种测试用例,能够定量地分析出各种调度算法在各种指标上的差异,说明调度算法的适用范围。

参考答案分析

接下对参考答案的实现与本实验中的实现进行比较分析:

  • 关于更新先前LAB的代码的实现部分,发现参考答案在处理时间中断的时候没有调用sched_class_proc_tick函数,这显然是一个错误;
  • 接下来比较参考答案的stride算法的实现,发现参考答案同时实现了使用链表和使用斜堆完成就绪队列的stride算法,而在本实验的实现中,仅仅采用了时间效率更高的斜堆实现;在具体使用斜堆实现stride算法部分,几乎没有太大的区别,但是参考答案选取的BigStride恰好是能够选取的最大值2^31-1,而本实现中直接使用了自己的学号作为BigStride的取值;

实验中涉及的知识点列举

  • 在本次实验中涉及到的知识点如下:
    • 面向对象编程思想;
    • Round-Robin调度算法;
    • Stride调度算法;
    • 多级反馈队列调度算法;
    • 调度算法框架的实现;
  • 对应的OS中的知识点如下:
    • ucore中对调度算法的具体封装方式;
    • ucore中具体的三种调度算法的实现;
  • 它们之间的关系为:
    • 前者的抽象的算法之后为后者具体的功能是实现提供了基础;
    • 前者中的面向对象等知识有利于简化后者的具体实现过程;

实验中未涉及的知识点列举

在本次实验中未涉及到的知识点列举如下:

  • 操作系统的启动过程;
  • 具体各种不同的调度算法之间的理论分析对比以及实验分析对比;
    lenge 2 :在ucore上实现尽可能多的各种基本调度算法(FIFO, SJF,…),并设计各种测试用例,能够定量地分析出各种调度算法在各种指标上的差异,说明调度算法的适用范围。
原网站

版权声明
本文为[湖大金胜宇]所创,转载请带上原文链接,感谢
https://blog.csdn.net/sfadjlha/article/details/125089388