Finish lab6

This commit is contained in:
Object Ho 2014-10-14 22:09:13 +08:00
parent dde4fe5f1d
commit bd716e0b99
34 changed files with 516 additions and 556 deletions

View File

@ -82,6 +82,26 @@
* [Lab 4](lab4.md) * [Lab 4](lab4.md)
* [Lab 5](lab5.md) * [Lab 5](lab5.md)
* [Lab 6](lab6.md) * [Lab 6](lab6.md)
* [实验目的](lab6/lab6_1_goals.md)
* [实验内容](lab6/lab6_2_labs.md)
* [练习](lab6/lab6_2_1_exercises.md)
* [项目组成](lab6/lab6_2_2_files.md)
* [调度框架和调度算法设计与实现](lab6/lab6_3_scheduler_design.md)
* [实验执行流程概述](lab6/lab6_3_1_exercises.md)
* [计时器的原理和实现](lab6/lab6_3_2_scheduler_implement.md)
* [进程状态](lab6/lab6_3_3_process_state.md)
* [进程调度实现](lab6/lab6_3_4_process_implement.md)
* [内核抢占点](lab6/lab6_3_4_1_kernel_preempt_point.md)
* [进程切换过程](lab6/lab6_3_4_2_process_switch.md)
* [调度框架和调度算法](lab6/lab6_3_5_scheduler_framework.md)
* [设计思路](lab6/lab6_3_5_1_designed.md)
* [数据结构](lab6/lab6_3_5_2_data_structure.md)
* [调度点的相关关键函数](lab6/lab6_3_5_3_scheduler_point_functions.md)
* [RR 调度算法实现](lab6/lab6_3_5_4_RR.md)
* [Stride Scheduling](lab6/lab6_3_6_stride_scheduling.md)
* [基本思路](lab6/lab6_3_6_1_basic_method.md)
* [使用优先队列实现 Stride Scheduling](lab6/lab6_3_6_2_priority_queue.md)
* [实验报告要求](lab6/lab6_4_labs_requirement.md)
* [Lab 7](lab7.md) * [Lab 7](lab7.md)
* [实验目的](lab7/lab7_1_goals.md) * [实验目的](lab7/lab7_1_goals.md)
* [实验内容](lab7/lab7_2_labs.md) * [实验内容](lab7/lab7_2_labs.md)

BIN
lab0/.DS_Store vendored

Binary file not shown.

BIN
lab0_figs/.DS_Store vendored

Binary file not shown.

BIN
lab1/.DS_Store vendored

Binary file not shown.

BIN
lab2/.DS_Store vendored

Binary file not shown.

BIN
lab2_figs/.DS_Store vendored

Binary file not shown.

BIN
lab3/.DS_Store vendored

Binary file not shown.

556
lab6.md
View File

