操作系统实验:Lab6 调度器

清华大学操作系统Lab6实验报告
课程主页:http://os.cs.tsinghua.edu.cn/oscourse/OS2018spring
实验指导书:https://chyyuu.gitbooks.io/ucore_os_docs/content/
github:https://github.com/chyyuu/ucore_os_lab

实验目的

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

实验内容

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

练习1:加载应用程序并执行

为了完成Lab6的练习1,首先需要对之前的代码做一些修改。
发生时钟中断时,不需要直接在trap_dispatch中进行调度的设置,而是间接使用sched_class中的函数指针proc_tick来完成:

    case IRQ_OFFSET + IRQ_TIMER:
    sched_class_proc_tick(current);
        break;

此外,还需要在对proc_struct中针对lab6添加的接口初始化。在proc.c的alloc_proc函数中:

        proc -> rq = NULL;
        list_init(&(proc -> run_link));
        proc -> time_slice = 0;

完成之后,运行make grade,结果如下。除了priority测试外,其预测是均能通过。

完成练习1的结果

请理解并分析sched_class中各个函数指针的用法,并接合Round Robin 调度算法描述ucore的调度执行过程

通过阅读sched.c中的代码,可以发现实现调度的主要函数schedule实际上调用的是sched_class_enqueuesched_class_dequeuesched_class_pick_nextsched_class_proc_ticksched_init这五个函数,在这五个函数内部,通过sched_class类的实例中的函数指针来调用具体的调度算法。

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

可以看到,其中有一个定义了调度算法名称的字符串和五个函数接口:

  • init:初始化运行队列
  • enqueue:将进程p加入到运行队列rq中
  • dequeue:将进程p从运行队列rq中删除
  • pick_next:返回运行队列中下一个可执行的进程
  • proc_tick:time tick的处理函数

如果要实现自己的调度算法,需要按照如上的功能完成这五个函数,并构造sched_class类的实例,将这五个函数传给实例中的函数指针,随后通过这个实例即可使用相应的调度算法。

结合Round Robin算法,具体的调度过程如下:

  • 当调用schedule函数时发生调度。首先清除当前进程需要调度的标记,把当前进程放进运行队列中去。
        current->need_resched = 0;
        if (current->state == PROC_RUNNABLE) {
            sched_class_enqueue(current);
        }

sched_class_enqueue函数调用Round Robin算法的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 ++;
}
  • 随后,通过Round Robin调度算法选择下一个要运行的进程。具体地,sched_class_pick_next函数调用Round Robin算法的RR_pick_next接口,
        if ((next = sched_class_pick_next()) != NULL) {
            sched_class_dequeue(next);
        }

Round Robin算法的RR_pick_next将返回运行队列中的第一个进程交给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;
}
  • 如果可以选出下一个要运行的进程,则将这个进程从运行队列中删除。否则,运行idleproc进程,即返回查询是否有新的进程需要运行。sched_class_dequeue函数会调用Round Robin算法的RR_dequeue函数。
        if ((next = sched_class_pick_next()) != NULL) {
            sched_class_dequeue(next);
        }
        if (next == NULL) {
            next = idleproc;
        }
  • 随后通过proc_run函数切换到新进程并进入新进程执行。proc_run的具体切换过程在Lab5中已经完成。
        next->runs ++;
        if (next != current) {
            proc_run(next);
        }

此外,在每一次时钟tick的时候,都会调用Round Robin的RR_proc_tick函数,将当前进程的所占用的时间片剩余时间减一,当时间片耗尽时,设置为需要调度并等待调度。

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;
    }
}
请在实验报告中简要说明如何设计实现”多级反馈队列调度算法“,给出概要设计,鼓励给出详细设计

多级反馈队列算法可以实现在不同的队列间使用不同的调度算法。
假设进程一共有4个调度优先级,分别为0、1、2、3,其中0位最高优先级,3位最低优先级。为了支持4个不同的优先级,在运行队列中开4个队列,分别命名为rq -> run_list[0..3]。除此之外,在proc_struct中加入priority成员表示该进程现在所处的优先级,初始化为0。

  • MLFQ_enqueue(struct run_queue *rq, struct proc_struct *proc):判断proc进程的时间片proc -> time_slice是否为0,如果为0,则proc -> priority += 1,否则不变。根据proc加入到对应优先级的列表中去。时间片的长度也和优先级有关,低优先级的时间片长度设置为高优先级的两倍。
static void
MLFQ_enqueue(struct run_queue *rq, struct proc_struct *proc) {
    assert(list_empty(&(proc->run_link)));
    if (proc -> time_slice == 0 && proc -> priority != 3) {
        ++(proc -> priority);
    }
    list_add_before(&(rq->run_list[proc ->priority]), &(proc->run_link));
    proc->time_slice = (rq->max_time_slice << proc -> priority);
    proc->rq = rq;
    rq->proc_num ++;
}
  • MLFQ_dequeue(struct run_queue *rq, struct proc_struct *proc):将proc进程从相应的优先级运行队列中删除。
