ucore_lab6实验报告

实验目的

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

熟悉ucore的系统调度器框架,以及基于此框架的Round-Robin(RR) 调度算法。然后参考RR调度算法的实现,完成Stride Scheduling调度算法。

进程状态

进程的正常生命周期如下:

  • 进程首先在 cpu 初始化或者 sys_fork 的时候被创建,当为该进程分配了一个进程控制块之后,该进程进入 uninit态(在proc.c 中 alloc_proc)。
  • 当进程完全完成初始化之后,该进程转为runnable态。
  • 当到达调度点时,由调度器 sched_class 根据运行队列rq的内容来判断一个进程是否应该被运行,即把处于runnable态的进程转换成running状态,从而占用CPU执行。
  • running态的进程通过wait等系统调用被阻塞,进入sleeping态。
  • sleeping态的进程被wakeup变成runnable态的进程。
  • running态的进程主动 exit 变成zombie态,然后由其父进程完成对其资源的最后释放,子进程的进程控制块成为unused。
  • 所有从runnable态变成其他状态的进程都要出运行队列,反之,被放入某个运行队列中。
内核抢占点

调度本质上体现了对CPU资源的抢占。对于用户进程而言,由于有中断的产生,可以随时打断用户进程的执行,转到操作系统内部,从而给了操作系统以调度控制权,让操作系统可以根据具体情况(比如用户进程时间片已经用完了)选择其他用户进程执行。这体现了用户进程的可抢占性(preemptive)。但如果把ucore操作系统也看成是一个特殊的内核进程或多个内核线程的集合,那ucore是否也是可抢占的呢?其实ucore内核执行是不可抢占的(non-preemptive),即在执行“任意”内核代码时,CPU控制权可被强制剥夺。这里需要注意,不是在所有情况下ucore内核执行都是不可抢占的,有以下几种“固定”情况是例外:

  1. 进行同步互斥操作,比如争抢一个信号量、锁(lab7中会详细分析);
  2. 进行磁盘读写等耗时的异步操作,由于等待完成的耗时太长,ucore会调用shcedule让其他就绪进程执行。

这几种情况其实都是由于当前进程所需的某个资源(也可称为事件)无法得到满足,无法继续执行下去,从而不得不主动放弃对CPU的控制权。如果参照用户进程任何位置都可被内核打断并放弃CPU控制权的情况,这些在内核中放弃CPU控制权的执行地点是“固定”而不是“任意”的,不能体现内核任意位置都可抢占性的特点。我们搜寻一下实验五的代码,可发现在如下几处地方调用了shedule函数:

表一:调用进程调度函数schedule的位置和原因

编号位置原因
1proc.c::do_exit用户线程执行结束,主动放弃CPU控制权。
2proc.c::do_wait用户线程等待子进程结束,主动放弃CPU控制权。
3proc.c::init_main1. initproc内核线程等待所有用户进程结束,如果没有结束,就主动放弃CPU控制权; 2. initproc内核线程在所有用户进程结束后,让kswapd内核线程执行10次,用于回收空闲内存资源
4proc.c::cpu_idleidleproc内核线程的工作就是等待有处于就绪态的进程或线程,如果有就调用schedule函数
5sync.h::lock在获取锁的过程中,如果无法得到锁,则主动放弃CPU控制权
6trap.c::trap如果在当前进程在用户态被打断去,且当前进程控制块的成员变量need_resched设置为1,则当前线程会放弃CPU控制权

仔细分析上述位置,第1、2、5处的执行位置体现了由于获取某种资源一时等不到满足、进程要退出、进程要睡眠等原因而不得不主动放弃CPU。第3、4处的执行位置比较特殊,initproc内核线程等待用户进程结束而执行schedule函数;idle内核线程在没有进程处于就绪态时才执行,一旦有了就绪态的进程,它将执行schedule函数完成进程调度。这里只有第6处的位置比较特殊:

if (!in_kernel) {
    ……

    if (current->need_resched) {
        schedule();
    }
}

这里表明了只有当进程在用户态执行到“任意”某处用户代码位置时发生了中断,且当前进程控制块成员变量need_resched为1(表示需要调度了)时,才会执行shedule函数。这实际上体现了对用户进程的可抢占性。如果没有第一行的if语句,那么就可以体现对内核代码的可抢占性。但如果要把这一行if语句去掉,我们就不得不实现对ucore中的所有全局变量的互斥访问操作,以防止所谓的racecondition现象,这样ucore的实现复杂度会增加不少。

练习0:填写已有实验

直接对比合入代码,需要做如下修改:

kern/process/proc.c->alloc_proc

        // LAB6
        proc->rq = NULL;
        // 初始化run_link
        list_init(&(proc->run_link));
		// memset(&proc->run_link, 0, sizeof(list_entry_t));
        proc->time_slice = 0;
        // 初始化lab6_run_pool
        proc->lab6_run_pool.left = proc->lab6_run_pool.right = proc->lab6_run_pool.parent = NULL;
		// memset(&proc->lab6_run_pool, 0, sizeof(skew_heap_entry_t));
		proc->lab6_stride = 0;
		proc->lab6_priority = 1;

