user avatar
submitfix
Rinffles authored
0137237e
Name Last commit Last update
SchedulingLab
.DS_Store
README.md

调度实验设计与改进

参赛队伍:OSGO

队伍成员:叶云汉(复旦大学)

指导老师:赵进

MIT 6.S081的学习过程中,我们会完成一系列针对xv6操作系统的实验,这些实验涵盖了系统调用、内存管理、文件系统等方面的知识。然而,6.S081的基础实验中相对缺失了对**进程调度(Scheduling)**机制的深入实践和拓展。调度是操作系统的核心内容之一,通过合理的调度策略,系统可以在多个进程之间分配CPU时间,提高整体性能和用户体验。

本实验在赵老师课程的Lab3 scheduling基础上新增和完善了调度相关的实验内容,包括从最基本的Round-Robin (RR)调度入手,加入了根据进程优先级决定调度顺序的优先级调度 (PR),并在此基础上进一步拓展,实现了更复杂和实际系统中更常见的多级反馈队列调度 (MLFQ)。通过这些扩展,学生可以更全面地理解和对比不同调度算法的特点、适用场景以及对系统性能的影响。

1 实验目标

  1. 巩固基础:理解并实现xv6内核中基本的Round-Robin调度机制。
  2. 系统调用扩展:通过wait_sched系统调用获取进程在不同状态下的时间统计,为调度器性能分析提供数据支撑。
  3. 优先级调度拓展 (PR):在默认RR调度的基础上,为进程引入优先级概念,并实现静态优先级调度,使学生了解调度策略如何根据进程重要性动态分配CPU。
  4. 多级反馈队列调度 (MLFQ):在更高层次上引入MLFQ调度策略,通过多级队列与时间片动态调整机制,让学生了解更成熟、实际应用更广的调度算法。
  5. 调度策略对比与分析:通过对RR、PR、MLFQ的运行结果和统计数据进行对比分析,总结不同调度策略的特点、优缺点及适用场景。

2 背景知识

2.1 基本调度概念

操作系统在多进程环境中需要高效地管理和分配 CPU 时间给多个运行中的进程,这一过程称为进程调度。良好的调度策略可以实现 CPU 的高效利用,提升整体系统性能和用户体验。

  • 进程调度:当系统中有多个进程并发执行时,操作系统需要决定由哪个进程获得 CPU 使用权,以及在何时获得。调度器会综合考虑进程的状态、优先级以及时间片使用情况,对可运行的进程进行合理分配。好的调度方案可以降低进程的等待时间和响应时间,提高系统吞吐量。

  • 进程状态:在 xv6 系统中,每个进程在其生命周期中会经历多种状态切换。

    • UNUSED:该进程槽位未被使用,无实际进程存在。
    • SLEEPING:进程正在等待某个事件(如 I/O)完成,此时不会获得 CPU。
    • RUNNABLE:进程已准备好运行,只等待调度器将其分配到 CPU 上执行。
    • RUNNING:进程当前正在 CPU 上执行指令。
    • ZOMBIE:进程已执行结束,但其父进程尚未回收(通过 wait() 系统调用)其退出状态。

在调度过程中,调度器通常重点关注处于 RUNNABLE 状态的进程队列。当一个正在运行 (RUNNING) 的进程用完其分配的时间片,或主动通过 yield() 释放 CPU 时,它将返回 RUNNABLE 状态并重新进入就绪队列。调度器则从该队列中选取下一个需要执行的进程,不断重复这一过程,从而实现 CPU 在多个进程之间的合理切换。

2.2 Round-Robin (RR) 调度

在 Round-Robin 调度策略中,每个 RUNNABLE 状态的进程都会分配到相同长度的时间片,调度器依次循环选择队列中的进程运行。一旦时间片用完,进程被切换下 CPU,并将 CPU 让给下一个进程。RR 调度简单易实现,可确保所有进程获得均等的 CPU 时间,避免单个进程长期占用 CPU 的现象。然而,RR 并不根据进程类型、执行特征或重要性对其进行区分,这意味着所有进程都会被同等对待。

2.3 优先级调度 (PR)

