上一篇: Part 1-xv6 调度代码讲解
TODO2: 统计RR调度情况
实现waitSch系统调用
todo2需要增加一个系统调用waitSch(int* rutime, int* retime, int* sltime)
作为原来wait
的扩展,除了执行原有功能以外,要将进程的运行时间、在等待队列中的时间和休眠时间输入到三个参数所代表的地址中。
首先我们将需要维护的数据结构定义在proc.h
中:
1 |
|
然后,在proc.c
中,也应该做出相关的修改。首先在进程被初始化的时候,应该将p->ctime
设置为当时的时钟ticks,然后其他的几个变量应该初始化为0。考虑到第一个用户进程并不是由fork
产生,我们将初始化放在allocproc
中:
1 |
|
相应地,当进程结束,资源要被回收时,应该将这几个变量清空。我们不修改在exit
中,因为此时这些进程运行的统计数据仍然要保留,直到父进程调用wait
找到它,将它回收的时候,再把这些数据处理完清空。在wait
中:
1 |
|
接下来是实现waitSch
,waitSch
的大部分逻辑都和wait
一样,不同的只是要先把p->rutime
赋给*rutime
,把p->retime
赋给*retime
, 以及p->sltime
→*sltime
,然后再把这些变量清空:
1 |
|
现在的问题是,如何动态地维护进程的运行时间、等待时间和休眠时间呢?
首先一个直观的思路是,在每一次进程状态变化的时候,例如由A→B,记录下这次状态转换的时间作为B状态的开始时间$t_{0}$, 等下一次再从B→C的时候,B的持续时间就加上这个时刻减去$t_{0}$. 但是这种方法实现起来非常复杂,首先进程状态转换可能在很多种情况下发生,每一次转换时,都要判断旧的状态和新的状态是什么,然后更新旧状态的持续时间,保存新状态的开始时间。这样代码会变得冗长而且容易出错,还需要为进程维护两倍的变量(对每一种状态,例如RUNNING, 至少需要维护最近一次RUNNING状态开始的时刻,以及累计运行时间)。
另一种思路是,在每一次收到时钟中断时,判断进程处于何种状态,为这种状态的持续时间加一。这样的计算是一种很粗糙的近似。这样计算,是假设进程会将这种状态维持一个时间片,在中间不发生状态的变换,并假设这种状态大概在时钟中断发生时开始。然而,在两个时钟中断之间,进程是很有可能调用sleep
或wait
进行休眠的,也有可能在下一个时钟中断到来之前,调度器就已经调度了另一个进程并将该进程唤醒,状态变为RUNNABLE,这样,中间的sleep阶段就没有被我们的计算捕捉到。不过因为进程一般是运行和等待的时间占大多数,sleep占比很少,运行和等待之间的切换又一般是通过时钟中断引发的yield
, 所以这种近似还是可以接受的。
xv6 的时钟机制
xv6的时钟在timer.c
中实现,每过100ms,硬件就会产生一个时钟中断。每个cpu都可以独立地接收时钟中断,并陷入中断处理。
在trap.c
中定义了一个uint类型的变量ticks
,每一次这个变量加一,代表系统的时钟往前走了一步。两个cpu收到时钟中断后,先后进入中断处理程序,为了让系统的时钟(ticks)能够真正在每次timer
产生时钟中断的时候自增一,只在某一个固定的cpu收到中断时让ticks++
, 因此xv6中每一次CPU0收到一个时钟中断,ticks
就自增1:
1 |
|
编译这段代码,输出是:
这段输出中,每一次cpu id 为0时,对应的时钟比上一次printf增加一,而对于cpu1则不一定是这样。
如果我们要在每次时钟中断时判断进程的状态,也有两种思路。
第一个是,在时钟中断的时候,调用myproc
先判断cpu上是否有进程在运行,如果有,则判断它的状态并更新变量。这种思路首先因为进程会在两个cpu上调度,而在cpu1上调度时,两次中断之间ticks
不一定有增加,所以要增加一个lasttick
保存上一次中断时ticks
的值,如果这个值有改变,才会进行判断。但即使是添加了这个逻辑,这种方法也是错误的,因为如果myproc()
能返回进程的PCB, 进程一定是处于RUNNING
状态(否则它就不会被cpu调度了),这样我们无法知道进程何时在休眠或等待。
第二个思路是,在ticks++
的时候,遍历整一个ptable表,对每个进程判断它是在何种状态,然后给变量+1.这样便可实现上面那种近似的算法。 在trap.c
中作以下修改:
1 |
|
其中,判断进程状态并更新变量的函数在proc.c
中实现:
1 |
|
我们按照上次实验的步骤将waitSch
包装成系统调用。 实现过程中比较重要的是,xv6syscall.c
中系统调用函数的统一格式是返回值int
, 不允许带参数。如果需要加入参数,则参数会被压入栈中,通过argint
和argptr
从栈上取出参数进行解析。在sysproc.c
中包装waitSch
如下:
1 |
|
在syscall.h
以及系统调用表中,为waitSch
增加系统调用号,并在user.h
中定义接口。waitSch
的实现已经完成。
编写RRsta.c 统计RR调度情况
编写RRsta.c, main函数接收一个命令行参数n, fork n个子进程进行相同的大规模计算,然后父进程调用waitSch
,等待每个子进程的结束,并输出每个子进程的运行时间、等待时间和休眠时间。最后统计n个进程在RR(Round Robin)调度下的平均轮转时间。
1 |
|
将RRsta.c
编译到qemu
中,在shell中运行
1 |
|
得到输出:
(实际上,手动计算平均周转时间的话会得到5.8, 但是xv6的printf
并没有使用C的标准输出库,不支持输出浮点数,只能将小数省去了。