@ -1,557 +1 @@
# 实验六: 调度器 # 实验六: 调度器
## 1 实验目的
* 理解操作系统的调度管理机制
* 熟悉 ucore 的系统调度器框架以及缺省的Round-Robin 调度算法
* 基于调度器框架实现一个(Stride Scheduling)调度算法来替换缺省的调度算法
## 2 实验内容
实验五完成了用户进程的管理可在用户态运行多个进程。但到目前为止采用的调度策略是很简单的FIFO调度策略。本次实验主要是熟悉ucore的系统调度器框架以及基于此框架的
Round-RobinRR 调度算法。然后参考RR调度算法的实现完成Stride
Scheduling调度算法。
### 2.1 练习
#### 练习0填写已有实验
本实验依赖实验1/2/3/4/5。请把你做的实验2/3/4/5的代码填入本实验中代码中有“LAB1”/
“LAB2”/“LAB3”/“LAB4”/“LAB5”的注释相应部分。并确保编译通过。注意为了能够正确执行lab6的测试应用程序可能需对已完成的实验1/2/3/4/5的代码进行进一步改进。
#### 练习1: 使用 Round Robin 调度算法(不需要编码)
完成练习0后建议大家比较一下可用kdiff3等文件比较软件个人完成的lab5和练习0完成后的刚修改的lab6之间的区别分析了解lab6采用RR调度算法后的执行过程。执行make
grade大部分测试用例应该通过。但执行priority.c应该过不去。
#### 练习2: 实现 Stride Scheduling 调度算法(需要编码)
首先需要换掉RR调度器的实现即用default\_sched\_stride\_c覆盖default\_sched.c。然后根据此文件和后续文档对Stride度器的相关描述完成Stride调度算法的实现。
后面的实验文档部分给出了Stride调度算法的大体描述。这里给出Stride调度算法的一些相关的资料目前网上中文的资料比较欠缺
* [http://wwwagss.informatik.uni-kl.de/Projekte/Squirrel/stride/node3.html](http://wwwagss.informatik.uni-kl.de/Projekte/Squirrel/stride/node3.html)
* [http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.138.3502&rank=1](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.138.3502&rank=1)
* 也可GOOGLE “Stride Scheduling” 来查找相关资料
执行make grade
。如果所显示的应用程序检测都输出ok则基本正确。如果只是priority.c过不去可执行
make run-priority
命令来单独调试它。大致执行结果可看附录。( 使用的是 qemu-1.0.1 )。
#### 扩展练习 Challenge :实现 Linux 的 CFS 调度算法
在ucore的调度器框架下实现下Linux的CFS调度算法。可阅读相关Linux内核书籍或查询网上资料可了解CFS的细节然后大致实现在ucore中。
### 2.2 项目组成
├── boot
├── kern
│ ├── debug
│ ├── driver
│ ├── fs
│ ├── init
│ ├── libs
│ ├── mm
│ ├── process
│ │ ├── .....
│ │ ├── proc.c
│ │ ├── proc.h
│ │ └── switch.S
│ ├── schedule
│ │ ├── default\_sched.c
│ │ ├── default\_sched.h
│ │ ├── default\_sched\_stride\_c
│ │ ├── sched.c
│ │ └── sched.h
│ ├── syscall
│ │ ├── syscall.c
│ │ └── syscall.h
相对与实验五,实验六主要增加的文件如上表红色部分所示,主要修改的文件如上表紫色部分所示。主要改动如下:
简单说明如下:
* libs/skew\_heap.h:
提供了基本的优先队列数据结构,为本次实验提供了抽象数据结构方面的支持。
* kern/process/proc.[ch]proc.h中扩展了proc\_struct的成员变量用于RR和stride调度算法。proc.c中实现了lab6\_set\_priority用于设置进程的优先级。
* kern/schedule/{sched.h,sched.c}: 定义了 ucore
的调度器框架,其中包括相关的数
据结构(包括调度器的接口和运行队列的结构),和具体的运行时机制。
* kern/schedule/{default\_sched.h,default\_sched.c}: 具体的 round-robin
算法,在本次实验中你需要了解其实现。
* kern/schedule/default\_sched\_stride\_c: Stride
Scheduling调度器的基本框架在此次
实验中你需要填充其中的空白部分以实现一个完整的 Stride 调度器。
* kern/syscall/syscall.[ch]:
增加了sys\_gettime系统调用便于用户进程获取当前时钟值增加了sys\_lab6\_set\_priority系统调用便于用户进程设置进程优先级给priority.c用
* user/{matrix.c,priority.c,. . . }:
相关的一些测试用户程序测试调度算法的正确性user
目录下包含但不限于这些程序。在完成实验过程中,建议阅读这些测试程序,以了解这些程序的行为,便于进行调试。
## 3 调度框架和调度算法设计与实现
### 3.1 实验执行流程概述
在实验五创建了用户进程并让它们正确运行。这中间也实现了FIFO调度策略。可通过阅读实验五下的
kern/schedule/sched.c 的 schedule
函数的实现来了解其FIFO调度策略。与实验五相比实验六专门需要针对处理器调度框架和各种算法进行设计与实现为此对ucore的调度部分进行了适当的修改使得kern/schedule/sched.c
只实现调度器框架而不再涉及具体的调度算法实现。而调度算法在单独的文件default\_sched.[ch])中实现。
除此之外实验中还涉及了idle进程的概念。当cpu没有进程可以执行的时候系统应该如何工作在实验五的scheduler实现中ucore内核不断的遍历进程池直到找到第一个runnable状态的
process调用并执行它。也就是说当系统没有进程可以执行的时候它会把所有
cpu 时间用在搜索进程池,以实现
idle的目的。但是这样的设计不被大多数操作系统所采用原因在于它将进程调度和
idle 进程两种不同的概念混在了一起而且当调度器比较复杂时schedule
函数本身也会比较复杂这样的设计结构很不清晰而且难免会出现错误。所以在此次实验中ucore
建立了一个单独的进程(kern/process/proc.c 中的 idleproc)作为 cpu 空闲时的
idle 进程,这个程序是通常一个死循环。你需要了解这个程序的实现。
接下来可看看实验六的大致执行过程在init.c中的kern\_init函数增加了对sched\_init函数的调用。sched\_init函数主要完成了对实现特定调度算法的调度类sched\_class的绑定使得ucore在后续的执行中能够通过调度框架找到实现特定调度算法的调度类并完成进程调度相关工作。为了更好地理解实验六整个运行过程这里需要关注的重点问题包括
1. 何时或何事件发生后需要调度?
2. 何时或何事件发生后需要调整实现调度算法所涉及的参数?
3. 如果基于调度框架设计具体的调度算法?
4. 如果灵活应用链表等数据结构管理进程调度?
大家可带着这些问题进一步阅读后续的内容。
### 3.2 计时器的原理和实现
在传统的操作系统中,计时器是其中一个基础而重要的功能.它提供了基于时间事件的调度机制。在
ucore 中timer 中断(irq0) 给操作系统提供了有一定间隔的时间事件,
操作系统将其作为基本的调度和计时单位(我们记两次时间中断之间的时间间隔为一个
时间片timer splice
基于此时间单位,操作系统得以向上提供基于时间点的事件,并实现基于时间长度的等待和唤醒机制。在每个时钟中断发生时,操作系统产生对应的时间事件。应用程序或者操作系统的其他组件可以以此来构建更复杂和高级的调度。
* sched.h, sched.c 定义了有关timer的各种相关接口来使用 timer
服务,其中主要包括:
* typedef struct {……} timer\_t: 定义了 timer\_t 的基本结构,其可以用
sched.h 中的timer\_init函数对其进行初始化。
* void timer\_init(timer t \*timer, struct proc\_struct \*proc, int
expires): 对某计时器 进行初始化,让它在 expires 时间片之后唤醒 proc
进程。
* void add\_timer(timer t \*timer): 向系统添加某个初始化过的
timer\_t该计时器在 指定时间后被激活,并将对应的进程唤醒至
runnable如果当前进程处在等待状态
* void del\_timer(timer\_t \*time):
向系统删除(或者说取消)某一个计时器。该计时
器在取消后不会被系统激活并唤醒进程。
* void run\_timer\_list(void):
更新当前系统时间点,遍历当前所有处在系统管理内的
计时器,找出所有应该激活的计数器,并激活它们。该过程在且只在每次计时器中断时被调用。在
ucore 中,其还会调用调度器事件处理程序。
一个 timer\_t 在系统中的存活周期可以被描述如下:
1. timer\_t 在某个位置被创建和初始化,并通过
add\_timer加入系统管理列表中
2. 系统时间被不断累加,直到 run\_timer\_list 发现该 timer\_t到期。
3. run\_timer\_list更改对应的进程状态并从系统管理列表中移除该timer\_t。
尽管本次实验并不需要填充计时器相关的代码但是作为系统重要的组件同时计时器也是调度器的一个部分你应该了解其相关机制和在ucore中的实现方法。接下来的实验描述将会在一定程度上忽略计时器对调度带来的影响即不考虑基于固定时间点的调度。
### 3.3 进程状态
在此次实验中,进程的状态之间的转换需要有一个更为清晰的表述,在 ucore
runnable的进程会被放在运行队列中。值得注意的是在具体实现中ucore定义的进程控制块struct
proc\_struct包含了成员变量state,用于描述进程的运行状态而running和runnable共享同一个状态(state)值(PROC\_RUNNABLE)。不同之处在于处于running态的进程不会放在运行队列中。进程的正常生命周期如下
* 进程首先在 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态变成其他状态的进程都要出运行队列反之被放入某个运行队列中。
### 3.4 进程调度实现
#### 3.4.1 内核抢占点
调度本质上体现了对CPU资源的抢占。对于用户进程而言由于有中断的产生可以随时打断用户进程的执行转到操作系统内部从而给了操作系统以调度控制权让操作系统可以根据具体情况比如用户进程时间片已经用完了选择其他用户进程执行。这体现了用户进程的可抢占性preemptive。但如果把ucore操作系统也看成是一个特殊的内核进程或多个内核线程的集合那ucore是否也是可抢占的呢其实ucore
内核执行是不可抢占的non-preemptive即在执行“任意”内核代码时CPU控制权可被强制剥夺。这里需要注意不是在所有情况下ucore内核执行都是不可抢占的有以下几种“固定”情况是例外
1. 进行同步互斥操作比如争抢一个信号量、锁lab7中会详细分析
2.进行磁盘读写等耗时的异步操作由于等待完成的耗时太长ucore会调用shcedule让其他就绪进程执行。
这几种情况其实都是由于当前进程所需的某个资源也可称为事件无法得到满足无法继续执行下去从而不得不主动放弃对CPU的控制权。如果参照用户进程任何位置都可被内核打断并放弃CPU控制权的情况这些在内核中放弃CPU控制权的执行地点是“固定”而不是“任意”的不能体现内核任意位置都可抢占性的特点。我们搜寻一下实验五的代码可发现在如下几处地方调用了shedule函数
表一调用进程调度函数schedule的位置和原因
<table>
<tr><td>编号</td><td>位置</td><td>原因</td></tr>
<tr><td>1</td><td>proc.c::do_exit</td><td>用户线程执行结束主动放弃CPU控制权。</td></tr>
<tr><td>2</td><td>proc.c::do_wait</td><td>用户线程等待子进程结束主动放弃CPU控制权。</td></tr>
<tr><td>3</td><td>proc.c::init_main</td><td>1. initproc内核线程等待所有用户进程结束如果没有结束就主动放弃CPU控制权;
2. initproc内核线程在所有用户进程结束后让kswapd内核线程执行10次用于回收空闲内存资源</td></tr>
<tr><td>4</td><td>proc.c::cpu_idle</td><td>idleproc内核线程的工作就是等待有处于就绪态的进程或线程如果有就调用schedule函数</td></tr>
<tr><td>5</td><td>sync.h::lock</td><td>在获取锁的过程中如果无法得到锁则主动放弃CPU控制权</td></tr>
<tr><td>6</td><td>trap.c::trap</td><td>如果在当前进程在用户态被打断去且当前进程控制块的成员变量need_resched设置为1则当前线程会放弃CPU控制权</td></tr>
</table>
仔细分析上述位置第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中的所有全局变量的互斥访问操作以防止所谓的race
condition现象这样ucore的实现复杂度会增加不少。
#### 3.4.2 进程切换过程
进程调度函数schedule选择了下一个将占用CPU执行的进程后将调用进程切换从而让新的进程得以执行。通过实验四和实验五的理解应该已经对进程调度和上下文切换有了初步的认识。在实验五中结合调度器框架的设计可对ucore中的进程切换以及堆栈的维护和使用等有更加深刻的认识。假定有两个用户进程在二者进行进程切换的过程中具体的步骤如下
首先在执行某进程A的用户代码时出现了一个 trap
(例如是一个外设产生的中断),这
个时候就会从进程A的用户态切换到内核态(过程(1))并且保存好进程A的trapframe
当内核态处理中断时发现需要进行进程切换时ucore要通过schedule函数选择下一个将占用CPU执行的进程即进程B然后会调用proc\_run函数proc\_run函数进一步调用switch\_to函数切换到进程B的内核态(过程(2))继续进程B上一次在内核态的操作并通过iret指令最终将执行权转交给进程B的用户空间(过程(3))。
当进程B由于某种原因发生中断之后(过程(4))会从进程B的用户态切换到内核态并且保存好进程B的trapframe当内核态处理中断时发现需要进行进程切换时即需要切换到进程Aucore再次切换到进程A(过程(5))会执行进程A上一次在内核调用
schedule (具体还要跟踪到 switch\_to 函数)
函数返回后的下一行代码这行代码当然还是在进程A的上一次中断处理流程中。最后当进程
A的中断处理完毕的时候执行权又会反交给进程A的用户代码(过程(6))。这就是在只有两个进程的情况下,进程切换间的大体流程。
几点需要强调的是:
**a)**
需要透彻理解在进程切换以后程序是从哪里开始执行的需要注意到虽然指令还是同一个cpu上执行但是此时已经是另外一个进程在执行了且使用的资源已经完全不同了。
**b)**
内核在第一个程序运行的时候需要进行哪些操作有了实验四和实验五的经验可以确定内核启动第一个用户进程的过程实际上是从进程启动时的内核状态切换到该用户进程的内核状态的过程而且该用户进程在用户态的起始入口应该是forkret。
### 3.5 调度框架和调度算法
#### 3.5.1 设计思路
实行一个进程调度策略到底需要实现哪些基本功能对应的数据结构首先考虑到一个无论哪种调度算法都需要选择一个就绪进程来占用CPU运行。为此我们可把就绪进程组织起来可用队列双向链表、二叉树、红黑树、数组…等不同的组织方式。
在操作方面如果需要选择一个就绪进程就可以从基于某种组织方式的就绪进程集合中选择出一个进程执行。需要注意这里“选择”和“出”是两个操作选择是在集合中挑选一个“合适”的进程“出”意味着离开就绪进程集合。另外考虑到一个处于运行态的进程还会由于某种原因比如时间片用完了回到就绪态而不能继续占用CPU执行这就会重新进入到就绪进程集合中。这两种情况就形成了调度器相关的三个基本操作在就绪进程集合中选择、进入就绪进程集合和离开就绪进程集合。这三个操作属于调度器的基本操作。
在进程的执行过程中,就绪进程的等待时间和执行进程的执行时间是影响调度选择的重要因素,这两个因素随着时间的流逝和各种事件的发生在不停地变化,比如处于就绪态的进程等待调度的时间在增长,处于运行态的进程所消耗的时间片在减少等。这些进程状态变化的情况需要及时让进程调度器知道,便于选择更合适的进程执行。所以这种进程变化的情况就形成了调度器相关的一个变化感知操作:
timer时间事件感知操作。这样在进程运行或等待的过程中调度器可以调整进程控制块中与进程调度相关的属性值比如消耗的时间片、进程优先级等并可能导致对进程组织形式的调整比如以时间片大小的顺序来重排双向链表等并最终可能导致调选择新的进程占用CPU运行。这个操作属于调度器的进程调度属性调整操作。
#### 3.5.2 数据结构
在理解框架之前,需要先了解一下调度器框架所需要的数据结构。
* 通常的操作系统中,进程池是很大的(虽然在 ucore 中MAX\_PROCESS 很小)。
在 ucore 中,调度器引入 run-queue简称rq,
即运行队列)的概念,通过链表结构管理进程。
* 由于目前 ucore 设计运行在单
CPU上其内部只有一个全局的运行队列用来管理系统内全部的进程。
* 运行队列通过链表的形式进行组织。链表的每一个节点是一个list\_entry\_t,
每个list\_entry\_t 又对应到了 struct proc\_struct \*,
这其间的转换是通过宏 le2proc 来完成 的。具体来说,我们知道在 struct
proc\_struct 中有一个叫 run\_link 的 list\_entry\_t
因此可以通过偏移量逆向找到对因某个 run\_list的 struct proc\_struct。即
进程结构指针 proc = le2proc(链表节点指针, run\_link)。
* 为了保证调度器接口的通用性ucore
调度框架定义了如下接口,该接口中,几乎全部成员变量均为函数指针。具体的功能会在后面的框架说明中介绍。
```
1 struct sched_class {
2 // 调度器的名字
3 const char *name;
4 // 初始化运行队列
5 void (*init) (struct run_queue *rq);
6 // 将进程 p 插入队列 rq
7 void (*enqueue) (struct run_queue *rq, struct proc_struct *p);
8 // 将进程 p 从队列 rq 中删除
9 void (*dequeue) (struct run_queue *rq, struct proc_struct *p);
10 // 返回 运行队列 中下一个可执行的进程
11 struct proc_struct* (*pick_next) (struct run_queue *rq);
12 // timetick 处理函数
13 void (*proc_tick)(struct run_queue* rq, struct proc_struct* p);
14 };
```
• 此外proc.h 中的 struct proc\_struct 中也记录了一些调度相关的信息:
```
1 struct proc_struct {
2 // . . .
3 // 该进程是否需要调度,只对当前进程有效
4 volatile bool need_resched;
5 // 该进程的调度链表结构,该结构内部的连接组成了 运行队列 列表
6 list_entry_t run_link;
7 // 该进程剩余的时间片,只对当前进程有效
8 int time_slice;
9 // round-robin 调度器并不会用到以下成员
10 // 该进程在优先队列中的节点,仅在 LAB6 使用
11 skew_heap_entry_t lab6_run_pool;
12 // 该进程的调度优先级,仅在 LAB6 使用
13 uint32_t lab6_priority;
14 // 该进程的调度步进值,仅在 LAB6 使用
15 uint32_t lab6_stride;
16 };
```
在此次实验中,你需要了解 default\_sched.c
中的实现RR调度算法的函数。在该文件中你可以看到ucore 已经为 RR
调度算法创建好了一个名为 RR\_sched\_class 的调度策略类。
通过数据结构 struct run\_queue 来描述完整的
run\_queue运行队列。它的主要结构如下
```
1 struct run_queue {
2 //其运行队列的哨兵结构,可以看作是队列头和尾
3 list_entry_t run_list;
4 //优先队列形式的进程容器,只在 LAB6 中使用
5 skew_heap_entry_t *lab6_run_pool;
6 //表示其内部的进程总数
7 unsigned int proc_num;
8 //每个进程一轮占用的最多时间片
9 int max_time_slice;
10 };
```
在 ucore 框架中,运行队列存储的是当前可以调度的进程,所以,只有状态为
runnable的进程才能够进入运行队列。当前正在运行的进程并不会在运行队列中这一点需要注意。
#### 3.5.3 调度点的相关关键函数
虽然进程各种状态变化的原因和导致的调度处理各异但其实仔细观察各个流程的共性部分会发现其中只涉及了三个关键调度相关函数wakup\_proc、shedule、run\_timer\_list。如果我们能够让这三个调度相关函数的实现与具体调度算法无关那么就可以认为ucore实现了一个与调度算法无关的调度框架。
wakeup\_proc函数其实完成了把一个就绪进程放入到就绪进程队列中的工作为此还调用了一个调度类接口函数sched\_class\_enqueue这使得wakeup\_proc的实现与具体调度算法无关。schedule函数完成了与调度框架和调度算法相关三件事情:把当前继续占用CPU执行的运行进程放放入到就绪进程队列中从就绪进程队列中选择一个“合适”就绪进程把这个“合适”的就绪进程从就绪进程队列中摘除。通过调用三个调度类接口函数sched\_class\_enqueue、sched\_class\_pick\_next、sched\_class\_enqueue来使得完成这三件事情与具体的调度算法无关。run\_timer\_list函数在每次timer中断处理过程中被调用从而可用来调用调度算法所需的timer时间事件感知操作调整相关进程的进程调度相关的属性值。通过调用调度类接口函数sched\_class\_proc\_tick使得此操作与具体调度算法无关。
这里涉及了一系列调度类接口函数:
* sched_class_enqueue
* sched_class_dequeue
* sched_class_pick_next
* sched_class_proc_tick
这4个函数的实现其实就是调用某基于sched\_class数据结构的特定调度算法实现的4个指针函数。采用这样的调度类框架后如果我们需要实现一个新的调度算法则我们需要定义一个针对此算法的调度类的实例一个就绪进程队列的组织结构描述就行了其他的事情都可交给调度类框架来完成。
#### 3.5.4 RR 调度算法实现
RR调度算法的调度思想 是让所有
runnable态的进程分时轮流使用CPU时间。RR调度器维护当前runnable进程的有序运行队列。当前进程的时间片用完之后调度器将当前进程放置到运行队列的尾部再从其头部取出进程进行调度。RR调度算法的就绪队列在组织结构上也是一个双向链表只是增加了一个成员变量表明在此就绪进程队列中的最大执行时间片。而且在进程控制块proc\_struct中增加了一个成员变量time\_slice用来记录进程当前的可运行时间片段。这是由于RR调度算法需要考虑执行进程的运行时间不能太长。在每个timer到时的时候操作系统会递减当前执行进程的time\_slice当time\_slice为0时就意味着这个进程运行了一段时间这个时间片段称为进程的时间片需要把CPU让给其他进程执行于是操作系统就需要让此进程重新回到rq的队列尾且重置此进程的时间片为就绪队列的成员变量最大时间片max\_time\_slice值然后再从rq的队列头取出一个新的进程执行。下面来分析一下其调度算法的实现。
RR\_enqueue的函数实现如下表所示。即把某进程的进程控制块指针放入到rq队列末尾且如果进程控制块的时间片为0则需要把它重置为rq成员变量max\_time\_slice。这表示如果进程在当前的执行时间片已经用完需要等到下一次有机会运行时才能再执行一段时间。
```
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 ++;
}
```
RR\_pick\_next的函数实现如下表所示。即选取就绪进程队列rq中的队头队列元素并把队列元素转换成进程控制块指针。
```
static struct proc_struct *
FCFS_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\_dequeue的函数实现如下表所示。即把就绪进程队列rq的进程控制块指针的队列元素删除并把表示就绪进程个数的proc\_num减一。
```
static void
FCFS_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 --;
}
```
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;
}
}
```
### 3.6 Stride Scheduling
#### 3.6.1 基本思路
**【提示】请先看练习2中提到的论文, 理解后在看下面的内容。**
考察 round-robin 调度器,在假设所有进程都充分使用了其拥有的 CPU
时间资源的情况下,所有进程得到的 CPU
时间应该是相等的。但是有时候我们希望调度器能够更智能地为每个进程分配合理的
CPU
资源。假设我们为不同的进程分配不同的优先级则我们有可能希望每个进程得到的时间资源与他们的优先级成正比关系。Stride调度是基于这种想法的一个较为典型和简单的算法。除了简单易于实现以外它还有如下的特点
* 可控性:如我们之前所希望的,可以证明 Stride Scheduling对进程的调度次数正比于其优先级。
* 确定性:在不考虑计时器事件的情况下,整个调度机制都是可预知和重现的。该算法的基本思想可以考虑如下:
1. 为每个runnable的进程设置一个当前状态stride表示该进程当前的调度权。另外定义其对应的pass值表示对应进程在调度后stride 需要进行的累加值。
2. 每次需要调度时,从当前 runnable 态的进程中选择 stride最小的进程调度。
3. 对于获得调度的进程P将对应的stride加上其对应的步长pass只与进程的优先权有关系
4. 在一段固定的时间之后,回到 2.步骤重新调度当前stride最小的进程。
可以证明,如果令 P.pass =BigStride / P.priority
其中 P.priority 表示进程的优先权(大于 1而 BigStride
表示一个预先定义的大常数,则该调度方案为每个进程分配的时间将与其优先级成正比。证明过程我们在这里略去,有兴趣的同学可以在网上查找相关资料。将该调度器应用到
ucore 的调度器框架中来,则需要将调度器接口实现如下:
* init:
初始化调度器类的信息(如果有的话)。
初始化当前的运行队列为一个空的容器结构。比如和RR调度算法一样初始化为一个有序列表
* enqueue
初始化刚进入运行队列的进程 proc的stride属性。
将 proc插入放入运行队列中去注意这里并不要求放置在队列头部
* dequeue
从运行队列中删除相应的元素。
* pick next
扫描整个运行队列返回其中stride值最小的对应进程。
更新对应进程的stride值即pass = BIG\_STRIDE / P-\>priority; P-\>stride += pass。
* proc tick:
检测当前进程是否已用完分配的时间片。如果时间片用完,应该正确设置进程结构的相关标记来引起进程切换。
一个 process 最多可以连续运行 rq.max\_time\_slice个时间片。
在具体实现时有一个需要注意的地方stride属性的溢出问题在之前的实现里面
我们并没有考虑 stride 的数值范围,而这个值在理论上是不断增加的,在
stride溢出以后基于
stride的比较可能会出现错误。比如假设当前存在两个进程A和Bstride属性采用16位无符号整数进行存储。当前队列中元素如下假设当前运行的进程已经被重新放置进运行队列中
![image](lab6/image001.png)
此时应该选择 A 作为调度的进程,而在一轮调度后,队列将如下:
![image](lab6/image002.png)
可以看到由于溢出的出现进程间stride的理论比较和实际比较结果出现了偏差。我们首先在理论上分析这个问题令PASS\_MAX为当前所有进程里最大的步进值。则我们可以证明如下结论对每次
Stride
调度器的调度步骤中有其最大的步进值STRIDE\_MAX和最小的步进值STRIDE\_MIN
之差:
STRIDE\_MAX STRIDE\_MIN <= PASS\_MAX
提问 1如何证明该结论 有了该结论在加上之前对优先级有Priority \> 1
限制,我们有
STRIDE\_MAX STRIDE\_MIN <= BIG\_STRIDE
于是我们只要将BigStride取在某个范围之内即可保证对于任意两个 Stride
之差都会在机器整数表示的范围之内。而我们可以通过其与0的比较结构来得到两个
Stride的大小关系。在上例中虽然在直接的数值表示上 98 < 65535但是 98 -
65535 的结果用带符号的 16位整数表示的结果为99,
与理论值之差相等。所以在这个意义下 98 \> 65535。
基于这种特殊考虑的比较方法,即便
Stride有可能溢出我们仍能够得到理论上的当前最小Stride并做出正确的调度决定。
提问 2在 ucore 中,目前
Stride是采用无符号的32位整数表示。则BigStride应该取多少才能保证比较的正确性
#### 3.6.2 使用优先队列实现 Stride Scheduling
在上述的实现描述中对于每一次pick\_next函数我们都需要完整地扫描来获得当前最小的stride及其进程。这在进程非常多的时候是非常耗时和低效的有兴趣的同学可以在实
现了基于列表扫描的Stride调度器之后比较一下priority程序在Round-Robin及Stride调度器下各自的运行时间。考虑到其调度选择于优先队列的抽象逻辑一致我们考虑使用优化的优先队列数据结构实现该调度。
优先队列是这样一种数据结构:使用者可以快速的插入和删除队列中的元素,并且在预先指定的顺序下快速取得当前在队列中的最小(或者最大)值及其对应元素。可以看到,这样的数据结构非常符合
Stride 调度器的实现。
本次实验提供了libs/skew\_heap.h
作为优先队列的一个实现,该实现定义相关的结构和接口,其中主要包括:
```
1 // 优先队列节点的结构
2 typedef struct skew_heap_entry skew_heap_entry_t;
3 // 初始化一个队列节点
4 void skew_heap_init(skew_heap_entry_t *a);
5 // 将节点 b 插入至以节点 a 为队列头的队列中去,返回插入后的队列
6 skew_heap_entry_t *skew_heap_insert(skew_heap_entry_t *a,
7 skew_heap_entry_t *b,
8 compare_f comp);
9 // 将节点 b 插入从以节点 a 为队列头的队列中去,返回删除后的队列
10 skew_heap_entry_t *skew_heap_remove(skew_heap_entry_t *a,
11 skew_heap_entry_t *b,
12 compare_f comp);
```
其中优先队列的顺序是由比较函数comp决定的sched\_stride.c中提供了proc\_stride\_comp\_f比较器用来比较两个stride的大小你可以直接使用它。当使用优先队列作为Stride调度器的实现方式之后运行队列结构也需要作相关改变其中包括
* struct
run\_queue中的lab6\_run\_pool指针在使用优先队列的实现中表示当前优先队列
的头元素如果优先队列为空则其指向空指针NULL
* struct
proc\_struct中的lab6\_run\_pool结构表示当前进程对应的优先队列节点。
本次实验已经修改了系统相关部分的代码使得其能够很好地适应LAB6
新加入的数据结构和接口。而在实验中我们需要做的是用优先队列实现一个正确和高效的Stride调度器如果用较简略的伪代码描述则有
* init(rq):
Initialize rq-\>run\_list
Set rq-\>lab6\_run\_pool to NULL
Set rq-\>proc\_num to 0
* enqueue(rq, proc)
Initialize proc-\>time\_slice
Insert proc-\>lab6\_run\_pool into rq-\>lab6\_run\_pool
rq-\>proc\_num ++
* dequeue(rq, proc)
Remove proc-\>lab6\_run\_pool from rq-\>lab6\_run\_pool
rq-\>proc\_num --
* pick\_next(rq)
If rq-\>lab6\_run\_pool == NULL, return NULL
Find the proc corresponding to the pointer rq-\>lab6\_run\_pool
proc-\>lab6\_stride += BIG\_STRIDE / proc-\>lab6\_priority
Return proc
* proc\_tick(rq, proc):
If proc-\>time\_slice \> 0, proc-\>time\_slice --
If proc-\>time\_slice == 0, set the flag proc-\>need\_resched
## 4 实验报告要求
从网站上下载lab6.zip后解压得到本文档和代码目录
lab6完成实验中的各个练习。完成代码编写并检查无误后在对应目录下执行
make handin 任务,即会自动生成
lab6-handin.tar.gz。最后请一定提前或按时提交到网络学堂上。
注意有“LAB6”的注释主要是修改default\_sched\_swide\_c中的内容。代码中所有需要完成的地方challenge除外都有“LAB6”和“YOUR
CODE”的注释请在提交时特别注意保持注释并将“YOUR
CODE”替换为自己的学号并且将所有标有对应注释的部分填上正确的代码。
## 附录:执行 priority大致的显示输出
```
$ make run-priority
......
check_swap() succeeded!
++ setup timer interrupts
kernel_execve: pid = 2, name = "priority".
main: fork ok,now need to wait pids.
child pid 7, acc 2492000, time 2001
child pid 6, acc 1944000, time 2001
child pid 4, acc 960000, time 2002
child pid 5, acc 1488000, time 2003
child pid 3, acc 540000, time 2004
main: pid 3, acc 540000, time 2004
main: pid 4, acc 960000, time 2004
main: pid 5, acc 1488000, time 2004
main: pid 6, acc 1944000, time 2004
main: pid 7, acc 2492000, time 2004
main: wait pids over
stride sched correct result: 1 2 3 4 5
all user-mode processes have quit.
init check memory pass.
kernel panic at kern/process/proc.c:426:
initproc exit.
Welcome to the kernel debug monitor!!
Type 'help' for a list of commands.
K>
```

BIN
lab6/.DS_Store vendored

Binary file not shown.

20
lab6/hehe Normal file
View File

@ -0,0 +1,20 @@
*[] (lab6/lab6_1_goals.md)
*[] (lab6/lab6_2_1_exercises.md)
*[] (lab6/lab6_2_2_files.md)
*[] (lab6/lab6_2_labs.md)
*[] (lab6/lab6_3_1_exercises.md)
*[] (lab6/lab6_3_2_scheduler_implement.md)
*[] (lab6/lab6_3_3_process_state.md)
*[] (lab6/lab6_3_4_1_kernel_preempt_point.md)
*[] (lab6/lab6_3_4_2_process_switch.md)
*[] (lab6/lab6_3_4_process_implement.md)
*[] (lab6/lab6_3_5_1_designed.md)
*[] (lab6/lab6_3_5_2_data_structure.md)
*[] (lab6/lab6_3_5_3_scheduler_point_functions.md)
*[] (lab6/lab6_3_5_4_RR.md)
*[] (lab6/lab6_3_5_scheduler_framework.md)
*[] (lab6/lab6_3_6_1_basic_method.md)
*[] (lab6/lab6_3_6_2_priority_queue.md)
*[] (lab6/lab6_3_6_stride_scheduling.md)
*[] (lab6/lab6_3_scheduler_design.md)
*[] (lab6/lab6_4_labs_requirement.md)

6
lab6/lab6_1_goals.md Normal file
View File

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

View File

@ -0,0 +1,26 @@
### 2.1 练习
#### 练习0填写已有实验
本实验依赖实验1/2/3/4/5。请把你做的实验2/3/4/5的代码填入本实验中代码中有“LAB1”/“LAB2”/“LAB3”/“LAB4”“LAB5”的注释相应部分。并确保编译通过。注意为了能够正确执行lab6的测试应用程序可能需对已完成的实验1/2/3/4/5的代码进行进一步改进。
#### 练习1: 使用 Round Robin 调度算法(不需要编码)
完成练习0后建议大家比较一下可用kdiff3等文件比较软件个人完成的lab5和练习0完成后的刚修改的lab6之间的区别分析了解lab6采用RR调度算法后的执行过程。执行make grade大部分测试用例应该通过。但执行priority.c应该过不去。
#### 练习2: 实现 Stride Scheduling 调度算法(需要编码)
首先需要换掉RR调度器的实现即用default\_sched\_stride\_c覆盖default\_sched.c。然后根据此文件和后续文档对Stride度器的相关描述完成Stride调度算法的实现。
后面的实验文档部分给出了Stride调度算法的大体描述。这里给出Stride调度算法的一些相关的资料目前网上中文的资料比较欠缺
* [http://wwwagss.informatik.uni-kl.de/Projekte/Squirrel/stride/node3.html](http://wwwagss.informatik.uni-kl.de/Projekte/Squirrel/stride/node3.html)
* [http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.138.3502&rank=1](http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.138.3502&rank=1)
* 也可GOOGLE “Stride Scheduling” 来查找相关资料
执行make grade。如果所显示的应用程序检测都输出ok则基本正确。如果只是priority.c过不去可执行 make run-priority 命令来单独调试它。大致执行结果可看附录。( 使用的是 qemu-1.0.1 )。
#### 扩展练习 Challenge :实现 Linux 的 CFS 调度算法
在ucore的调度器框架下实现下Linux的CFS调度算法。可阅读相关Linux内核书籍或查询网上资料可了解CFS的细节然后大致实现在ucore中。

39
lab6/lab6_2_2_files.md Normal file
View File

@ -0,0 +1,39 @@
### 2.2 项目组成
```
├── boot
├── kern
│ ├── debug
│ ├── driver
│ ├── fs
│ ├── init
│ ├── libs
│ ├── mm
│ ├── process
│ │ ├── .....
│ │ ├── proc.c
│ │ ├── proc.h
│ │ └── switch.S
│ ├── schedule
│ │ ├── default\_sched.c
│ │ ├── default\_sched.h
│ │ ├── default\_sched\_stride\_c
│ │ ├── sched.c
│ │ └── sched.h
│ ├── syscall
│ │ ├── syscall.c
│ │ └── syscall.h
```
相对与实验五,实验六主要增加的文件如上表红色部分所示,主要修改的文件如上表紫色部分所示。主要改动如下:
简单说明如下:
* libs/skew\_heap.h:
提供了基本的优先队列数据结构,为本次实验提供了抽象数据结构方面的支持。
* kern/process/proc.[ch]proc.h中扩展了proc\_struct的成员变量用于RR和stride调度算法。proc.c中实现了lab6\_set\_priority用于设置进程的优先级。
* kern/schedule/{sched.h,sched.c}: 定义了 ucore 的调度器框架,其中包括相关的数据结构(包括调度器的接口和运行队列的结构),和具体的运行时机制。
* kern/schedule/{default\_sched.h,default\_sched.c}: 具体的 round-robin 算法,在本次实验中你需要了解其实现。
* kern/schedule/default\_sched\_stride\_c: Stride Scheduling调度器的基本框架在此次实验中你需要填充其中的空白部分以实现一个完整的 Stride 调度器。
* kern/syscall/syscall.[ch]: 增加了sys\_gettime系统调用便于用户进程获取当前时钟值增加了sys\_lab6\_set\_priority系统调用便于用户进程设置进程优先级给priority.c用
* user/{matrix.c,priority.c,. . . }: 相关的一些测试用户程序测试调度算法的正确性user目录下包含但不限于这些程序。在完成实验过程中建议阅读这些测试程序以了解这些程序的行为便于进行调试。

4
lab6/lab6_2_labs.md Normal file
View File

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

View File

@ -0,0 +1,15 @@
### 3.1 实验执行流程概述
在实验五创建了用户进程并让它们正确运行。这中间也实现了FIFO调度策略。可通过阅读实验五下的 kern/schedule/sched.c 的 schedule 函数的实现来了解其FIFO调度策略。与实验五相比实验六专门需要针对处理器调度框架和各种算法进行设计与实现为此对ucore的调度部分进行了适当的修改使得kern/schedule/sched.c 只实现调度器框架而不再涉及具体的调度算法实现。而调度算法在单独的文件default\_sched.[ch])中实现。
除此之外实验中还涉及了idle进程的概念。当cpu没有进程可以执行的时候系统应该如何工作在实验五的scheduler实现中ucore内核不断的遍历进程池直到找到第一个runnable状态的 process调用并执行它。也就是说当系统没有进程可以执行的时候它会把所有 cpu 时间用在搜索进程池,以实现 idle的目的。但是这样的设计不被大多数操作系统所采用原因在于它将进程调度和 idle 进程两种不同的概念混在了一起而且当调度器比较复杂时schedule 函数本身也会比较复杂这样的设计结构很不清晰而且难免会出现错误。所以在此次实验中ucore建立了一个单独的进程(kern/process/proc.c 中的 idleproc)作为 cpu 空闲时的 idle 进程,这个程序是通常一个死循环。你需要了解这个程序的实现。
接下来可看看实验六的大致执行过程在init.c中的kern\_init函数增加了对sched\_init函数的调用。sched\_init函数主要完成了对实现特定调度算法的调度类sched\_class的绑定使得ucore在后续的执行中能够通过调度框架找到实现特定调度算法的调度类并完成进程调度相关工作。为了更好地理解实验六整个运行过程这里需要关注的重点问题包括
1. 何时或何事件发生后需要调度?
2. 何时或何事件发生后需要调整实现调度算法所涉及的参数?
3. 如果基于调度框架设计具体的调度算法?
4. 如果灵活应用链表等数据结构管理进程调度?
大家可带着这些问题进一步阅读后续的内容。

View File

@ -0,0 +1,23 @@
### 3.2 计时器的原理和实现
在传统的操作系统中,计时器是其中一个基础而重要的功能.它提供了基于时间事件的调度机制。在ucore 中timer 中断(irq0)给操作系统提供了有一定间隔的时间事件操作系统将其作为基本的调度和计时单位我们记两次时间中断之间的时间间隔为一个时间片timer splice
基于此时间单位,操作系统得以向上提供基于时间点的事件,并实现基于时间长度的等待和唤醒机制。在每个时钟中断发生时,操作系统产生对应的时间事件。应用程序或者操作系统的其他组件可以以此来构建更复杂和高级的调度。
* sched.h, sched.c 定义了有关timer的各种相关接口来使用 timer 服务,其中主要包括:
* typedef struct {……} timer\_t: 定义了 timer\_t 的基本结构,其可以用 sched.h 中的timer\_init函数对其进行初始化。
* void timer\_init(timer t \*timer, struct proc\_struct \*proc, int expires): 对某计时器 进行初始化,让它在 expires 时间片之后唤醒 proc
进程。
* void add\_timer(timer t \*timer): 向系统添加某个初始化过的timer\_t该计时器在 指定时间后被激活并将对应的进程唤醒至runnable如果当前进程处在等待状态
* void del\_timer(timer\_t \*time): 向系统删除(或者说取消)某一个计时器。该计时器在取消后不会被系统激活并唤醒进程。
* void run\_timer\_list(void): 更新当前系统时间点遍历当前所有处在系统管理内的计时器找出所有应该激活的计数器并激活它们。该过程在且只在每次计时器中断时被调用。在ucore 中,其还会调用调度器事件处理程序。
一个 timer\_t 在系统中的存活周期可以被描述如下:
1. timer\_t 在某个位置被创建和初始化,并通过
add\_timer加入系统管理列表中
2. 系统时间被不断累加,直到 run\_timer\_list 发现该 timer\_t到期。
3. run\_timer\_list更改对应的进程状态并从系统管理列表中移除该timer\_t。
尽管本次实验并不需要填充计时器相关的代码但是作为系统重要的组件同时计时器也是调度器的一个部分你应该了解其相关机制和在ucore中的实现方法。接下来的实验描述将会在一定程度上忽略计时器对调度带来的影响即不考虑基于固定时间点的调度。

View File

@ -0,0 +1,12 @@
### 3.3 进程状态
在此次实验中,进程的状态之间的转换需要有一个更为清晰的表述,在 ucore中runnable的进程会被放在运行队列中。值得注意的是在具体实现中ucore定义的进程控制块struct proc\_struct包含了成员变量state,用于描述进程的运行状态而running和runnable共享同一个状态(state)值(PROC\_RUNNABLE。不同之处在于处于running态的进程不会放在运行队列中。进程的正常生命周期如下
* 进程首先在 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态变成其他状态的进程都要出运行队列反之被放入某个运行队列中。

View File

@ -0,0 +1,36 @@
#### 3.4.1 内核抢占点
调度本质上体现了对CPU资源的抢占。对于用户进程而言由于有中断的产生可以随时打断用户进程的执行转到操作系统内部从而给了操作系统以调度控制权让操作系统可以根据具体情况比如用户进程时间片已经用完了选择其他用户进程执行。这体现了用户进程的可抢占性preemptive。但如果把ucore操作系统也看成是一个特殊的内核进程或多个内核线程的集合那ucore是否也是可抢占的呢其实ucore内核执行是不可抢占的non-preemptive即在执行“任意”内核代码时CPU控制权可被强制剥夺。这里需要注意不是在所有情况下ucore内核执行都是不可抢占的有以下几种“固定”情况是例外
1. 进行同步互斥操作比如争抢一个信号量、锁lab7中会详细分析
2. 进行磁盘读写等耗时的异步操作由于等待完成的耗时太长ucore会调用shcedule让其他就绪进程执行。
这几种情况其实都是由于当前进程所需的某个资源也可称为事件无法得到满足无法继续执行下去从而不得不主动放弃对CPU的控制权。如果参照用户进程任何位置都可被内核打断并放弃CPU控制权的情况这些在内核中放弃CPU控制权的执行地点是“固定”而不是“任意”的不能体现内核任意位置都可抢占性的特点。我们搜寻一下实验五的代码可发现在如下几处地方调用了shedule函数
表一调用进程调度函数schedule的位置和原因
<table>
<tr><td>编号</td><td>位置</td><td>原因</td></tr>
<tr><td>1</td><td>proc.c::do_exit</td><td>用户线程执行结束主动放弃CPU控制权。</td></tr>
<tr><td>2</td><td>proc.c::do_wait</td><td>用户线程等待子进程结束主动放弃CPU控制权。</td></tr>
<tr><td>3</td><td>proc.c::init_main</td><td>1. initproc内核线程等待所有用户进程结束如果没有结束就主动放弃CPU控制权;
2. initproc内核线程在所有用户进程结束后让kswapd内核线程执行10次用于回收空闲内存资源</td></tr>
<tr><td>4</td><td>proc.c::cpu_idle</td><td>idleproc内核线程的工作就是等待有处于就绪态的进程或线程如果有就调用schedule函数</td></tr>
<tr><td>5</td><td>sync.h::lock</td><td>在获取锁的过程中如果无法得到锁则主动放弃CPU控制权</td></tr>
<tr><td>6</td><td>trap.c::trap</td><td>如果在当前进程在用户态被打断去且当前进程控制块的成员变量need_resched设置为1则当前线程会放弃CPU控制权</td></tr>
</table>
仔细分析上述位置第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的实现复杂度会增加不少。

View File

@ -0,0 +1,16 @@
#### 3.4.2 进程切换过程
进程调度函数schedule选择了下一个将占用CPU执行的进程后将调用进程切换从而让新的进程得以执行。通过实验四和实验五的理解应该已经对进程调度和上下文切换有了初步的认识。在实验五中结合调度器框架的设计可对ucore中的进程切换以及堆栈的维护和使用等有更加深刻的认识。假定有两个用户进程在二者进行进程切换的过程中具体的步骤如下
首先在执行某进程A的用户代码时出现了一个 trap (例如是一个外设产生的中断)这个时候就会从进程A的用户态切换到内核态(过程(1))并且保存好进程A的trapframe当内核态处理中断时发现需要进行进程切换时ucore要通过schedule函数选择下一个将占用CPU执行的进程即进程B然后会调用proc\_run函数proc\_run函数进一步调用switch\_to函数切换到进程B的内核态(过程(2))继续进程B上一次在内核态的操作并通过iret指令最终将执行权转交给进程B的用户空间(过程(3))。
当进程B由于某种原因发生中断之后(过程(4))会从进程B的用户态切换到内核态并且保存好进程B的trapframe当内核态处理中断时发现需要进行进程切换时即需要切换到进程Aucore再次切换到进程A(过程(5))会执行进程A上一次在内核调用schedule (具体还要跟踪到 switch\_to 函数)函数返回后的下一行代码这行代码当然还是在进程A的上一次中断处理流程中。最后当进程A的中断处理完毕的时候执行权又会反交给进程A的用户代码(过程(6))。这就是在只有两个进程的情况下,进程切换间的大体流程。
几点需要强调的是:
**a)**
需要透彻理解在进程切换以后程序是从哪里开始执行的需要注意到虽然指令还是同一个cpu上执行但是此时已经是另外一个进程在执行了且使用的资源已经完全不同了。
**b)**
内核在第一个程序运行的时候需要进行哪些操作有了实验四和实验五的经验可以确定内核启动第一个用户进程的过程实际上是从进程启动时的内核状态切换到该用户进程的内核状态的过程而且该用户进程在用户态的起始入口应该是forkret。