kern/trap/trap.c->trap_dispatch

    case IRQ_OFFSET + IRQ_TIMER:
        ticks ++;
        assert(current != NULL);
        sched_class_proc_tick(current);
        
        break;
练习1: 使用 Round Robin 调度算法(不需要编码)

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

请在实验报告中完成:

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

    sched_class代码如下:

    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->RR_init

    即初始化

    enqueue->RR_enqueue

    某个进程加入队列中

    dequeue->RR_dequeue

    某个进程出队

    pick_next->RR_pick_next

    在就绪队列中选择一个最适合运行的进程(选择进程但不将从队列中移除,通过dequeue出队

    proc_tick->RR_proc_tick

    在时钏中断处理例程中被调用,以减小当前运行进程的剩余时间片。若时间片耗尽,则设置当前进程的need_resched为1。

  • 请在实验报告中简要说明如何设计实现”多级反馈队列调度算法“,给出概要设计,鼓励给出详细设计

    多级反馈队列调度算法(Multilevel Feedback Queue Scheduling)

    参考:多级反馈队列调度算法

    Xv6-MLFQ-Scheduler

    MLFQ-scheduler-OS

    假设系统中有3个反馈队列Q1,Q2,Q3,时间片分别为2,4,8

    设有3个作业J1,J2,J3分别在时间 0 ,1,3时刻到达。而它们所需要的CPU时间分别是3,2,1个时间片。

    **1、**时刻0 J1到达。于是进入到队列1 , 运行1个时间片 , 时间片还未到,此时J2到达。

    **2、**时刻1 J2到达。 由于同一队列采用先来先服务,于是J2等待。 J1在运行了1个时间片后,已经完成了在Q1中的2个时间片的限制,于是J1置于Q2等待被调度。当前处理机分配给J2。

    **3、**时刻2 J1进入Q2等待调度,J2获得CPU开始运行。

    **4、**时刻3 J3到达,由于同一队列采用先来先服务,故J3在Q1等待调度,J1也在Q2等待调度。

    **5、**时刻4 J2处理完成,由于J3,J1都在等待调度,但是J3所在的队列比J1所在的队列的优先级要高,于是J3被调度,J1继续在Q2等待。

    **6、**时刻5 J3经过1个时间片,完成。

    **7、**时刻6 由于Q1已经空闲,于是开始调度Q2中的作业,则J1得到处理器开始运行。 J1再经过一个时间片,完成了任务。于是整个调度过程结束。

练习2: 实现 Stride Scheduling 调度算法(需要编码)

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

后面的实验文档部分给出了Stride调度算法的大体描述。这里给出Stride调度算法的一些相关的资料(目前网上中文的资料比较欠缺)。

上面给的资料打不开,可以参考论文:

StrideScheduling.pdf

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

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

参考ucore的文档Stride Scheduling基本思路

而该算法的基本思想如下:

  1. 为每个runnable的进程设置一个当前状态stride,表示该进程当前的调度权。另外定义其对应的pass值,表示对应进程在调度后,stride 需要进行的累加值。
  2. 每次需要调度时,从当前 runnable 态的进程中选择 stride最小的进程调度。
  3. 对于获得调度的进程P,将对应的stride加上其对应的步长pass(只与进程的优先权有关系)。
  4. 在一段固定的时间之后,回到 2.步骤,重新调度当前stride最小的进程。

BigStride应该设置为无符号整型的最大值也就是01111111111111111111111111111111,即:

#define BIG_STRIDE 0x7fffffff

具体代码如下:

stride_init

static void
stride_init(struct run_queue *rq) {
     list_init(&rq->run_list);
     rq->lab6_run_pool = NULL;
     rq->proc_num = 0;
}

stride_enqueue

static void
stride_enqueue(struct run_queue *rq, struct proc_struct *proc) {
     rq->lab6_run_pool = skew_heap_insert(rq->lab6_run_pool, &(proc->lab6_run_pool), proc_stride_comp_f);
     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++;
}

stride_dequeue

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

stride_pick_next

static struct proc_struct *
stride_pick_next(struct run_queue *rq) {
     skew_heap_entry_t* she = rq->lab6_run_pool;
     if (she != NULL) {
          struct proc_struct *proc = le2proc(she, lab6_run_pool);
          proc->lab6_stride += BIG_STRIDE / proc->lab6_priority;
          return proc;
     }
     return NULL;
}

stride_proc_tick

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

执行结果如下所示:

image-20220919145403678

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

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

参考链接:

后面再实现

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

后面再实现

参考文章:

Lab 6 · ucore_os_docs (gitbooks.io)

uCore实验 - Lab6

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值