nachos 3.4 实现抢占式多级队列反馈算法

今天开始进行文件系统的实验,就把线程部分最后一个稍有难度的练习给贴上来吧。

老实说,这个练习应该是这次实验中最难的一个练习了。因为要实现多级队列反馈调度算法,就必须利用到时间片和中断机制,而这在刚才的实验中完全不需要考虑。尤其是中断机制,应该算是整个线程模块最难的一部分了,搞懂了这部分,基本上线程模块的整个运行流程就十分清晰了,所以下面我先分析下这部分代码,然后再描述我是如何通过修改原有的代码实现多级队列反馈调度算法的。

提到多级队列反馈调度算法,首先想到的就是时间片机制了,还好nachos已经为我们实现好了一个时间片的类,就是Timer类。Timer类实现了每隔一定的时间向Interrupt发送一个中断,从Timer这个类的构造函数开始就在执行这个过程了。Timer类的第一个关键语句在构造函数中:

// schedule the first interrupt from the timer device interrupt->Schedule(TimerHandler, (int) this, TimeOfNextInterrupt(), TimerInt);  

这里表示加入一个中断,这个中断的处理函数是TimerHandler,参数是自己,发生的时间是TimeOfNextInterrupt(),类型是一个时钟中断,其中处理函数又是一个非常重要的函数:

static void TimerHandler(int arg) { Timer *p = (Timer *)arg; p->TimerExpired(); }  

它表示将参数还原成Timer对象,然后调用TimerTimerExpired函数,那么再来看看这个函数:

void Timer::TimerExpired() { // schedule the next timer device interrupt interrupt->Schedule(TimerHandler, (int) this, TimeOfNextInterrupt(), TimerInt); // invoke the Nachos interrupt handler for this device (*handler)(arg); }  

这个函数又加入了一个了刚才一模一样的中断,这样的话就可以实现每隔一段时间就发一个类似的中断,然后再执行中断处理函数,这个中断处理函数是在构造函数中赋值的,专门用来处理时钟中断,下面会提到。这里我们可能会疑问,为什么它不直接将TimerExpired作为参数传入呢,非要绕一个弯子呢,其实这是因为C++不允许将类的成员函数作为函数指针进行传递,所以需要借助一个静态函数

弄清楚了Timer类的发中断的过程,就应该想,这个中断处理程序到底是什么呢?通过SourceInsight的搜索机制,可在System.cc中找到timer的初始化语句:

if (randomYield) // start the timer (if needed) timer = new Timer(TimerInterruptHandler, 0, randomYield);  

看到这句就明白了为什么中断没有启动,因为我使用参数,但是很显然我们这里需要使用固定时间片的时钟中断,所以后来修改的时候会在这里添加一句else语句,并将randomYield。这会,先关注TimerInterruptHandler这个函数,这个函数就是处理时钟中断的,继续搜索可以看到:

static void TimerInterruptHandler(int dummy) { if (interrupt->getStatus() != IdleMode) interrupt->YieldOnReturn(); }  

这个函数只有一句话,就是非空闲态调用InterruptYieldOnReturn函数,而找到YieldOnReturn函数就会发现同样只有一句话,将Interrupt的一个变量yieldOnReturn赋值为TRUE,到这里就没有了,那么这个变量yieldOnReturn是如果生效的呢?我们可以按照上面的方法搜索所有引用了yieldOnReturn的地方,其中最“可疑”的就是在OneTick函数中,有一句:

if (yieldOnReturn) { // if the timer device handler asked for a context switch, ok to do it now yieldOnReturn = FALSE; status = SystemMode; // yield is a kernel routine currentThread->Yield(); status = old; }  

看注释就很明白了,这里真正落实了时钟中断的效果,就是将当前正在运行的线程赶下CPU,然后从就绪队列中选出一个新的线程来执行,这样就实现了时间片的轮转效果。如果大家有兴趣,可以从OneTick函数的注释看到,这个函数是在中断被恢复的时候被调用的(在SetLevel这个函数中),这个设计是非常符合逻辑的,因为在屏蔽中断期间,不应该允许任何中断的发生或者线程的调度,这样才能模拟原子操作,同时一次OneTick函数的执行也为模拟的系统时间(这里我们的讨论都是在系统态下)增加了10个单位的时间,记住这个时间可以在接下来对我们实现时间片的调度有帮助。当然,还有一个机制需要了解,那就是时钟中断处理程序是在什么时候被执行的,这个也是在OneTick函数中,更具体的说是OneTick函数调用的的CheckIfDue函数中,这个很容易看明白,就不详细说明了。

 

 

以上是对时钟中断模块的分析,明白了时钟中断的原理就可以开始着手设计多级队列反馈调度算法了。

第一步:在Thread类中添加已使用的时间片这个整型属性,这里命名为usedTimeSlices,用来在调度和切换的时候判断放入哪个队列,在构造函数中初始化为0,并添加getset函数。

第二步:在Scheduler类中添加两个队列,一共是三个队列,同时定义三个常量Q1TimeSlicesQ2TimeSlicesQ3TimeSlices,分别代表三个队列的时间片数量,值分别为248。接下来修改ReadyToRun函数,很明显,我们要根据线程使用过的时间片数目来决定将线程加到哪个就绪队列中,如果小于2,加入队列1,如果大于等于2小于6,加入队列2,否则加入队列3。然后是FindNextToRun函数,根据多级队列反馈调度算法的性质,队列1的优先级高于队列2,队列2的优先级高于队列3,所以在这个函数中,要先找队列1中的线程,如果为空,则找队列2,如果还为空则找队列3。到这里,调度部分的修改也已经完成了。

第三步:修改Timer类的TimerExpired函数,每个时间片过完,都要对运行线程的usedTimeSlices1,所以在这个函数里面要加上修改的语句,这里将不足1个时间片的时间也记为1个时间片。

第四步:由于给每个队列的可用时间片都大于1,所以并不是每次时钟中断都要对线程进行切换,所以必须修改System.ccTimerInterruptHandler函数,判断当前线程的usedTimeSlices是否等于26或者14,如果是则调用YieldOnReturn函数,否则不进行任何操作。当已使用时间片大于14的时候,可以考虑使用mod8的方式进行判断,如果余数为6,则调用YieldOnReturn函数,这样的话可以保证第三队列的某个线程某次调度只能占用CPU8个时间片,防止了独占的情况。当然,这里可以将这个判断和614两种情况结合起来,也就是说或者是2,或者模8的结果为6,在这两种情况下调用YieldOnReturn函数。

       第五步:最后,可以考虑实现抢占式的调度模式,即如果当前运行的是第二队列或者第三队列的线程,而新加入的线程(可以是新线程,也可能是从阻塞状态变为就绪状态的线程)只使用了1个时间片或者没有使用时间片。这时候,可以将当前线程按照已经使用的时间片数目重新加入某个就绪队列中,而将执行新的线程。具体的实现来说,就是将currentThread的状态设为Ready,然后利用ReadyToRun原有的语句,将currentThread加到某个就绪队列中,然后Run新的线程。

到这里,整个的多级队列反馈调度算法已经实现了,当然,还要把timer初始化为固定大小的时钟中断。如果测试语句中的开关中断语句比较少,则时间片的大小TimerTicks也要设置的比较小,否则基本上看不到时钟中断的效果,如果设成两百,则一个时钟中断内可能所有的程序已经执行完成了,因为通常某个函数里不可能有二十个恢复中断的语句,而在nachos模拟中一次恢复中断就调用一次OneTick函数,一次OneTick函数在系统态下前进10个时间单位。在试验中,我将时间片设成了二十个单位,恰好是一次OneTick2倍,也就是说如果程序中有两个恢复中断的语句,就会发生一次时钟中断,这显然是个很好满足的条件,尤其是是涉及到互斥和同步的时候。结果在打印语句中确实可以看到比较明显的中断调度效果,当然前提是插入了很多的打印语句,根据打印语句再去和自己的流程分析进行对比,能加深对整个流程的理解,也可以发现一些之前可能被忽视的问题。

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(nachos 3.4 实现抢占式多级队列反馈算法)