static void
MLFQ_enqueue(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 --;
}
  • MLFQ_pick_next(struct run_queue *rq):为了避免优先级较低的进程出现饥饿现象,对每个优先级设置一定的选中概率,高优先级是低优先级选中概率的两倍,然后选出一个优先级,找到这个优先级中的第一个进程返回。
static struct proc_struct *
MLFQ_pick_next(struct run_queue *rq) {
    int p = rand() % (8 + 4 + 2 + 1);
    int priority;
    if (p >= 0 && p < 8) {
        priority = 0;
    } else if (p >= 8 && p < 12) {
        priority = 1;
    } else if (p >= 12 && p < 14) {
        priority = 2;
    } else priority = 3;
    list_entry_t *le = list_next(&(rq->run_list[priority]));
    if (le != &(rq->run_list[priority])) {
        return le2proc(le, run_link);
    } else {
        for (int i = 0; i < 3; ++i) {
            le = list_next(&(rq->run_list[i]));
            if (le != &(rq -> run_list[i])) return le2proc(le, run_link);
        }
    }
    return NULL;
}
  • MLFQ_proc_tick(struct run_queue *rq, struct proc_struct *proc):和RR算法相似。

练习2: 实现 Stride Scheduling 调度算法

Stride算法的原理是对于每一个进程,有一个stride值和一个pass值。每次进行调度时,选择stride最小的进程运行,并将这个进程的stride加上pass。pass越小那么被调度的次数就会越多。在实验中,pass依托优先级实现。优先级越大,pass即用一个大常数除以优先级得到的值就越小,也就意味着被调度的次数越多。具体实现如下

  • 首先,需要设置一个大整数,将来的所有pass值都由这个大整数除以进程的优先级得到:
#define BIG_STRIDE  0x7FFFFFFF
  • 初始化时,需要将列表、斜堆清空,并置运行列表中的进程数为0。
static void
stride_init(struct run_queue *rq) {
// (1) init the ready process list: rq->run_list
    list_init(&(rq -> run_list));
// (2) init the run pool: rq->lab6_run_pool
    rq -> lab6_run_pool = NULL;
// (3) set number of process: rq->proc_num to 0       
    rq -> proc_num = 0;
}
  • 将进程加入运行队列时,插入到斜堆中,并将运行队列的进程计数加一,同时需要在进程的数据结构中关联运行队列。
static void
stride_enqueue(struct run_queue *rq, struct proc_struct *proc) {
#if USE_SKEW_HEAP
// (1) insert the proc into rq correctly
    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
// (2) recalculate proc->time_slice
    if (proc->time_slice == 0 || proc->time_slice > rq->max_time_slice) {
        proc->time_slice = rq->max_time_slice;
    }
// (3) set proc->rq pointer to rq
    proc -> rq = rq;
// (4) increase rq->proc_num
    rq -> proc_num ++;
}
  • 将进程从运行队列移走时,需要将进程从斜堆中删除,并将运行队列的进程计数减一。
static void
stride_dequeue(struct run_queue *rq, struct proc_struct *proc) {
#if USE_SKEW_HEAP
// (1) remove the proc from rq correctly
    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
// (2) decrease rq -> proc_num by 1
    rq -> proc_num --;
}
  • 选择接下来调度的进程时,如果使用斜堆只需要取出堆顶,随后要更新进程的stride值。
static struct proc_struct *
stride_pick_next(struct run_queue *rq) {
    struct proc_struct *proc;
#if USE_SKEW_HEAP
    if (rq -> lab6_run_pool == NULL) {
        return NULL;
    }
//  (1) get a  proc_struct pointer p  with the minimum value of stride
    proc = 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;
    }
    proc = le2proc(le, run_link);
    while (le != &(rq -> run_list)) {
        struct proc_struct *temp = le2proc(le, run_link);
        if (proc_stride_comp_f(temp, proc) < 0) {
            proc = temp;
        }
        le = list_next(le);
    }
#endif
//  (2) update p;s stride value: p->lab6_stride
    if (proc -> lab6_priority == 0) {
        proc -> lab6_stride += BIG_STRIDE;
    } else {
        proc -> lab6_stride += BIG_STRIDE / proc -> lab6_priority;
    }
    return proc;
}
  • 处理时钟和RR算法一样,如果time slice大于0,则将值减一。否则以为着时间片用完,需要调度。
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;
    }
}

覆盖的知识点

  • 进程调度的过程
  • RR算法和Stride算法

与参考答案的区别

  • 练习1:自己完成。
  • 练习2:自己完成,但是没有加使用列表的情况,后补上。

总结

感觉这个实验的难度并不大,但是在完成实验二的时候遇到了玄学问题,盯了一个小时代码之后就莫名其妙的出正确结果了,目前不知道原因。

推荐阅读更多精彩内容