View File

@ -0,0 +1,2 @@
### 3.4 进程调度实现

View File

@ -0,0 +1,8 @@
#### 3.5.1 设计思路
实行一个进程调度策略到底需要实现哪些基本功能对应的数据结构首先考虑到一个无论哪种调度算法都需要选择一个就绪进程来占用CPU运行。为此我们可把就绪进程组织起来可用队列双向链表、二叉树、红黑树、数组…等不同的组织方式。
在操作方面如果需要选择一个就绪进程就可以从基于某种组织方式的就绪进程集合中选择出一个进程执行。需要注意这里“选择”和“出”是两个操作选择是在集合中挑选一个“合适”的进程“出”意味着离开就绪进程集合。另外考虑到一个处于运行态的进程还会由于某种原因比如时间片用完了回到就绪态而不能继续占用CPU执行这就会重新进入到就绪进程集合中。这两种情况就形成了调度器相关的三个基本操作在就绪进程集合中选择、进入就绪进程集合和离开就绪进程集合。这三个操作属于调度器的基本操作。
在进程的执行过程中就绪进程的等待时间和执行进程的执行时间是影响调度选择的重要因素这两个因素随着时间的流逝和各种事件的发生在不停地变化比如处于就绪态的进程等待调度的时间在增长处于运行态的进程所消耗的时间片在减少等。这些进程状态变化的情况需要及时让进程调度器知道便于选择更合适的进程执行。所以这种进程变化的情况就形成了调度器相关的一个变化感知操作timer时间事件感知操作。这样在进程运行或等待的过程中调度器可以调整进程控制块中与进程调度相关的属性值比如消耗的时间片、进程优先级等并可能导致对进程组织形式的调整比如以时间片大小的顺序来重排双向链表等并最终可能导致调选择新的进程占用CPU运行。这个操作属于调度器的进程调度属性调整操作。