在优先级调度中,每个进程被分配一个优先级(数值越小代表优先级越高)。调度器在选择下一个进程执行时,会优先选取优先级最高的 RUNNABLE 进程。这确保了重要或紧急任务可以更早获得 CPU 时间片,从而减少关键任务的等待时间。与 RR 相比,PR 调度能更好地满足特定需求的进程(如实时任务)的响应时间要求。但同时,由于低优先级进程可能长时间得不到调度,PR 调度可能导致这些进程出现“饥饿”现象,即长期无法获得 CPU 运行机会。

2.4 多级反馈队列调度 (MLFQ)

多级反馈队列(Multi-Level Feedback Queue,MLFQ)是在实际系统中广泛应用的一种调度策略。它通过多层次的就绪队列和动态的优先级调整机制,为进程分配更灵活的运行机会。具体表现为:

  • 多级队列结构:系统维护多个优先级队列,最高优先级队列中进程的时间片较短,但可以更频繁地获得 CPU。当进程在高优先级队列中多次用完时间片仍未完成任务,它会被降低到下一级队列以获得更长的时间片,但调度频率也会降低。

  • 动态调整机制:当进程表现为 I/O 密集型(频繁等待 I/O 操作)时,它往往在每次 I/O 完成后被唤醒并返回高优先级队列,从而快速获得 CPU 时间。CPU 密集型进程由于持续占用 CPU,会逐渐降到较低优先级队列中执行。这种自适应的等级变换使 MLFQ 能够同时兼顾系统的响应性和吞吐量。

通过这种基于多级反馈的调度策略,不同类型的进程(I/O 密集型、CPU 密集型、混合型)能获得更优化的调度处理。MLFQ 不仅能像 RR 一样保证基本的公平性,还能通过动量调节实现类似优先级调度的效果,同时通过反馈机制有效缓解低优先级进程的饥饿问题。它在实际操作系统中应用广泛,更贴近生产系统的复杂调度情形。

3 实验内容

3.1 任务点一:实现进程状态时间统计(wait_sched 系统调用)

3.1.1 任务目标

在本任务中需要实现一个名为 wait_sched 的系统调用,使父进程在回收子进程时能够获取子进程在不同状态下运行的时间数据。通过 wait_sched,当子进程结束 (ZOMBIE 状态) 时,父进程可以得到该子进程处于 RUNNABLERUNNINGSLEEPING 状态下累计的 ticks 数。ticks 是 xv6 内核的时钟中断周期计数,用来相对反映进程的时间消耗。

完成本任务后,我们可在后续实验对调度策略(如 RR、PR、MLFQ)的影响进行量化分析,以便对比进程在不同调度模式下的表现差异。

3.1.2 代码实现指导

  1. 新增进程时间统计字段
    proc.hstruct proc 中新增如下字段:

    struct proc {
      ...
      uint64 created_time;   // 进程创建时间
      uint64 finish_time;    // 进程结束时间
      uint64 running_time;   // 进程处于RUNNING状态的总时间
      uint64 runable_time;   // 进程处于RUNNABLE状态的总时间
      uint64 sleep_time;     // 进程处于SLEEPING状态的总时间
      uint64 start;          // 当前状态开始时间戳(ticks)
      uint64 end;            // 当前状态结束时间戳(ticks)
      ...
    };
  2. 实现 on_state_change 函数
    proc.c 中完善 on_state_change(int cur_state, int nxt_state, struct proc *p)

    • 当进程状态从 cur_state 切换至 nxt_state 时,通过对比 p->start 和当前 ticks 计算该状态停留时间。
    • 根据 cur_stateRUNNINGRUNNABLESLEEPING 中的哪一种,将停留时间加到相应的计时字段中 (running_time / runable_time / sleep_time)。
    • 更新 p->start = ticks 为下一个状态的起始时间。

    在代码标记的 TODO 处(如 allocprocuserinitforkexitscheduleryieldsleepwakeupkill)调用 on_state_change,以确保进程从诞生到结束的状态变化都被统计。

  3. 实现 wait_sched 系统调用
    wait_sched(int *runable_time, int *running_time, int *sleep_time) 的逻辑与 wait() 类似,但在回收子进程的同时,通过 copyout 将子进程的时间统计数据返回给父进程。

    实现步骤:

    • defs.h 中声明 int wait_sched(int*, int*, int*);
    • syscall.h 中定义 SYS_wait_sched 的编号
    • syscall.c 中加入 wait_sched 系统调用条目
    • sysproc.c 中实现 sys_wait_sched() 函数,内部调用 wait_sched() 内核函数
    • 在用户态:
      • usys.pl 中添加 entry("wait_sched")
      • user.h 中声明 int wait_sched(int*, int*, int*);
    • 修改 Makefilestat 程序编译进用户态可执行文件,用于测试。
  4. 测试程序说明
    实验提供了 user/stat.c 用户程序用于测试 wait_schedstat nfork 出 n 个子进程执行计算密集任务(big_calculate_task),然后父进程通过 wait_sched 回收子进程时获取每个子进程的时间统计数据,以此直观展示 RR 调度下进程的运行特征。

