Alarm类的功能为使用硬件计时器提供抢占,允许线程挂起一段确定的时间, 到达线程应该唤醒的时间后,将线程唤醒。
在Alarm类中有Alarm()、timerInterrupt()、waitUntil(x)三个方法,三个方法实现的功能分别为:
Alarm():设置机器的定时器中断处理程序来实现alarm的回调。
timerInterrupt():计时器中断处理程序,这个方法周期性的被机器的时钟调用(大概500 ticks一次),让当前执行的进程让出CPU,如果有其他的线程应该执行,会发生强制上下文转换。
waitUntil(x):让当前线程挂起,挂起的时间长度为x,在计时器中断处理程序中将挂起的程序唤醒,线程必须在不等式(current time) >= (WaitUntil called time)+(x)成立的第一个时钟中断期间被唤醒。
Alarm类的工作过程为,某个进程调用waitUntil(x)后,这个进程将被挂起,理论上线程被唤醒的时间应该为当前时间+x。timerInterrupt()方法大约每500ticks执行一次,检查是否有应该被唤醒线程(线程应该被唤醒的条件为(current time) >= (WaitUntil called time)+(x)成立),如果有应该被唤醒的线程,则将其唤醒。
基于Alarm类的工作过程,每个线程应该有一个被唤醒的时间属性wakeTime;Alarm类的对象应该有一个等待队列waitQueue,等待队列中存放的是因为调用waitUntil(x)方法被挂起的线程。
waitUntil(x)方法中,要根据当前时间和传进来的参数,设置进程应该被唤醒的时间;然后将当前进程存入waitQueue, 为了查找应该唤醒的线程简单,插入线程时可以按照线程应该被唤醒的时间有序插入;插入后,应该将当前进程挂起,等待被timerInterrupt()唤醒。
在timerInterrupt()方法中,要遍历waitQueue,将满足被唤醒条件(current time) >= (WaitUntil called time)+(x)的线程唤醒。
需要修改及添加的代码有:KThread类中新增的全局变量:
public long wakeTime=0;//线程的等待时间,初始化为0
Alarm类:
package nachos.threads;
import java.util.LinkedList;
import nachos.machine.*;
public class Alarm {
public Alarm() {
Machine.timer().setInterruptHandler(new Runnable() {
public void run() { timerInterrupt(); }
});
}
public void timerInterrupt() {
boolean status = Machine.interrupt().disable();
KThread thread;
long currentTime = Machine.timer().getTime();//当前时间
int size=waitQueue.size();//得到等待队列的长度
//如果等待队列为空,return
if(size==0)
return;
//如果等待队列不为空,则将应该唤醒的线程唤醒
else{
for(int i=0;i<size;i++){
thread=waitQueue.get(i);
//如果索引为i的线程不应该被唤醒,那么其后的线程也不应该被唤醒
if(thread.wakeTime>currentTime)
break;
else{
thread.ready();
// System.out.println("实际被唤醒时间:"+currentTime);
waitQueue.remove(thread);
size--;
i--;
}
}
}
Machine.interrupt().restore(status);
}
public void waitUntil(long x) {
boolean status = Machine.interrupt().disable();
long wakeTime = Machine.timer().getTime() + x;//线程应该被唤醒的时间
// System.out.println("理论被唤醒时间:"+wakeTime);
KThread.currentThread().wakeTime=wakeTime;//设置当前线程的wakeTime属性
int size=waitQueue.size();//等待队列长度
//将当前线程放入等待队列中,队列 为有序队列,应放入相应位置
if(size==0)
waitQueue.add(KThread.currentThread());
else{
for(int i=0;i<size;i++){
if(wakeTime<waitQueue.get(i).wakeTime){
waitQueue.add(i, KThread.currentThread());
break;
}
if(i==size-1)
waitQueue.add(size, KThread.currentThread());
}
}
KThread.currentThread().sleep();//将当前线程挂起
Machine.interrupt().restore(status);
}
LinkedList<KThread> waitQueue = new LinkedList();//有序的等待队列,存放的为调用了waitUntil方法的线程,前面的线程应该被唤醒的时间早
}
为了测试Alarm类,可以创建一个线程t,先让线程打印一下当前时间t1,然后调用waitUntil(x)方法;在waitUntil(x)方法中,打印一下线程理论上应该被唤醒的时间t2;在timerInterrupt()方法中,线程被唤醒后,打印一下当前时间t3;线程再次获得执行机会时,打印一下当前时间t4。理论上,这四个时间应满足的关系为:t2=t1+x t3>=t2 t4>t3。测试的代码为:
private static void alarmTest(){
new KThread(new Runnable() {
public void run() {
System.out.println("当前时间:"+Machine.timer().getTime());
ThreadedKernel.alarm.waitUntil(400);
System.out.println("当前时间:"+Machine.timer().getTime());
}
}).fork();
}
可以得到的测试结果截图:
等待时间为400时
等待时间为500时
根据测试结果可以看出当等待时间为400 ticks时,理论被唤醒的时间与实际被唤醒的时间基本一致,但是当等待时间为500 ticks时,实际被唤醒的时间与理论唤醒的时间相差较大。这和Alarm类中唤醒进程的timerInterrupt()方法的工作机制有关,线程并不是到达应该被唤醒的时间后立即被唤醒,而是每隔大约500 ticks,发生计时器中断时,才有被唤醒的机会。只有当线程应该被唤醒和发生计时器中断两个条件都满足时,线程才能被唤醒。