View File

@ -0,0 +1,66 @@
#### 3.5.2 数据结构
在理解框架之前,需要先了解一下调度器框架所需要的数据结构。
* 通常的操作系统中,进程池是很大的(虽然在 ucore 中MAX\_PROCESS 很小)。在 ucore 中,调度器引入 run-queue简称rq,即运行队列)的概念,通过链表结构管理进程。
* 由于目前 ucore 设计运行在单CPU上其内部只有一个全局的运行队列用来管理系统内全部的进程。
* 运行队列通过链表的形式进行组织。链表的每一个节点是一个list\_entry\_t,每个list\_entry\_t 又对应到了 struct proc\_struct \*,这其间的转换是通过宏 le2proc 来完成 的。具体来说,我们知道在 struct proc\_struct 中有一个叫 run\_link 的 list\_entry\_t因此可以通过偏移量逆向找到对因某个 run\_list的 struct proc\_struct。即进程结构指针 proc = le2proc(链表节点指针, run\_link)。
* 为了保证调度器接口的通用性ucore调度框架定义了如下接口该接口中几乎全部成员变量均为函数指针。具体的功能会在后面的框架说明中介绍。
```
1 struct sched_class {
2 // 调度器的名字
3 const char *name;
4 // 初始化运行队列
5 void (*init) (struct run_queue *rq);
6 // 将进程 p 插入队列 rq
7 void (*enqueue) (struct run_queue *rq, struct proc_struct *p);
8 // 将进程 p 从队列 rq 中删除
9 void (*dequeue) (struct run_queue *rq, struct proc_struct *p);
10 // 返回 运行队列 中下一个可执行的进程
11 struct proc_struct* (*pick_next) (struct run_queue *rq);
12 // timetick 处理函数
13 void (*proc_tick)(struct run_queue* rq, struct proc_struct* p);
14 };
```
* 此外proc.h 中的 struct proc\_struct 中也记录了一些调度相关的信息:
```
1 struct proc_struct {
2 // . . .
3 // 该进程是否需要调度,只对当前进程有效
4 volatile bool need_resched;
5 // 该进程的调度链表结构,该结构内部的连接组成了 运行队列 列表
6 list_entry_t run_link;
7 // 该进程剩余的时间片,只对当前进程有效
8 int time_slice;
9 // round-robin 调度器并不会用到以下成员
10 // 该进程在优先队列中的节点,仅在 LAB6 使用
11 skew_heap_entry_t lab6_run_pool;
12 // 该进程的调度优先级,仅在 LAB6 使用
13 uint32_t lab6_priority;
14 // 该进程的调度步进值,仅在 LAB6 使用
15 uint32_t lab6_stride;
16 };
```
在此次实验中,你需要了解 default\_sched.c中的实现RR调度算法的函数。在该文件中你可以看到ucore 已经为 RR 调度算法创建好了一个名为 RR\_sched\_class 的调度策略类。
通过数据结构 struct run\_queue 来描述完整的 run\_queue运行队列。它的主要结构如下
```
1 struct run_queue {
2 //其运行队列的哨兵结构,可以看作是队列头和尾
3 list_entry_t run_list;
4 //优先队列形式的进程容器,只在 LAB6 中使用
5 skew_heap_entry_t *lab6_run_pool;
6 //表示其内部的进程总数
7 unsigned int proc_num;
8 //每个进程一轮占用的最多时间片
9 int max_time_slice;
10 };
```
在 ucore 框架中运行队列存储的是当前可以调度的进程所以只有状态为runnable的进程才能够进入运行队列。当前正在运行的进程并不会在运行队列中这一点需要注意。