3.1.3 测试方法

  1. 基本功能测试
    编译并运行 xV6:

    make qemu

    在xv6命令行中执行:

    stat 5

    stat 5 将创建 5 个子进程,每个子进程执行耗时计算任务,结束后父进程通过 wait_sched 获得每个子进程在 RUNNABLERUNNINGSLEEPING 状态下的统计数据。

  2. 示例输出
    若实现正确,你可能会看到类似如下的输出结果(示例):

    $ stat 5
    PID: 4 | Runnable Time: 36 ticks | Running Time: 49 ticks | Sleep Time: 12 ticks
    PID: 7 | Runnable Time: 39 ticks | Running Time: 61 ticks | Sleep Time: 12 ticks
    PID: 5 | Runnable Time: 37 ticks | Running Time: 63 ticks | Sleep Time: 12 ticks
    PID: 6 | Runnable Time: 44 ticks | Running Time: 60 ticks | Sleep Time: 12 ticks
    PID: 8 | Runnable Time: 44 ticks | Running Time: 60 ticks | Sleep Time: 12 ticks
    Average Turnaround Time: 110 ticks

    输出说明:

    • PID: 子进程的进程 ID
    • Runnable Time: 子进程在可运行状态下等待 CPU 的总时间
    • Running Time: 子进程真正占用 CPU 执行的总时间
    • Sleep Time: 子进程在等待 I/O 或其他事件的睡眠时间
    • 最后输出的平均轮转时间(Turnaround Time)是统计所有子进程完成所花费的平均总时间。
  3. 分析和对比
    在完成这一任务后,将为后续对比不同调度策略(RR、PR、MLFQ)的影响奠定基础。通过观察进程在各个状态下花费的时间,结合平均周转时间指标,可帮助理解当前默认调度策略(RR)如何影响进程的执行情况。

    后续实验中修改调度策略后(如切换到优先级调度、MLFQ调度),可再次运行 stat n 对比结果,从而直观观察调度策略改变所带来的性能差异与影响。

3.2 任务点二:优先级调度(PR)

3.2.1 任务目标

在本任务中,将在已有的 RR 调度机制基础上,为 xV6 引入基于静态优先级的调度模式(PR)。通过修改调度器(scheduler()函数)的实现,使操作系统在选择下一个运行的进程时优先考虑进程优先级,从而实现高级别(数值更低)的进程先获得 CPU 的调度策略。

此外,还需实现 set_priority 系统调用,允许用户在运行时动态修改进程的优先级值。这种可由用户控制的优先级调度模式可用于加速关键进程的执行、对系统资源进行更灵活和高效的分配。

