1、08010314 计科3班 王操HUNAN UNIVERSITY操作系统实验报告题 目:调度器学生姓名:wc学生学号:201308010314专业班级:计科1303同组成员:上课老师:肖德贵目 录一、内容 2二、目的 2三、实验设计思想和流程 2四、主要数据结构及符号说明 2五、程序初值及运行结果 2六、实验体会和思考题 2附录(源代码及注释) 2一、内容实验五完成了用户进程的管理,可在用户态运行多个进程。但到目前为止,采用的调度策略是很简单的FIFO调度策略。本次实验,主要是熟悉ucore的系统调度器框架,以及基于此框架的Round-Robin(RR) 调度算法。然后参考RR调度算法的实现,
2、完成Stride Scheduling调度算法。二、目的理解操作系统的调度管理机制熟悉 ucore 的系统调度器框架,以及缺省的Round-Robin 调度算法基于调度器框架实现一个(Stride Scheduling)调度算法来替换缺省的调度算法三、实验设计思想和流程实行一个进程调度策略,到底需要实现哪些基本功能对应的数据结构?首先考虑到一个无论哪种调度算法都需要选择一个就绪进程来占用CPU运行。为此我们可把就绪进程组织起来,可用队列(双向链表)、二叉树、红黑树、数组等不同的组织方式。在操作方面,如果需要选择一个就绪进程,就可以从基于某种组织方式的就绪进程集合中选择出一个进程执行。需要注意,
3、这里“选择”和“出”是两个操作,选择是在集合中挑选一个“合适”的进程,“出”意味着离开就绪进程集合。另外考虑到一个处于运行态的进程还会由于某种原因(比如时间片用完了)回到就绪态而不能继续占用CPU执行,这就会重新进入到就绪进程集合中。这两种情况就形成了调度器相关的三个基本操作:在就绪进程集合中选择、进入就绪进程集合和离开就绪进程集合。这三个操作属于调度器的基本操作。在进程的执行过程中,就绪进程的等待时间和执行进程的执行时间影响调度选择的重要因素,这两个因素随着时间的流逝和各种事件的发生在不停地变化,比如处于就绪态的进程等待调度的时间在增长,处于运行态的进程所消耗的时间片在减少等。这些进程状态变
4、化的情况需要及时让进程调度器知道,便于选择更合适的进程执行。所以这种进程变化的情况就形成了调度器相关的一个变化感知操作:timer时间事件感知操作。这样在进程运行或等待的过程中,调度器可以调整进程控制块中与进程调度相关的属性值(比如消耗的时间片、进程优先级等)并可能导致对进程组织形式的调整(比如以时间片大小的顺序来重排双向链表等),并最终可能导致调选择新的进程占用CPU运行。这个操作属于调度器的进程调度属性调整操作。考察 round-robin 调度器,在假设所有进程都充分使用了其拥有的 CPU 时间资源的情况下,所有进程得到的 CPU 时间应该是相等的。但是有时候我们希望调度器能够更智能地为
5、每个进程分配合理的 CPU 资源。假设我们为不同的进程分配不同的优先级,则我们有可能希望每个进程得到的时间资源与他们的优先级成正比关系。Stride调度是基于这种想法的一个较为典型和简单的算法。除了简单易于实现以外,它还有如下的特点:可控性:如我们之前所希望的,可以证明 Stride Scheduling对进程的调度次数正比于其优先级。确定性:在不考虑计时器事件的情况下,整个调度机制都是可预知和重现的。该算法的基本思想可以考虑如下:1. 为每个runnable的进程设置一个当前状态stride,表示该进程当前的调度权。另外定义其对应的pass值,表示对应进程在调度后,stride 需要进行的累
6、加值。2. 每次需要调度时,从当前 runnable 态的进程中选择 stride最小的进程调度。3. 对于获得调度的进程P,将对应的stride加上其对应的步长pass(只与进程的优先权有关系)。4. 在一段固定的时间之后,回到 2.步骤,重新调度当前stride最小的进程。可以证明,如果令 P.pass =BigStride / P.priority 其中 P.priority 表示进程的优先权(大于 1),而 BigStride 表示一个预先定义的大常数,则该调度方案为每个进程分配的时间将与其优先级成正比。证明过程我们在这里略去,有兴趣的同学可以在网上查找相关资料。将该调度器应用到 uc
7、ore 的调度器框架中来,则需要将调度器接口实现如下:init: 初始化调度器类的信息(如果有的话)。 初始化当前的运行队列为一个空的容器结构。(比如和RR调度算法一样,初始化为一个有序列表)enqueue 初始化刚进入运行队列的进程 proc的stride属性。 将 proc插入放入运行队列中去(注意:这里并不要求放置在队列头部)。dequeue 从运行队列中删除相应的元素。pick next 扫描整个运行队列,返回其中stride值最小的对应进程。 更新对应进程的stride值,即pass = BIG_STRIDE / P-priority; P-stride += pass。proc t
8、ick: 检测当前进程是否已用完分配的时间片。如果时间片用完,应该正确设置进程结构的相关标记来引起进程切换。 一个 process 最多可以连续运行 rq.max_time_slice个时间片。在具体实现时,有一个需要注意的地方:stride属性的溢出问题,在之前的实现里面我们并没有考虑 stride 的数值范围,而这个值在理论上是不断增加的,在 stride溢出以后,基于stride的比较可能会出现错误。比如假设当前存在两个进程A和B,stride属性采用16位无符号整数进行存储。当前队列中元素如下(假设当前运行的进程已经被重新放置进运行队列中):使用优先队列实现 Stride Schedu
9、ling可以看到由于溢出的出现,进程间stride的理论比较和实际比较结果出现了偏差。我们首先在理论上分析这个问题:令PASS_MAX为当前所有进程里最大的步进值。则我们可以证明如下结论:对每次Stride调度器的调度步骤中,有其最大的步进值STRIDE_MAX和最小的步进值STRIDE_MIN之差:STRIDE_MAX STRIDE_MIN 1限制,我们有STRIDE_MAX STRIDE_MIN = BIG_STRIDE,于是我们只要将BigStride取在某个范围之内,即可保证对于任意两个 Stride 之差都会在机器整数表示的范围之内。而我们可以通过其与0的比较结构,来得到两个Stri
10、de的大小关系。在上例中,虽然在直接的数值表示上 98 65535。基于这种特殊考虑的比较方法,即便Stride有可能溢出,我们仍能够得到理论上的当前最小Stride,并做出正确的调度决定。提问 2:在 ucore 中,目前Stride是采用无符号的32位整数表示。则BigStride应该取多少,才能保证比较的正确性?在上述的实现描述中,对于每一次pick_next函数,我们都需要完整地扫描来获得当前最小的stride及其进程。这在进程非常多的时候是非常耗时和低效的,有兴趣的同学可以在实现了基于列表扫描的Stride调度器之后比较一下priority程序在Round-Robin及Stride调
11、度器下各自的运行时间。考虑到其调度选择于优先队列的抽象逻辑一致,我们考虑使用优化的优先队列数据结构实现该调度。优先队列是这样一种数据结构:使用者可以快速的插入和删除队列中的元素,并且在预先指定的顺序下快速取得当前在队列中的最小(或者最大)值及其对应元素。可以看到,这样的数据结构非常符合 Stride 调度器的实现。本次实验提供了libs/skew_heap.h 作为优先队列的一个实现,该实现定义相关的结构和接口,其中主要包括:1 / 优先队列节点的结构2 typedef struct skew_heap_entry skew_heap_entry_t;3 / 初始化一个队列节点4 void s
12、kew_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 com
13、pare_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结构,表示当前进程对应的优先队列节点。本次实验已经修改了系统相关部分的代码,使得
14、其能够很好地适应LAB6新加入的数据结构和接口。而在实验中我们需要做的是用优先队列实现一个正确和高效的Stride调度器,如果用较简略的伪代码描述,则有:init(rq): Initialize rq-run_list Set rq-lab6_run_pool to NULL Set rq-proc_num to 0enqueue(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_
15、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 procproc_tick(rq, proc): If proc-time_slice 0, proc-time_slice - If proc-time_slice
16、 = 0, set the flag proc-need_resched四、主要数据结构及符号说明在理解框架之前,需要先了解一下调度器框架所需要的数据结构。通常的操作系统中,进程池是很大的(虽然在 ucore 中,MAX_PROCESS 很小)。在 ucore 中,调度器引入 runqueue(简称rq,即运行队列)的概念,通过链表结构管理进程。由于目前 ucore 设计运行在单CPU上,其内部只有一个全局的运行队列,用来管理系统内全部的进程。运行队列通过链表的形式进行组织。链表的每一个节点是一个list_entry_t,每个list_entry_t 又对应到了 struct proc_str
17、uct*,这其间的转换是通过宏 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 /
18、初始化运行队列5 void (*init) (struct run_queue *rq);6 / 将进程 p 插入队列 rq7 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 / tim
19、etick 处理函数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_s
20、lice;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 的调度策略类
21、。通过数据结构 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 /每个进程一轮占用的最多时间片uCore Lab Documents9 int max_time_slice;10 ;在 ucore 框架中,运行队列存
22、储的是当前可以调度的进程,所以,只有状态为runnable的进程才能够进入运行队列。当前正在运行的进程并不会在运行队列中,这一点需要注意。五、程序初值及运行结果六、实验体会和思考题这次实验并没有做的很好,首先是实验之前我没有认真做好准备,一些函数名我也不知道是什么意思。尤其是default_sched.cpp文件中的stride_pick_next(struct run_queue *rq)函数,我觉得这是这个文件中的所有函数最难搞明白的一个。其次,由于我之前在学习C语言和C+时没有很认真,导致我对数据结构这一块掌握不太好,我在看一些函数的时候感觉很吃力。但是我从这次实验还是收货良多。一是搞清
23、楚了计算机调度进程的方法。其实就是一个选择,入队,出队的过程。进程很多,所以要选择优先级最高的进行调度。二是搞清楚了函数assert的用法。assert宏的原型定义在中,其作用是如果它的条件返回错误,则终止程序执行。在本实验中多次用到了assert函数。比如在入队函数中调用了assert(list_empty(&(proc-run_link);这是判断运行队列是否为空的函数,如果为空的话就继续否则终止运行。三是搞清楚了struct的用法,在操作系统中函数体用的很多。总之希望第四次实验能第一个验收。附录(源代码及注释)附:default_sched.cpp文件#include #include
24、#include #include #include #define USE_SKEW_HEAP 1/* You should define the BigStride constant here*/* LAB6: YOUR CODE */#define BIG_STRIDE 0x7FFFFFFF /* ? */实现对进程的比较。其实是比较步数,如果进程p的tride大就返回1,一样大返回0,如果p的小则返回-1./* The compare function for two skew_heap_node_ts and the * corresponding procs*/static int
25、proc_stride_comp_f(void *a, void *b) struct proc_struct *p = le2proc(a, lab6_run_pool); 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;/*将运行列表的数列为0,进程数为0,运行池的数目为0. * stride_init initializes the
26、 run-queue rq with correct assignment for * member variables, including: * * - run_list: should be a empty list after initialization. * - lab6_run_pool: NULL * - proc_num: 0 指的是进程数为0. * - max_time_slice: no need here, the variable would be assigned by the caller. * * hint: see proj13.1/libs/list.h f
27、or routines of the list structures. */static voidstride_init(struct run_queue *rq) /* LAB6: YOUR CODE */ list_init(&(rq-run_list); rq-lab6_run_pool = NULL; rq-proc_num = 0;/*在进程控制块proc_struct中增加了一个成员变量time_slice,用来记录进程当前的可运行时间片段。这是由于RR调度算法需要考虑执行进程的运行时间不能太长。在每个timer到时的时候,操作系统会递减当前执行进程的time_slice,当tim
28、e_slice为0时,就意味着这个进程运行了一段时间(这个时间片段称为进程的时间片),需要把CPU让给其他进程执行,于是操作系统就需要让此进程重新回到rq的队列尾,且重置此进程的时间片为就绪队列的成员变量最大时间片max_time_slice值,然后再从rq的队列头取出一个新的进程执行。下面来分析一下其调度算法的实现。把某进程的进程控制块指针放入到rq队列末尾,且如果进程控制块的时间片为0,则需要把它重置为rq成员变量max_time_slice。这表示如果进程在当前的执行时间片已经用完,需要等到下一次有机会运行时,才能再执行一段时间。初始化刚进入运行队列的进程 proc的stride属性。
29、将 proc插入放入运行队列中去(注意:这里并不要求放置在队列头部)。*/ * stride_enqueue inserts the process proc into the run-queue * rq. The procedure should verify/initialize the relevant members * of proc, and then put the lab6_run_pool node into the * queue(since we use priority queue here). The procedure should also * update t
30、he meta date in rq structure. * * proc-time_slice denotes the time slices allocation for the * process, which should set to rq-max_time_slice. * * hint: see proj13.1/libs/skew_heap.h for routines of the priority * queue structures. */static voidstride_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_li
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1