View File

@ -0,0 +1,15 @@
#### 3.5.3 调度点的相关关键函数
虽然进程各种状态变化的原因和导致的调度处理各异但其实仔细观察各个流程的共性部分会发现其中只涉及了三个关键调度相关函数wakup\_proc、shedule、run\_timer\_list。如果我们能够让这三个调度相关函数的实现与具体调度算法无关那么就可以认为ucore实现了一个与调度算法无关的调度框架。
wakeup\_proc函数其实完成了把一个就绪进程放入到就绪进程队列中的工作为此还调用了一个调度类接口函数sched\_class\_enqueue这使得wakeup\_proc的实现与具体调度算法无关。schedule函数完成了与调度框架和调度算法相关三件事情:把当前继续占用CPU执行的运行进程放放入到就绪进程队列中从就绪进程队列中选择一个“合适”就绪进程把这个“合适”的就绪进程从就绪进程队列中摘除。通过调用三个调度类接口函数sched\_class\_enqueue、sched\_class\_pick\_next、sched\_class\_enqueue来使得完成这三件事情与具体的调度算法无关。run\_timer\_list函数在每次timer中断处理过程中被调用从而可用来调用调度算法所需的timer时间事件感知操作调整相关进程的进程调度相关的属性值。通过调用调度类接口函数sched\_class\_proc\_tick使得此操作与具体调度算法无关。
这里涉及了一系列调度类接口函数:
* sched_class_enqueue
* sched_class_dequeue
* sched_class_pick_next
* sched_class_proc_tick
这4个函数的实现其实就是调用某基于sched\_class数据结构的特定调度算法实现的4个指针函数。采用这样的调度类框架后如果我们需要实现一个新的调度算法则我们需要定义一个针对此算法的调度类的实例一个就绪进程队列的组织结构描述就行了其他的事情都可交给调度类框架来完成。

