实验目的
(1)、掌握计算机操作系统管理进程、处理机、存储器、文件系统的基本方法。
(2)、了解进程的创建、撤消和运行,进程并发执行;自行设计解决哲学家就餐问题的并发线程,了解线程(进程)调度方法;掌握内存空间的分配与回收的基本原理;通过模拟文件管理的工作过程,了解文件操作命令的实质。
(3)、了解现代计算机操作系统的工作原理,具有初步分析、设计操作系统的能力。
(4)、通过在计算机上编程实现操作系统中的各种管理功能,在系统程序设计能力方面得到提升。
实验要求
(1)题目1:
试解释一下yield函数、scheduler函数和sched函数的用途。
结合书本,确定xv6使用的是哪种调度算法。给出你的理由(通过分析代码证明你的观点)。
(proc.c L268) 有一个疑问,似乎每次xv6都是从进程表开头开始查找Runnable的进程。如果刚从CPU切换下来的进程恰好是进程表的第一个PCB,会不会调度器永远都选择它进行调度?
(2)题目2:
在xv6界面上按Ctrl+P,你会看到终端上会显示当前系统中的进程。
仿照相关代码,实现以下功能:在xv6界面上按Ctrl+R,打印出当前系统中sleeping的进程,并确定它们对应的等待队列是什么(chan/waiting channel)。
实验内容
题目1:
试解释一下yield函数、scheduler函数和sched函数的用途。
结合书本,确定xv6使用的是哪种调度算法。给出你的理由(通过分析代码证明你的观点)。
(proc.c L268) 有一个疑问,似乎每次xv6都是从进程表开头开始查找Runnable的进程。如果刚从CPU切换下来的进程恰好是进程表的第一个PCB,会不会调度器永远都选择它进行调度?
yield函数、scheduler函数和sched函数都是xv6操作系统中proc.c文件下用于实现进程调度的函数。
yield函数
当一个进程使用完时间片后,会中断并调用yield函数来让出CPU给新的进程。
yield函数首先获取进程表锁,并将进程状态设为可运行,以便下次遍历时可以被唤醒。
之后执行sched函数,准备将CPU切换到scheduler context。最后释放进程表锁。
sched函数
sched()是切换至CPU context,并在切换context之前,进行一系列判断,以避免出现冲突的函数。
切换到scheduler必须:
1.持有p->lock并且已更改proc->state。
2.保存和恢复intena,因为intena是这个内核线程的属性,而不是这个CPU的属性。
检查完后调用scheduler()。
scheduler函数
scheduler函数是xv6中的核心调度器函数,用于根据选择的调度算法从就绪队列中选择下一个要运行的进程。
scheduler函数的实现方式取决于所选的调度算法。
当CPU初始化之后,即调用scheduler(),循环从进程队列中选择一个进程执行。
计划程序永远不会返回。它循环执行以下操作:
1.选择要运行的进程。
2.swtch开始运行该进程。
3.最终该进程会转移控制权,通过swtch返回到调度程序。
问1:调度算法
为了确定采用了哪种调度算法,我们需要分析scheduler函数,代码如下:
首先,定义一个指向当前CPU结构的指针c,用于跟踪当前CPU上正在运行的进程。然后,将c->proc设置为0,以表示当前没有进程在运行。
在每次执行一个进程之前,开启CPU的中断。通过确保设备可以中断,来避免死锁。
循环遍历存放进程的proc数组,从中选择状态为RUNNABLE的进程来运行。
当找到一个RUNNABLE的进程时,首先获得该进程的锁,以确保它的状态不会在此期间被其他进程修改。
然后,将该进程的状态设置为RUNNING,表示它正在运行。
将该进程的指针赋值给c->proc,表示该进程正在当前CPU上运行。
通过调用swtch函数,将CPU的上下文切换到该进程的上下文,swtch函数负责保存当前CPU的上下文,并将控制权转移到指定进程的上下文。
当该进程运行结束并返回时, c->proc将被设置为0表示当前没有进程在运行,并且释放锁。
这是一个简单的时间轮转调度算法。它遍历所有的进程,并按顺序选择每个RUNNABLE进程运行一定时间,然后切换到下一个RUNNABLE进程。如果进程的运行时间达到了一定的限制,也会强制切换到下一个进程。
问2:调度问题
(proc.c L268) 有一个疑问,似乎每次xv6都是从进程表开头开始查找Runnable的进程。如果刚从CPU切换下来的进程恰好是进程表的第一个PCB,会不会调度器永远都选择它进行调度?
不会,由前面for循环部分的代码可知,在执行完第一个进程后,并不会跳出for循环,而是接着遍历完整个进程表,寻找下一个RUNAABLE状态,故不会出现这种情况。
题目2:
在xv6界面上按Ctrl+P,你会看到终端上会显示当前系统中的进程。
仿照相关代码,实现以下功能:在xv6界面上按Ctrl+R,打印出当前系统中sleeping的进程,并确定它们对应的等待队列是什么(chan/waiting channel)。
首先找到这个代码的出处,在 xv6 的 console.c 文件中,函数名为 consoleintr(),如下图:
为了实现这个功能,我们首先要识别我们输入了Ctrl+R,因此,增加一栏case,如下图所示:
其中print_sleeping_procs()是我们实现这个功能的函数
其中我们可以在proc.c文件中找到procdump()函数,故我们仿照其在proc.c文件中添加procdump_sleeping函数,遍历整个进程表,找到所有state为SLEEPING的进程,并将它们的信息打印出来。
在proc.h中找到进程块的代码
我们发现,如果为SLEEPING状态,那么等待队列的名字会保存在chan上,因此我们如果想知道对应的等待队列,只需输出他的chan即可。
print_sleeping_procs函数代码如下图所示
遍历每一个进程,若状态为SLEEPING,则输出他的chan。
在defs.h中添加函数声明
Ctrl+S保存后在xv6目录下输入make qemu编译运行,按下ctrl+p,结果如下:
那么按下Ctrl+R后显示的结果就应该包含这两个进程,即:
发现没有输出等待队列,上网查询资料得知:
在xv6中,如果一个进程处于睡眠状态,它的chan成员变量会被设置为一个标识睡眠原因的字符串。如果进程不是因为睡眠而阻塞,chan会被设置为空字符串。
因此,如果打印的进程的chan是空字符串,说明该进程当前不是因为睡眠而阻塞。
将结果强行转换:
这时编译运行后结果为:
实验小结
通过本次实验,对xv6的调度算法有了一定理解,结合最近的课程,更加熟悉了调度算法。在实验过程中遇到了一些问题,比如一开始ctrl+p无法打印,原因是与vscode快捷键冲突;又比如print_sleeping_procs函数编写后一直无法成功编译,查阅资料后得知需要在defs.h中添加声明;再还有就是实现函数后输出与预测不同的问题,通过强行转换结果后成功输出。
(by 归忆)