如何让Alarm类更像一个“闹钟”

问题描述

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)测试截图

如何让Alarm类更像一个“闹钟”_第1张图片

从截图中可以看出测试线程设置的等待时间是530 clock ticks,所以其在timerInterrupt()方法第二次被调用时被唤醒(加入到就绪队列),随后被执行。


功能优化

问题:当把线程的睡眠时间设置的足够大时,该线程并不会得到执行。

如何让Alarm类更像一个“闹钟”_第2张图片

可以看到线程设置“睡眠”时间在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();
	}

如何让Alarm类更像一个“闹钟”_第3张图片

这里设置同样的“睡眠”时间,子线程得到执行。

(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系统下真正意义的“闹钟”,在子线程“睡眠”的时候,主线程可以去干其他事而不用阻塞;同时也保证了每一个“睡眠”的子线程都会被

唤醒。是不是就很像我们生活中使用的闹钟了呢?

你可能感兴趣的:(nachos操作系统)