57
lab6/lab6_3_5_4_RR.md Normal file
View File

@ -0,0 +1,57 @@
#### 3.5.4 RR 调度算法实现
RR调度算法的调度思想 是让所有runnable态的进程分时轮流使用CPU时间。RR调度器维护当前runnable进程的有序运行队列。当前进程的时间片用完之后调度器将当前进程放置到运行队列的尾部再从其头部取出进程进行调度。RR调度算法的就绪队列在组织结构上也是一个双向链表只是增加了一个成员变量表明在此就绪进程队列中的最大执行时间片。而且在进程控制块proc\_struct中增加了一个成员变量time\_slice用来记录进程当前的可运行时间片段。这是由于RR调度算法需要考虑执行进程的运行时间不能太长。在每个timer到时的时候操作系统会递减当前执行进程的time\_slice当time\_slice为0时就意味着这个进程运行了一段时间这个时间片段称为进程的时间片需要把CPU让给其他进程执行于是操作系统就需要让此进程重新回到rq的队列尾且重置此进程的时间片为就绪队列的成员变量最大时间片max\_time\_slice值然后再从rq的队列头取出一个新的进程执行。下面来分析一下其调度算法的实现。
RR\_enqueue的函数实现如下表所示。即把某进程的进程控制块指针放入到rq队列末尾且如果进程控制块的时间片为0则需要把它重置为rq成员变量max\_time\_slice。这表示如果进程在当前的执行时间片已经用完需要等到下一次有机会运行时才能再执行一段时间。
```
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 ++;
}
```
RR\_pick\_next的函数实现如下表所示。即选取就绪进程队列rq中的队头队列元素并把队列元素转换成进程控制块指针。
```
static struct proc_struct *
FCFS_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\_dequeue的函数实现如下表所示。即把就绪进程队列rq的进程控制块指针的队列元素删除并把表示就绪进程个数的proc\_num减一。
```
static void
FCFS_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 --;
}
```
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;
}
}
```

