问题描述
nachos.thread包下的Alarm类要求实现“闹钟”的功能,“闹钟”功能是对于每个Kthread而言的。像我们生活中使用起床闹钟那样,闹钟会在起床时间将我们叫醒。同样的,Alarm类要求完成在正确的第一时间将Kthread唤醒。
结合nachos系统特色的细节分析
上面的描述用到了“正确的第一时间”这个修饰词,为什么不能说是“准确的时间”呢?
原因也很简单,nachos系统是一个模拟实现的操作系统,当然也会维护时钟系统(以clock tick为单位),而Alarm类就是基于时钟系统构建的,规定每500 clock ticks,“闹钟”可以生效一次(对应着timerInterrupt()方法调用一次)。所以“闹钟”对Kthread的唤醒时间并不一定是准确的,它只能在每500 clock ticks间隔下唤醒应该被唤醒的Kthread,因此叫做“正确的第一时间”。
实现思路
Alarm类有两个成员函数。
第一个:public void waitUntil(long x)方法,完成当前Kthread的闹钟设置,长整形参数x表示“睡眠”的时间。在方法的内部我们将Kthread加入等待队列(新增的Alarm类类成员变量,用来存储“睡眠”的Kthread),防止其被加入就绪队列,也就实现了“睡眠”的功能。
第二个:public void timerInterrupt()方法,其每500 clock ticks被系统调用一次。在方法内部我们需要对当前每一个正在“睡眠”的Kthread进行检查,并唤醒需要被唤醒的所有Kthread,将其加入到就绪队列。
结合方法功能,我们可以想到:不同的Kthread之间应该可以设置独立的“睡眠”的时间,所以“睡眠”时间需要作为Kthread的属性存在,则为Kthread类成员增添睡眠时间属性waitTime,初始化为0。当Kthread被创建后而不想立即被加入到就绪队列时,则调用waitUntil(long x)方法,设置“睡眠”时间(waitTime = x)。同时在 timerInterrupt()方法内部,我们需要更新等待队列所有Kthread的“睡眠”时间waitTime ,如果某个Kthread的waitTime <=0,则需要将其加入就绪队列。
代码摘取
(1)添加Kthread类成员变量
// 添加Kthread等待时间属性
public long waitTime = 0;
(2)添加Alarm类类成员变量
// readyQueue用来保存未到达唤醒时间的KThread
public static ThreadQueue waitQueue = ThreadedKernel.scheduler.newThreadQueue(false);
// readyQueue0用来保存更新过waitTime的KThread
private static ThreadQueue waitQueue0 = ThreadedKernel.scheduler.newThreadQueue(false);
(3)waitUntil(long x)方法实现
public void waitUntil(long x) {
// for now, cheat just to get something working (busy waiting is bad)
//确定唤醒时间
long wakeTime = Machine.timer().getTime() + x;
KThread.currentThread().waitTime = x;
if (wakeTime > Machine.timer().getTime()) {
System.out.println("waitTime\t" + x);
waitQueue.waitForAccess(KThread.currentThread());
KThread.sleep();
}
}
(4)timerInterrupt()方法实现
public void timerInterrupt() {
System.out.println("call timerInterrupt() time:\t" + Machine.timer().getTime());
boolean intStatus = Machine.interrupt().disable();
if (waitQueue != null) {
// 如果等待队列不为空,则更新每个KThread等待时间waitTime
KThread refreshThread = waitQueue.nextThread();
while (refreshThread != null) {
long oldTime = refreshThread.waitTime;
long newTime = oldTime - 500;
System.out.println("newTime:\t" + newTime);
refreshThread.waitTime = newTime;
if (newTime <= 0) {
// 唤醒
refreshThread.ready();
} else {
waitQueue0.waitForAccess(refreshThread);
}
refreshThread = waitQueue.nextThread();
}
// 周转依旧需要睡眠的KThread
KThread kt = waitQueue0.nextThread();
while (kt != null) {
waitQueue.waitForAccess(kt);
kt = waitQueue0.nextThread();
}
}
KThread.currentThread().yield();
Machine.interrupt().restore(intStatus);
}
(5)测试方法
修改后的PingTest类
private static class PingTest implements Runnable {
PingTest(int which) {
this.which = which;
}
public void run() {
this.setWakeupTime(530);
System.out.println("time to wake up at: " + Machine.timer().getTime() + " ticks");
for (int i = 0; i < 5; i++) {
System.out.println("*** thread " + which + " looped " + i + " times");
currentThread.yield();
}
}
// setWakeupTime(long t)方法
public void setWakeupTime(long t) {
boolean intStatus = Machine.interrupt().disable();
Alarm alarm = ThreadedKernel.alarm;
alarm.waitUntil(t);
Machine.interrupt().restore(intStatus);
}
private int which;
}
Test Alarm
public static void selfTest3() {
System.out.println("/**************Test Alarm****************/");
KThread thread = new KThread(new PingTest(1)).setName("forked thread");
thread.fork();
}
(6)测试截图
从截图中可以看出测试线程设置的等待时间是530 clock ticks,所以其在timerInterrupt()方法第二次被调用时被唤醒(加入到就绪队列),随后被执行。
功能优化
问题:当把线程的睡眠时间设置的足够大时,该线程并不会得到执行。
可以看到线程设置“睡眠”时间在1530 clock ticks时,其并不能得到执行。
原因:因为主线程(main线程)在执行完成后,并不知道还有子线程在“睡眠”,所以直接停机。
解决步骤:
(1)为了让主线程知道有子线程在“睡眠”,可以在子线程被创建后,主线程立即执行join()方法,这样主线程被加入到子线程的等待队列,直到子线程执行结束,主
线程才能继续执行,也就保证了“睡眠”的线程一定能被唤醒。
Test Alarm
public static void selfTest3() {
System.out.println("/**************Test Alarm****************/");
KThread thread = new KThread(new PingTest(1)).setName("forked thread");
thread.fork();
thread.join();
}
这里设置同样的“睡眠”时间,子线程得到执行。
(2)虽然现在所有“睡眠”的线程都得到了执行的机会,但是在线程睡眠的期间,主线程是阻塞的。所以我们可以再系统停机之前来判
断是否还有线程在“睡眠”,如果有则主线程等待睡眠线程都执行完后在执行停机指令。
去掉Test Alarm的'thread.join();'语句
public static void selfTest3() {
System.out.println("/**************Test Alarm****************/");
KThread thread = new KThread(new PingTest(1)).setName("forked thread");
thread.fork();
// thread.join();
}
修改Machine类的halt()方法
public static void halt() {
//添加对“睡眠”线程的判断
boolean intStatus = Machine.interrupt().disable();
KThread refreshThread = Alarm.waitQueue.nextThread();
while(refreshThread != null){
Alarm.waitQueue.waitForAccess(refreshThread);
refreshThread.join();
refreshThread = Alarm.waitQueue.nextThread();
}
Machine.interrupt().restore(intStatus);
System.out.print("Machine halting!\n\n");
stats.print();
terminate();
}
总结
这样,我们就得到了nachos系统下真正意义的“闹钟”,在子线程“睡眠”的时候,主线程可以去干其他事而不用阻塞;同时也保证了每一个“睡眠”的子线程都会被
唤醒。是不是就很像我们生活中使用的闹钟了呢?