3.2.2 代码实现指导

  1. 启用优先级调度宏定义
    proc.h 中切换调度策略宏定义,将 #define PR 打开,并注释掉 #define RR(若原本存在)。

    // #define RR
    #define PR

    这样在编译时,调度器将使用优先级调度策略的分支逻辑。

  2. 进程优先级字段与默认值
    proc.hstruct proc 中已经添加了 int priority; 字段,优先级范围为 [0,3],其中0为最高优先级,3为最低优先级。进程默认优先级可在进程创建时设为2(中等优先级)。

    struct proc {
      ...
      int priority;  // 进程的静态优先级 [0..3]
    };

    allocproc()fork() 新建进程时,为进程设置初始优先级为2:

    p->priority = 2; // Default priority
  3. 修改scheduler()实现优先级调度
    scheduler() 函数中,当定义了 PR 时,需要遍历 proc 数组,查找状态为 RUNNABLE 且优先级最高(数值最小)的进程。

    实现逻辑示意:

    #elif defined(PR)
    struct proc *max_p = 0;
    int max_priority = 4; // 高于最大优先级范围的初始值
    
    // 遍历所有进程,寻找优先级最高的RUNNABLE进程
    for(p = proc; p < &proc[NPROC]; p++){
      acquire(&p->lock);
      if(p->state == RUNNABLE && p->priority < max_priority){
        max_priority = p->priority;
        max_p = p;
      }
      release(&p->lock);
    }
    
    // 如果找到最高优先级的RUNNABLE进程,则进行调度
    if (max_p != 0){
      acquire(&max_p->lock);
      // 进程状态更新、上下文切换与运行
      // 参考RR调度的上下文切换逻辑
      on_state_change(RUNNABLE, RUNNING, max_p);
      max_p->state = RUNNING;
      c->proc = max_p;
      swtch(&c->context, &max_p->context);
      c->proc = 0; // 进程运行结束后返回
      release(&max_p->lock);
    }
    #endif

    需要特别注意锁的获取与释放顺序,以及对 proc 数组的遍历过程中,保证数据结构访问的原子性和无死锁。

  4. 实现 set_priority 系统调用
    proc.c 中已经有 int set_priority(int priority, int pid) 函数接口定义,实现逻辑如下:

    • 检查 priority 范围是否在 [0,3] 之间。
    • 遍历所有进程,找到 pid 对应的进程,若存在则更新其 priority 字段。
    • 返回0表示成功,-1表示失败(找不到进程或优先级不合规)。

    在kernel侧:

    • defs.h 中声明 int set_priority(int priority, int pid);
    • syscall.h 中分配一个 SYS_set_priority 系统调用编号
    • syscall.c 中注册 set_priority 的系统调用处理函数
    • sysproc.c 中实现 sys_set_priority(),通过 argint() 获取参数并调用内核函数 set_priority()

    在用户侧:

    • usys.pl 中添加 entry("set_priority")
    • user.h 中声明 int set_priority(int priority, int pid);

    在运行时,用户可以调用 set_priority(new_priority, pid) 改变指定进程的优先级。