View File

@ -0,0 +1,2 @@
### 3.5 调度框架和调度算法

View File

@ -0,0 +1,52 @@
#### 3.6.1 基本思路
**【提示】请先看练习2中提到的论文, 理解后在看下面的内容。**
考察 round-robin 调度器,在假设所有进程都充分使用了其拥有的 CPU 时间资源的情况下,所有进程得到的 CPU 时间应该是相等的。但是有时候我们希望调度器能够更智能地为每个进程分配合理的 CPU 资源。假设我们为不同的进程分配不同的优先级则我们有可能希望每个进程得到的时间资源与他们的优先级成正比关系。Stride调度是基于这种想法的一个较为典型和简单的算法。除了简单易于实现以外它还有如下的特点
* 可控性:如我们之前所希望的,可以证明 Stride Scheduling对进程的调度次数正比于其优先级。
* 确定性:在不考虑计时器事件的情况下,整个调度机制都是可预知和重现的。该算法的基本思想可以考虑如下:
1. 为每个runnable的进程设置一个当前状态stride表示该进程当前的调度权。另外定义其对应的pass值表示对应进程在调度后stride 需要进行的累加值。
2. 每次需要调度时,从当前 runnable 态的进程中选择 stride最小的进程调度。
3. 对于获得调度的进程P将对应的stride加上其对应的步长pass只与进程的优先权有关系
4. 在一段固定的时间之后,回到 2.步骤重新调度当前stride最小的进程。
可以证明,如果令 P.pass =BigStride / P.priority 其中 P.priority 表示进程的优先权(大于 1而 BigStride 表示一个预先定义的大常数,则该调度方案为每个进程分配的时间将与其优先级成正比。证明过程我们在这里略去,有兴趣的同学可以在网上查找相关资料。将该调度器应用到
ucore 的调度器框架中来,则需要将调度器接口实现如下:
* init:
初始化调度器类的信息(如果有的话)。
初始化当前的运行队列为一个空的容器结构。比如和RR调度算法一样初始化为一个有序列表
* enqueue
初始化刚进入运行队列的进程 proc的stride属性。
将 proc插入放入运行队列中去注意这里并不要求放置在队列头部
* dequeue
从运行队列中删除相应的元素。
* pick next
扫描整个运行队列返回其中stride值最小的对应进程。
更新对应进程的stride值即pass = BIG\_STRIDE / P-\>priority; P-\>stride += pass。
* proc tick:
检测当前进程是否已用完分配的时间片。如果时间片用完,应该正确设置进程结构的相关标记来引起进程切换。
一个 process 最多可以连续运行 rq.max\_time\_slice个时间片。
在具体实现时有一个需要注意的地方stride属性的溢出问题在之前的实现里面我们并没有考虑 stride 的数值范围,而这个值在理论上是不断增加的,在
stride溢出以后基于stride的比较可能会出现错误。比如假设当前存在两个进程A和Bstride属性采用16位无符号整数进行存储。当前队列中元素如下假设当前运行的进程已经被重新放置进运行队列中
![image](../lab6_figs/image001.png)
此时应该选择 A 作为调度的进程,而在一轮调度后,队列将如下:
![image](../lab6_figs/image002.png)
可以看到由于溢出的出现进程间stride的理论比较和实际比较结果出现了偏差。我们首先在理论上分析这个问题令PASS\_MAX为当前所有进程里最大的步进值。则我们可以证明如下结论对每次Stride调度器的调度步骤中有其最大的步进值STRIDE\_MAX和最小的步进值STRIDE\_MIN
之差:
STRIDE\_MAX STRIDE\_MIN <= PASS\_MAX
提问 1如何证明该结论 有了该结论在加上之前对优先级有Priority \> 1限制我们有STRIDE\_MAX STRIDE\_MIN <= BIG\_STRIDE,于是我们只要将BigStride取在某个范围之内即可保证对于任意两个 Stride 之差都会在机器整数表示的范围之内。而我们可以通过其与0的比较结构来得到两个
Stride的大小关系。在上例中虽然在直接的数值表示上 98 < 65535但是 98 - 65535 的结果用带符号的 16位整数表示的结果为99,与理论值之差相等所以在这个意义下 98 \> 65535
基于这种特殊考虑的比较方法即便Stride有可能溢出我们仍能够得到理论上的当前最小Stride并做出正确的调度决定。
提问 2在 ucore 中目前Stride是采用无符号的32位整数表示。则BigStride应该取多少才能保证比较的正确性

View File

@ -0,0 +1,56 @@
#### 3.6.2 使用优先队列实现 Stride Scheduling
在上述的实现描述中对于每一次pick\_next函数我们都需要完整地扫描来获得当前最小的stride及其进程。这在进程非常多的时候是非常耗时和低效的有兴趣的同学可以在实现了基于列表扫描的Stride调度器之后比较一下priority程序在Round-Robin及Stride调度器下各自的运行时间。考虑到其调度选择于优先队列的抽象逻辑一致我们考虑使用优化的优先队列数据结构实现该调度。
优先队列是这样一种数据结构:使用者可以快速的插入和删除队列中的元素,并且在预先指定的顺序下快速取得当前在队列中的最小(或者最大)值及其对应元素。可以看到,这样的数据结构非常符合 Stride 调度器的实现。
本次实验提供了libs/skew\_heap.h
作为优先队列的一个实现,该实现定义相关的结构和接口,其中主要包括:
```
1 // 优先队列节点的结构
2 typedef struct skew_heap_entry skew_heap_entry_t;
3 // 初始化一个队列节点
4 void skew_heap_init(skew_heap_entry_t *a);
5 // 将节点 b 插入至以节点 a 为队列头的队列中去,返回插入后的队列
6 skew_heap_entry_t *skew_heap_insert(skew_heap_entry_t *a,
7 skew_heap_entry_t *b,
8 compare_f comp);
9 // 将节点 b 插入从以节点 a 为队列头的队列中去,返回删除后的队列
10 skew_heap_entry_t *skew_heap_remove(skew_heap_entry_t *a,
11 skew_heap_entry_t *b,
12 compare_f comp);
```
其中优先队列的顺序是由比较函数comp决定的sched\_stride.c中提供了proc\_stride\_comp\_f比较器用来比较两个stride的大小你可以直接使用它。当使用优先队列作为Stride调度器的实现方式之后运行队列结构也需要作相关改变其中包括
* struct
run\_queue中的lab6\_run\_pool指针在使用优先队列的实现中表示当前优先队列的头元素如果优先队列为空则其指向空指针NULL
* struct
proc\_struct中的lab6\_run\_pool结构表示当前进程对应的优先队列节点。本次实验已经修改了系统相关部分的代码使得其能够很好地适应LAB6新加入的数据结构和接口。而在实验中我们需要做的是用优先队列实现一个正确和高效的Stride调度器如果用较简略的伪代码描述则有
* init(rq):
Initialize rq-\>run\_list
Set rq-\>lab6\_run\_pool to NULL
Set rq-\>proc\_num to 0
* enqueue(rq, proc)
Initialize proc-\>time\_slice
Insert proc-\>lab6\_run\_pool into rq-\>lab6\_run\_pool
rq-\>proc\_num ++
* dequeue(rq, proc)
Remove proc-\>lab6\_run\_pool from rq-\>lab6\_run\_pool
rq-\>proc\_num --
* pick\_next(rq)
If rq-\>lab6\_run\_pool == NULL, return NULL
Find the proc corresponding to the pointer rq-\>lab6\_run\_pool
proc-\>lab6\_stride += BIG\_STRIDE / proc-\>lab6\_priority
Return proc
* proc\_tick(rq, proc):
If proc-\>time\_slice \> 0, proc-\>time\_slice --
If proc-\>time\_slice == 0, set the flag proc-\>need\_resched

View File

@ -0,0 +1,2 @@
### 3.6 Stride Scheduling

View File

@ -0,0 +1,2 @@
## 3 调度框架和调度算法设计与实现

View File

@ -0,0 +1,37 @@
## 4 实验报告要求
从网站上下载lab6.zip后解压得到本文档和代码目录lab6完成实验中的各个练习。完成代码编写并检查无误后在对应目录下执行 make handin 任务即会自动生成lab6-handin.tar.gz。最后请一定提前或按时提交到网络学堂上。
注意有“LAB6”的注释主要是修改default\_sched\_swide\_c中的内容。代码中所有需要完成的地方challenge除外都有“LAB6”和“YOUR CODE”的注释请在提交时特别注意保持注释并将“YOUR CODE”替换为自己的学号并且将所有标有对应注释的部分填上正确的代码。
## 附录:执行 priority大致的显示输出
```
$ make run-priority
......
check_swap() succeeded!
++ setup timer interrupts
kernel_execve: pid = 2, name = "priority".
main: fork ok,now need to wait pids.
child pid 7, acc 2492000, time 2001
child pid 6, acc 1944000, time 2001
child pid 4, acc 960000, time 2002
child pid 5, acc 1488000, time 2003
child pid 3, acc 540000, time 2004
main: pid 3, acc 540000, time 2004
main: pid 4, acc 960000, time 2004
main: pid 5, acc 1488000, time 2004
main: pid 6, acc 1944000, time 2004
main: pid 7, acc 2492000, time 2004
main: wait pids over
stride sched correct result: 1 2 3 4 5
all user-mode processes have quit.
init check memory pass.
kernel panic at kern/process/proc.c:426:
initproc exit.
Welcome to the kernel debug monitor!!
Type 'help' for a list of commands.
K>
```

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
lab7/.DS_Store vendored

Binary file not shown.

BIN
lab8/.DS_Store vendored

Binary file not shown.