3.2.3 测试方法

  1. 基本功能验证
    切换至PR模式(在 proc.h 中定义 #define PR),然后 make qemu 启动xv6。
    运行一些简单命令或用户程序,确保系统能够正常运作和调度进程。

  2. 使用 priostat 程序测试
    实验提供 priostat n 用户级程序(位于 user/priostat.c),与 stat 程序类似。
    执行:

    $ priostat 5

    该程序会:

    • fork()n 个子进程并为它们分别调用 set_priority 设置不同的优先级。
    • 启用PR模式的调度后,高优先级的子进程将更快获得CPU。
    • 父进程通过 wait_sched 获取每个子进程的 RUNNABLERUNNINGSLEEPING 时间,并最终计算平均周转时间。
  3. 示例输出与分析
    如果实现正确,priostat 5 的输出可能类似于:

    $ priostat 5
    Set priority 0 to PID 4
    Set priority 1 to PID 5
    Set priority 2 to PID 6
    Set priority 3 to PID 7
    Set priority 0 to PID 8
    PID: 4 | Runnable Time: 9 ticks | Running Time: 48 ticks | Sleep Time: 12 ticks
    PID: 8 | Runnable Time: 7 ticks | Running Time: 52 ticks | Sleep Time: 12 ticks
    PID: 5 | Runnable Time: 25 ticks | Running Time: 57 ticks | Sleep Time: 12 ticks
    PID: 6 | Runnable Time: 60 ticks | Running Time: 59 ticks | Sleep Time: 12 ticks
    PID: 7 | Runnable Time: 95 ticks | Running Time: 51 ticks | Sleep Time: 12 ticks
    Average Turnaround Time: 104 ticks

    从输出中可以观察到:

    • 高优先级(0)的进程(如PID=4、PID=8)获得更多的CPU运行时间,RUNNABLE等待时间较短。
    • 低优先级(3)的进程(如PID=7)则明显等待更久的可运行时间(RUNNABLE Time较长),反映出优先级调度策略对不同优先级进程调度行为的显著影响。
  4. 扩展测试与思考
    可尝试使用 set_priority 动态改变正在运行进程的优先级,观察其RUNNABLE与RUNNING时间分布的变化情况。
    与RR策略结果对比:在RR中,所有进程不分优先级均衡获得CPU;而在PR中,关键任务可优先获得CPU时间,提高响应速度,但代价是低优先级进程可能遭遇长期延迟甚至饥饿。这有助于学生更深入理解调度策略设计中的权衡与取舍。

通过本任务的实现和测试,学生可以对优先级调度策略有更直观的理解,也为后续引入更复杂的多级反馈队列(MLFQ)调度策略打下基础。

3.3 任务点三:多级反馈队列调度(MLFQ)

3.3.1 任务目标

在前两个任务中,我们实现了RR(时间片轮转)和PR(静态优先级)调度策略。但在实际操作系统中,单纯的RR或PR难以兼顾CPU密集型与I/O密集型进程的不同需求。多级反馈队列(Multi-Level Feedback Queue, MLFQ)调度通过维护多个优先级队列、为不同队列分配不同长度的时间片,并动态调整进程所在的队列等级,从而在公平性与灵活性之间取得更好的平衡。

完成本任务后,你将:

  • 理解MLFQ调度的设计思想:高优先级队列给进程短时间片、更高调度频率,适合短任务和I/O密集进程;低优先级队列给进程长时间片、更低调度频率,适合长任务和CPU密集进程。
  • 掌握如何在已有调度框架下扩展更复杂的调度算法,并根据进程运行特征动态调整其优先级队列。
  • 借助之前实现的wait_sched统计工具,对比MLFQ与RR、PR调度模式下进程的时间分布和性能指标。

3.3.2 代码实现指导

设计思路:在已有RR/PR基础上添加MLFQ。MLFQ有多个队列(例如4个),每个队列对应不同的时间片长度。初始时新建进程放入最高优先级队列(队列0)。当进程耗尽当前队列的时间片仍未完成,就降入下一级队列,以获得更长的时间片但更低的调度优先级。I/O密集进程经常sleep/wakeup,因此频繁返回高优先级队列,从而获得较快的响应。可选地,定期老化(aging)机制保证长时间未调度的低优先级进程得到优先级提升,防止饥饿。

  1. 宏定义与数据结构扩展
    proc.h 中添加 MLFQ相关宏和数据结构,定义队列数量与每个队列的时间片长度。为每个进程添加 queue_leveltime_slice_used 字段,用于记录进程当前所在的队列及在该队列中已使用的时间片数。

    // proc.h
    
    // 切换调度策略
    // #define RR
    // #define PR
    #define MLFQ
    
    #define NQUEUE 4             // 四个队列
    #define TSLICE_Q0 4          // 队列0的时间片长度
    #define TSLICE_Q1 8          // 队列1的时间片长度
    #define TSLICE_Q2 16         // 队列2的时间片长度
    #define TSLICE_Q3 32         // 队列3的时间片长度
    
    struct proc {
      ...
      int queue_level;           // 当前队列级别 [0..NQUEUE-1]
      int time_slice_used;       // 当前队列中已使用的时间片数
      ...
    };
    
    struct runqueue {
      struct proc *procs[NPROC];
      int head;
      int tail;
    };
    
    extern struct runqueue mlfq_queue[NQUEUE];
  2. 初始化与队列操作函数
    proc.c 中定义 mlfq_queue[] 并实现 init_mlfq_queues() 初始化队列,push_to_queue()pop_from_queue() 函数用于在队列中插入和取出进程。

    // proc.c
    struct runqueue mlfq_queue[NQUEUE];
    
    static void init_mlfq_queues() {
      for (int i = 0; i < NQUEUE; i++) {
        mlfq_queue[i].head = 0;
        mlfq_queue[i].tail = 0;
      }
    }
    
    static int push_to_queue(int level, struct proc *p) {
      struct runqueue *q = &mlfq_queue[level];
      int next = (q->tail + 1) % NPROC;
      if (next == q->head) {
        // 队列已满,理论上不常发生
        return -1;
      }
      q->procs[q->tail] = p;
      q->tail = next;
      return 0;
    }
    
    static struct proc* pop_from_queue(int level) {
      struct runqueue *q = &mlfq_queue[level];
      if (q->head == q->tail) {
        // 队列为空
        return 0;
      }
      struct proc *p = q->procs[q->head];
      q->head = (q->head + 1) % NPROC;
      return p;
    }
    
    static int get_timeslice(int level) {
      switch(level) {
        case 0: return TSLICE_Q0;
        case 1: return TSLICE_Q1;
        case 2: return TSLICE_Q2;
        default: return TSLICE_Q3;
      }
    }
  3. 为进程分配初始队列等级
    allocproc()userinit()fork() 函数中,当创建新的进程时,设置 queue_level = 0time_slice_used = 0,并将新进程加入最高优先级队列(Q0)。

    // 在allocproc成功创建后
    p->queue_level = 0;
    p->time_slice_used = 0;
    push_to_queue(p->queue_level, p);

    fork() 完成子进程创建后同样为其赋初始队列等级为0并入队。

  4. 状态变更与队列维护
    wakeup()yield()exit()sleep() 等状态切换点调用 on_state_change() 更新进程时间统计,并根据进程从SLEEPING到RUNNABLE或从RUNNING到RUNNABLE的转变,将其加入相应的队列。

    // wakeup中,当进程变为RUNNABLE时
    p->state = RUNNABLE;
    push_to_queue(p->queue_level, p);
    on_state_change(SLEEPING, RUNNABLE, p);
    
    // yield中,当进程从RUNNING转回RUNNABLE时
    p->state = RUNNABLE;
    p->time_slice_used = 0;
    push_to_queue(p->queue_level, p);
    on_state_change(RUNNING, RUNNABLE, p);

    确保每次状态变更时都正确维护队列,避免队列与进程状态不一致。

  5. 在scheduler()中实现MLFQ调度逻辑
    启用 #define MLFQ 后,在 scheduler() 中依次从最高级队列到最低级队列查找RUNNABLE进程。当找到进程时,将其设为RUNNING并进行上下文切换。若未找到则继续检查下一级队列。

    // scheduler中
    #elif defined(MLFQ)
    for (int level = 0; level < NQUEUE; level++) {
      acquire(&plist_lock);
      struct proc *p = pop_from_queue(level);
      if (p) {
        acquire(&p->lock);
        if (p->state == RUNNABLE) {
          on_state_change(RUNNABLE, RUNNING, p);
          p->state = RUNNING;
          c->proc = p;
          p->time_slice_used = 0;
          release(&plist_lock);
    
          swtch(&c->context, &p->context);
          // 返回后进程可能已切换状态
          c->proc = 0;
          release(&p->lock);
          break; 
        } else {
          // 状态不对则放回队列
          push_to_queue(p->queue_level, p);
          release(&p->lock);
        }
        release(&plist_lock);
      } else {
        release(&plist_lock);
        // 当前队列为空,检查下一个队列
      }
    }
  6. 在trap中进行时间片消耗和队列降级
    trap.cusertrap() 中,当时钟中断发生时,若当前进程正在运行,time_slice_used++。当达到该队列分配的时间片上限时(通过 get_timeslice() 获取),则降低该进程的队列等级(若还未到底层队列)并 yield() 强制切换。

    // trap.c usertrap函数中
    if (which_dev == 2) { // timer interrupt
      struct proc *p = myproc();
      if (p && p->state == RUNNING) {
        p->time_slice_used++;
        if (p->time_slice_used >= get_timeslice(p->queue_level)) {
          // 用尽当前队列的时间片,降级队列(若可),并yield
          if (p->queue_level < NQUEUE - 1)
            p->queue_level++;
          yield();
        }
      }
      lapiceoi();
    }
  7. 老化与rebalance机制(可选)
    scheduler()trap() 中定期调用 mlfq_rebalance(),对长期未获得CPU的进程进行队列提升,从而防止低优先级进程饥饿。例如,根据 ticks - p->last_scheduled_time 的差值判断是否提高队列优先级。

    static void mlfq_rebalance() {
      for (struct proc *p = proc; p < &proc[NPROC]; p++) {
        acquire(&p->lock);
        if (p->state == RUNNABLE) {
          // 简单策略:若长期未调度,上调其优先级等级
          // p->queue_level = max(p->queue_level - 1, 0);
        }
        release(&p->lock);
      }
    }

    在实际中可以通过计时器或特定ticks间隔定期调用 mlfq_rebalance()

3.3.3 测试方法

  1. 基本测试
    启用MLFQ宏定义后 make qemu 启动。执行 lsecho 确保系统正常运行。

  2. CPU密集与I/O密集对比测试
    同时运行 cpucalc(CPU密集型)与 iotask(I/O密集型)进程:

    $ cpucalc &
    $ cpucalc &
    $ iotask &
    $ iotask &

    观察I/O密集进程是否保持在较高优先级队列,快速响应;CPU密集进程是否逐渐降入较低优先级队列。

  3. 数据统计与比较
    使用 stat npriostat n 对进程状态时间进行统计,并与RR或PR模式下的结果对比。

    • I/O密集型进程在MLFQ中应比在RR/PR中获得更短的RUNNABLE等待时间,更快的响应。
    • CPU密集型进程会有较长的RUNNABLE等待,但系统整体响应性可能更佳。
  4. 老化机制验证
    若实现老化机制,可运行长时间测试观察低优先级进程是否在长时间等待后得到队列提升。

  5. 调试与日志输出
    push_to_queue()pop_from_queue()mlfq_rebalance()中添加printf(),记录进程队列变化、优先级上升/下降的过程。通过日志分析对照预期行为,确保实现正确性。

3.4 任务点四:不同调度策略对比

在完成了 RR(Round-Robin)、PR(优先级调度)和 MLFQ(多级反馈队列调度)三种调度策略的实现和测试之后,我们需要对这三种调度策略在实际运行中的表现进行对比和分析。通过量化进程在各状态下花费的时间(RUNNABLE、RUNNING、SLEEPING)以及平均周转时间(Turnaround Time),我们可以更直观地理解这些调度策略各自的特点和适用场景。

对比内容与指标

  1. 状态时间分布
    使用 stat npriostat n 等实验程序获取在不同调度策略下进程的时间统计数据。当运行相同数量、相同负载特征的子进程时:

    • RR模式下:所有进程的RUNNABLERUNNING时间分布相对均衡。由于RR不考虑进程特征,CPU密集和I/O密集进程的处理方式相同,这可能导致I/O密集进程的响应不够快速。
    • PR模式下:高优先级的进程RUNNABLE时间较短、RUNNING时间较为集中,而低优先级进程可能会长时间处于RUNNABLE状态却得不到执行,出现"饥饿"现象。PR策略提高了关键进程的响应速度,却牺牲了一部分进程的公平性。
    • MLFQ模式下:I/O密集型进程有机会经常回到高优先级队列,因此RUNNABLE等待时间较短,加快了对交互性进程的响应。而CPU密集型进程在多次用完时间片后逐渐下沉到低优先级队列,RUNNABLE时间可能增加,但系统整体响应速度对短任务和I/O密集任务更友好,兼顾了公平性与适应性。
  2. 平均周转时间(Turnaround Time)
    周转时间是进程从创建到完成的总耗时。通过对比RR、PR、MLFQ三种策略的平均周转时间,我们可以看出:

    • RR模式下平均周转时间较为均衡,但在处理CPU密集型任务多、或者需要快速响应的I/O密集任务时不够灵活。
    • PR模式下关键任务(高优先级)平均周转时间显著降低,但低优先级进程的周转时间可能大幅增加,整体系统的平均周转时间可能受到影响。
    • MLFQ模式下,如果有合适的时间片设置和老化策略,系统对大多数进程类型的平均周转时间能有更好的平衡。I/O密集进程的周转时间会显著降低,而CPU密集型进程的周转时间会略长一些,但整体仍然能较好地满足系统对响应和吞吐的双重要求。
  3. 进程类型对比
    同时运行多种类型进程(如CPU密集型的cpucalc与I/O密集型的iotask)并对比三种策略下的结果会更直观。

    • RR:两者平等对待,无区分;I/O密集进程无法迅速获得CPU。
    • PR:若I/O进程赋予高优先级,可比CPU进程更快获得CPU,但需手动设置优先级。
    • MLFQ:无需手动设置优先级,I/O进程自动频繁回到高优先级队列,CPU进程则渐渐下沉。进程特性被自动适应,提升使用体验。
  4. 饥饿与公平性

    • RR:几乎不存在饥饿问题,每个进程轮流获得CPU。但无法对关键任务优先处理。
    • PR:高优先级进程占优,低优先级进程可能长期饥饿,失去公平性。
    • MLFQ:通过老化和多级队列调度策略,MLFQ可以在一定程度上避免进程长期饥饿问题,又能给I/O密集进程更多优先机会。公平性和响应性能达到较好的平衡。

总结与适用场景

  • RR:适用于简单的时间共享系统,有利于保证基本的公平性,但缺乏进程特性区分能力,无法优先处理关键任务或适应混合工作负载。

  • PR:适合需要显著区分任务优先级的场景(如实时任务、关键进程必须快速响应等),但需谨防低优先级进程长期被延迟的问题。

  • MLFQ:在多任务混合场景下(既有CPU密集进程,又有I/O密集进程),MLFQ能够更好地自动调配资源、提高系统响应性能。其多层队列机制可以在不依赖手动优先级设置的情况下自动适应不同类型进程的需求,并通过定期老化机制保证长期公平性,适合更接近真实生产环境的需求。

通过对RR、PR、MLFQ三种调度策略的比较与分析,学生可以深入理解不同调度策略的设计理念和实际影响,掌握各种调度策略在性能和公平性之间的权衡原则。这为日后深入研究高级调度策略、实时系统调度、多核环境调度优化等奠定了坚实基础。

4 和现有实验相比的完善性和创新点

MIT 6.S081的学习过程中,我们会完成一系列针对xv6操作系统的实验,这些实验涵盖了系统调用、内存管理、文件系统等重要内容。然而,这些实验相对缺失了对**进程调度(Scheduling)**机制的深入实践和拓展。调度是操作系统的核心内容之一,通过合理的调度策略,操作系统能够高效地在多个进程之间分配CPU时间,从而提高系统的整体性能和用户体验。

因此,赵进老师为我们布置了调度实验,而我对这个实验进行了优化和扩充,使其更利于本科教学。

本实验的完善性与创新性主要体现在以下几个方面

  1. 弥补基础实验中的调度空白
    MIT 6.S081的实验中虽然涉及到调度器的基础实现,但并未提供深入实践不同调度策略的内容。本实验通过扩展Lab3 Scheduling,从最基本的时间片轮转调度(RR)入手,逐步引入优先级调度(PR)和多级反馈队列调度(MLFQ),补充了调度机制这一关键内容的完整学习路径。

  2. 实验深度的提升
    原有的调度实验仅限于RR调度和优先级调度(PR)的实现,而本实验在其基础上拓展了多级反馈队列调度(MLFQ)。通过这些逐层深入的调度策略设计与实现,学生能够更直观地理解进程调度的复杂性,尤其是多级反馈队列调度如何通过动态调整队列优先级和时间片长度,兼顾不同类型进程的响应时间与公平性。

  3. 加强理论与实践结合
    本实验不仅让学生动手实现不同的调度策略,还借助wait_sched系统调用和statpriostat等统计工具,对调度策略的实际效果进行量化分析。通过对不同调度策略下的状态时间分布、平均周转时间等关键指标进行比较,学生可以将理论学习与实验结果结合,深入理解调度策略对系统性能的影响。

  4. 引入实际系统中常见的调度机制
    多级反馈队列调度(MLFQ)是现代操作系统中常见的调度策略之一。其动态调整优先级的设计广泛应用于Windows、Linux等系统中。本实验通过实现MLFQ,使学生能够在实验环境中观察到该策略对CPU密集型与I/O密集型进程的差异化处理效果,从而理解其在实际系统中的重要性和优势。

  5. 提供可扩展的实验框架
    本实验的设计不仅局限于现有内容,还为后续扩展提供了良好的框架。例如,学生可以尝试在MLFQ基础上实现更复杂的功能,如实时调度、基于权重的调度、优先级动态调整策略等。这种开放性设计鼓励学生进行更深层次的探索与创新,提升综合实践能力。

  6. 增强对调度策略特点与适用场景的理解
    通过实现和对比RR、PR和MLFQ三种调度策略,学生可以深入理解这些算法的特点:

    • RR:公平、简单,但无法区分任务特性。
    • PR:对关键任务响应迅速,但可能导致低优先级任务饥饿。
    • MLFQ:兼顾不同任务类型的需求,自动适应进程特性,适合实际操作系统的复杂工作负载。
  7. 对标真实系统需求的设计思路
    实验内容从调度器的设计原则出发,结合实际应用场景,逐步引导学生从简单到复杂的调度策略实现,贴近真实操作系统的需求。通过实验,学生不仅掌握了调度器的实现方法,还理解了不同调度算法的适用场景及其在实际系统中的意义。

通过上述完善与创新,本实验将原有调度内容从浅层次的实现引导升级为深层次的对比与分析,使学生能够全面掌握调度的核心概念、实现方法及其对系统性能的影响。这一设计为学生今后学习更高级的操作系统概念和实践提供了坚实的基础,也激发了他们在操作系统方向上的进一步探索与创新。