【操作系统】Nachos 内核线程

2021SC@SDUSC

配置文件

在与nachos同层目录下创建nachos.conf文件,将nachos/proj1/nachos.conf中的内容复制进去(直接复制文件也行)。

Machine.stubFileSystem = false
Machine.processor = false
Machine.console = false
Machine.disk = false
Machine.bank = false
Machine.networkLink = false
ElevatorBank.allowElevatorGUI = true
NachosSecurityManager.fullySecure = false
ThreadedKernel.scheduler = nachos.threads.RoundRobinScheduler #nachos.threads.PriorityScheduler
Kernel.kernel = nachos.threads.ThreadedKernel

创建线程

在 Nachos 中,我们要如何去创建一个线程呢?将目光聚集到nachos/threads/KThread.java,在类的注释中,我们能找到答案。

要创建一个新线程,首先得声明一个实现Runnable接口的类,并实现里面的run方法。

class PiRun implements Runnable {
    
    
	public void run() {
    
    
		// compute pi
		...
	}
}

该类的对象将在实例化KThread时作为参数传入,线程在创建后我们可以调用它的fork方法使它运行。

PiRun p = new PiRun();
new KThread(p).fork();

测试代码

我们可以在nachos/threads/KThread.java下的selfTest方法中写我们的测试代码,比如我们让控制台打印一行数字。

public static void selfTest() {
    
    
    Lib.debug(dbgThread, "Enter KThread.selfTest");

    System.out.println(123456789);

    new KThread(new PingTest(1)).setName("forked thread").fork();
    new PingTest(0).run();
}

进入nachos/machine/Machine.java中,从main入口点运行这个程序,控制台输出如下。

nachos 5.0j initializing... config interrupt timer user-check grader
123456789
*** thread 0 looped 0 times
*** thread 1 looped 0 times
*** thread 0 looped 1 times
*** thread 1 looped 1 times
*** thread 0 looped 2 times
*** thread 1 looped 2 times
*** thread 0 looped 3 times
*** thread 1 looped 3 times
*** thread 0 looped 4 times
*** thread 1 looped 4 times
Machine halting!

Ticks: total 2130, kernel 2130, user 0
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: page faults 0, TLB misses 0
Network I/O: received 0, sent 0

我们清楚地看到,我们想要的内容在第 2 行打印出来了。

题目 1

实现 Kthread 的 join() 函数。join() 函数的作用是,B 线程调用了 A.join(),B 线程应该等待,直到 A 结束后再执行。

实验大纲中给出的提示是:为每个 Kthread 增添一个等待队列,里面存放所有需要等待该线程执行结束后才能继续执行的线程。然后当该线程执行结束后,将所有等待队列中的线程唤醒。

现在,nachos/threads/KThread.java下的join方法中不存在业务逻辑代码,需要等待我们去完善。

在构思等待队列waitQueue怎么写之前,我们先看一下就绪队列readyQueue的写法。

public KThread() {
    
    
	if (currentThread != null) {
    
    
		tcb = new TCB();
    } else {
    
    
        readyQueue = ThreadedKernel.scheduler.newThreadQueue(false);
        readyQueue.acquire(this);
		...
    }
}

public void ready() {
    
    
	...
    if (this != idleThread)
        readyQueue.waitForAccess(this);
    ...
}

private static void runNextThread() {
    
    
    KThread nextThread = readyQueue.nextThread();
    ...
}

private static ThreadQueue readyQueue = null;

readyQueue在创建第一个线程的时候进行实例化。当某线程调用ready方法的时候,该线程会进入到就绪队列中,等待调度。在runNextThread中,readyQueue会调用nextThread方法,获取队列中下一个线程。

值得一说的是,acquire方法会通知队列,一个线程已经得到访问权限,此时其他线程不能得到执行。但是,nextThread方法会将访问权限转移到队列中的下一个线程,这样的话,得到访问权限的线程就可以运行了。

我们可以仿照readyQueue的方式,来构建我们的waitQueue,但要注意几点不同:

  • 一个内核中,一个就绪队列就足够了,但等待队列不行,每个线程都可能有它们各自的等待队列,所以,waitQueue不能是静态变量;
  • 某个线程的等待队列中,访问权限的转移需要该线程finish之后才能转移,而非yield(让出 CPU)或sleep(休眠);
  • join方法中,因为涉及到当前线程的切换,所以我们需要进行关中断以及恢复中断;
  • CPU 的使用权会在当前线程离开后交给就绪队列中的第一个线程,而在等待队列中的线程只等待队列的所属线程,队列中的线程之间不存在谁等着谁的关系,所以在等待队列的所属线程finish之后,让队列中的所有线程都进入就绪状态。

KThread类中声明变量waitThreadList

private LinkedList<KThread> waitThreadList = null;

在构造函数中将waitThreadList初始化:

public KThread() {
    
    
	...
    // 初始化等待线程列表
    waitThreadList = new LinkedList<>();
}

join方法如下:

public void join() {
    
    
    Lib.debug(dbgThread, "Joining to thread: " + toString());

    Lib.assertTrue(this != currentThread);

    // 关中断,同时获取到之前的中断状态
    boolean intStatus = Machine.interrupt().disable();

    // 如果该线程(调用 join 方法的线程)未处于完成状态
    if (this.status != statusFinished) {
    
    
        // 将当前线程(正在运行的线程)添加到等待线程列表
        waitThreadList.add(KThread.currentThread());
        // 让当前线程进入休眠状态
        currentThread.sleep();
    }

    // 恢复中断
    Machine.interrupt().restore(intStatus);
}

完善finish方法:

public static void finish() {
    
    
    Lib.debug(dbgThread, "Finishing thread: " + currentThread.toString());

    Machine.interrupt().disable();

    Machine.autoGrader().finishingCurrentThread();

    Lib.assertTrue(toBeDestroyed == null);

    toBeDestroyed = currentThread;
    currentThread.status = statusFinished;

    // 遍历当前线程的等待线程列表
    for (KThread waitThread : currentThread().waitThreadList) {
    
    
        // 让等待线程进入就绪装填
        waitThread.ready();
    }

    sleep();
}

测试程序:

public static void test1() {
    
    
    System.out.println("\n<--- 题目 1 开始测试 --->\n");

    // 创建 A 线程
    KThread threadA = new KThread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            for (int i = 0; i < 5; i++) {
    
    
                // 如果就绪队列中有其他线程,那么该线程让出 CPU,进入就绪队列
                System.out.println("A 线程尝试让出 CPU");
                currentThread.yield();
            }
            System.out.println("A 线程运行结束");
        }
    });

    // 创建 B 线程
    KThread threadB = new KThread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            System.out.println("B 线程开始运行");
            // A 线程加入
            System.out.println("A 线程加入");
            threadA.join();
            System.out.println("B 线程结束运行");
            // 要在两个线程都结束的时候打印下面语句
            System.out.println("\n<--- 题目 1 结束测试 --->\n");
        }
    });

    // 启动线程
    threadB.fork();
    threadA.fork();
}

修改selfTest方法:

 public static void selfTest() {
    
    
     Lib.debug(dbgThread, "Enter KThread.selfTest");
     test1();
 }

启动主程序,控制台打印如下:

nachos 5.0j initializing... config interrupt timer user-check grader

<--- 题目 1 开始测试 --->

B 线程开始运行
A 线程加入
A 线程尝试让出 CPU
A 线程尝试让出 CPU
A 线程尝试让出 CPU
A 线程尝试让出 CPU
A 线程尝试让出 CPU
A 线程运行结束
B 线程结束运行

<--- 题目 1 结束测试 --->

Machine halting!

Ticks: total 2110, kernel 2110, user 0
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: page faults 0, TLB misses 0
Network I/O: received 0, sent 0

Process finished with exit code 0

题目 2

提供一个不使用信号量工具来实现的条件变量。条件变量的作用是,当一个进程/线程执行过程中发现有一些前提条件没有完成,那么这个进程/线程可以使用条件变量挂起,直到其他进程/线程前提条件完成再将该进程/线程唤醒。

实验大纲中给出的提示是:为每个条件变量实现一个队列,使用条件变量的线程在该条件变量的队列上挂起,唤醒时从队列中移除。

nachos/threads下,有两个条件变量类,分别是ConditionCondition2。它们的区别是:前者是基于信号量的实现,后者是通过关中断进行同步的实现。根据题目要求,我们选择后者,我们需要补全Condition2.java中的方法。

根据提示,我们需要创建一个等待线程列表,当调用sleep方法时,我们将当前线程添加到等待列表,而当调用wakewakeAll)方法时,我们将一个(全部)线程从等待列表中取出,让它们进入就绪队列。

创建等待队列waitThreadList

private LinkedList<KThread> waitThreadList;

public Condition2(Lock conditionLock) {
    
    
    this.conditionLock = conditionLock;

    waitThreadList = new LinkedList<>();
}

sleep方法如下:

public void sleep() {
    
    
    Lib.assertTrue(conditionLock.isHeldByCurrentThread());

    conditionLock.release();

    // 关中断,并获取之前的中断状态
    boolean intStatus = Machine.interrupt().disable();

    // 将当前线程添加到等待线程列表
    waitThreadList.add(KThread.currentThread());
    KThread.currentThread().sleep();

    // 恢复中断
    Machine.interrupt().restore(intStatus);

    conditionLock.acquire();
}

wake方法如下:

public void wake() {
    
    
    Lib.assertTrue(conditionLock.isHeldByCurrentThread());

    // 关中断,并获取之前的中断状态
    boolean intStatus = Machine.interrupt().disable();

    // 如果等待线程列表非空
    if (!waitThreadList.isEmpty()) {
    
    
        // 获取等待线程队列的第一个线程
        KThread thread = waitThreadList.removeFirst();
        if (thread != null) {
    
    
            // 让它进入就绪状态
            thread.ready();
        }
    }

    // 恢复中断
    Machine.interrupt().restore(intStatus);
}

wakeAll方法如下:

public void wakeAll() {
    
    
    Lib.assertTrue(conditionLock.isHeldByCurrentThread());

    // 关中断,并获取之前的中断状态
    boolean intStatus = Machine.interrupt().disable();

    // 遍历条件变量中所有等待的线程
    for (KThread thread : waitThreadList){
    
    
        // 让这些线程进入就绪状态
        thread.ready();
    }

	// 清空等待列表
    waitThreadList.clear();

    // 恢复中断
    Machine.interrupt().restore(intStatus);
}

回到nachos/threads/KThread.java,测试程序如下:

public static void test2() {
    
    
    System.out.println("\n<--- 题目 2 开始测试 --->\n");

    // 创建锁以及条件变量
    Lock lock = new Lock();
    Condition2 condition = new Condition2(lock);

    // 创建 A 线程
    KThread threadA = new KThread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            System.out.println("A 线程开始运行");
            // 获得锁
            lock.acquire();
            // 挂起当前线程
            System.out.println("A 线程进入休眠状态");
            condition.sleep();
            System.out.println("A 线程被唤醒");
            // 释放锁
            lock.release();
            System.out.println("A 线程结束运行");
        }
    });

    // 创建 B 线程
    KThread threadB = new KThread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            System.out.println("B 线程开始运行");
            // 获得锁
            lock.acquire();
            // 挂起当前线程
            System.out.println("B 线程进入休眠状态");
            condition.sleep();
            System.out.println("B 线程被唤醒");
            // 释放锁
            lock.release();
            System.out.println("B 线程结束运行");
            System.out.println("\n<--- 题目 2 结束测试 --->\n");
        }
    });

    // 创建 C 线程
    KThread threadC = new KThread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            System.out.println("C 线程开始运行");
            // 获得锁
            lock.acquire();
            // 唤醒所有线程
            System.out.println("C 线程尝试唤醒所有线程");
            condition.wakeAll();
            // 释放锁
            lock.release();
            System.out.println("C 线程结束运行");
        }
    });

	// 启动线程
    threadA.fork();
    threadB.fork();
    threadC.fork();
}

selfTest方法调用一下test2方法中,即可在运行时执行上述代码。打印信息如下:

nachos 5.0j initializing... config interrupt timer user-check grader

<--- 题目 2 开始测试 --->

A 线程开始运行
A 线程即将被挂起
B 线程开始运行
B 线程即将被挂起
C 线程开始运行
C 线程尝试唤醒所有线程
C 线程结束运行
A 线程被唤醒
A 线程结束运行
B 线程被唤醒
B 线程结束运行

<--- 题目 2 结束测试 --->

Machine halting!

Ticks: total 2160, kernel 2160, user 0
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: page faults 0, TLB misses 0
Network I/O: received 0, sent 0

Process finished with exit code 0

题目 3

实现 Alarm 中的 waitUntil(long) 方法。作用是使得一个内核线程等待一段时间后再继续执行。

实验大纲中给出的提示是:在 Alarm 中实现一个队列,所有 waitUntil() 等待的线程都在队列里,然后每次时钟中断时检查队列,将到时间的线程唤醒。

实际上,用普通的先出先出队列是不合适的。因为线程的最早唤醒时刻是进入休眠的时刻加休眠的时间,休眠之间足够长的话,早进入休眠状态的线程不一定就能早被唤醒。因此,我们用普通的链表就行了。

进入nachos/threads/Alarm.java,链表以及链表中的元素声明如下(别忘记引入java.util.LinkedList):

// 等待中的线程以及它的唤醒时间
private static class WakeInfo {
    
    
    private KThread thread;
    private long wakeTime;

    private WakeInfo(KThread thread, long waitTime) {
    
    
        this.thread = thread;
        this.wakeTime = waitTime;
    }

    private KThread getThread() {
    
    
        return this.thread;
    }

    private long getWakeTime() {
    
    
        return this.wakeTime;
    }
}

LinkedList<WakeInfo> list;

Alarm的构造方法中将链表进行初始化:

public Alarm() {
    
    
    // 链表初始化
    list = new LinkedList<WakeInfo>();
    
    Machine.timer().setInterruptHandler(new Runnable() {
    
    
        public void run() {
    
    
            timerInterrupt();
        }
    });
}

waitUnitl方法中,根据当前线程以及最早唤醒时间创建一个WakeInfo,并将它添加到链表中:

public void waitUntil(long x) {
    
    
    // 关中断,并获取之前的中断状态
    boolean intStatus = Machine.interrupt().disable();

    // 计算被唤醒的时间
    long wakeTime = Machine.timer().getTime() + x;

    // 创建一个 WakeInfo,并将它添加到链表中去
    list.add(new WakeInfo(KThread.currentThread(), wakeTime));

    // 让当前线程进入睡眠状态
    KThread.currentThread().sleep();

    // 恢复中断
    Machine.interrupt().restore(intStatus);
}

检查处于休眠状态的线程,如果到唤醒时间了,就把它放入就绪队列中:

public void timerInterrupt() {
    
    
    // 关中断,并获取之前的中断状态
    boolean intStatus = Machine.interrupt().disable();

    // 遍历链表中的每一个元素
    for (WakeInfo wakeInfo : list) {
    
    
        // 如果当前时间超过线程的唤醒时间
        if (Machine.timer().getTime() >= wakeInfo.getWakeTime()) {
    
    
            // 将该线程添加到就绪队列
            wakeInfo.getThread().ready();
            // 从线程链表中移出这个元素
            list.remove(wakeInfo);
        }
    }

    // 当前线程让出 CPU
    KThread.currentThread().yield();

    // 恢复中断
    Machine.interrupt().restore(intStatus);
}

回到nachos/threads/KThread.java中,该题目的测试程序如下:

public static void test3(){
    
    
    System.out.println("\n<--- 题目 3 开始测试 --->\n");

    // 创建 A 进程
    KThread threadA = new KThread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            System.out.println("A 线程开始运行");
            // 等待时间以及计算最早唤醒时刻
            long waitTime = 100;
            long earliestWakeTime = waitTime + Machine.timer().getTime();
            System.out.println("A 线程将要休眠,最早唤醒时刻:" + earliestWakeTime);
            // 让线程 A 进入休眠状态
            ThreadedKernel.alarm.waitUntil(waitTime);
            // 实际唤醒时刻以及与最早唤醒时刻的差值
            long actualWakeTime = Machine.timer().getTime();
            System.out.println("A 线程退出休眠,实际唤醒时刻:" + actualWakeTime);
            System.out.println("A 线程两时刻差值:" + (actualWakeTime - earliestWakeTime));
        }
    });

    // 创建 B 进程
    KThread threadB = new KThread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            System.out.println("B 线程开始运行");
            // 等待时间以及计算最早唤醒时刻
            long waitTime = 1000;
            long earliestWakeTime = waitTime + Machine.timer().getTime();
            System.out.println("B 线程将要休眠,最早唤醒时刻:" + earliestWakeTime);
            // 让线程 B 进入休眠状态
            ThreadedKernel.alarm.waitUntil(waitTime);
            // 实际唤醒时刻以及与最早唤醒时刻的差值
            long actualWakeTime = Machine.timer().getTime();
            System.out.println("B 线程退出休眠,实际唤醒时刻:" + actualWakeTime);
            System.out.println("B 线程两时刻差值:" + (actualWakeTime - earliestWakeTime));

            System.out.println("\n<--- 题目 3 结束测试 --->\n");
        }
    });

    // 启动线程
    threadA.fork();
    threadB.fork();
}

selfTest方法调用一下test3方法中,即可在运行时执行上述代码。打印信息如下:

nachos 5.0j initializing... config interrupt timer user-check grader

<--- 题目 3 开始测试 --->

A 线程开始运行
A 线程将要休眠,最早唤醒时刻:160
B 线程开始运行
B 线程将要休眠,最早唤醒时刻:1070
A 线程退出休眠,实际唤醒时刻:510
A 线程两时刻差值:350
B 线程退出休眠,实际唤醒时刻:1540
B 线程两时刻差值:470

<--- 题目 3 结束测试 --->

Machine halting!

Ticks: total 2070, kernel 2070, user 0
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: page faults 0, TLB misses 0
Network I/O: received 0, sent 0

Process finished with exit code 0

题目 4

使用条件变量实现一个类似于生产者消费者功能的程序。

实验大纲中给出的提示是:利用信号量或者锁等同步工具实现的一个进程同步问题的程序。更具体的说,是一个经典的生产者消费者问题。

因为在做题目 2 的时候,我们按照题目要求,使用锁实现了条件变量,所以为了减少时间成本,在该题目中,我们沿用之前的知识。如果对信号量感兴趣的话,可以去阅读nachos/threads/Condition.java中的代码。

我们进入nachos/threads/Communicator.java中,实现其中的线程同步。

这个类中,它允许多个说者同时等待着写数据,也允许多个听者同时等待着读数据,但是,它不允许让说者和听者都处于等待状态。

说者在使用speak方法的时候,如果发现有听者正在等待着读数据,那么它在执行方法的时候便不会阻塞住,如果发现此时没有听者在等待着,那么它在生产一定量数据之后便会进入休眠状态,等待听者来唤醒它。听者在使用listen方法的时候同理。

为了实现同步,我们需要创建一个锁,然后将这个锁传入进说者的条件变量和听者的条件变量中。除此之外,我们还需要记录一下处于休眠状态的说者和听者的数量,以便于决定在发送或接收数据的时候决定是否要等待。

变量的定义及初始化如下:

public Communicator() {
    
    
    lock = new Lock();
    speakerCondition = new Condition2(lock);
    listenerCondition = new Condition2(lock);

    wordQueue = new LinkedList<>();
}

// 锁和条件变量
private Lock lock;
private Condition2 speakerCondition;
private Condition2 listenerCondition;

// 存储 word 的队列
private Queue<Integer> wordQueue;

// 处于休眠状态的 speaker 和 listener 的数量,二者不可同时大于 0
private int sleepSpeakerCount;
private int sleepListenerCount;

speak方法如下:

public void speak(int word) {
    
    
    // 获得锁
    lock.acquire();

    // 将 word 存入队列
    wordQueue.offer(word);

    // 如果此时没有 listener 处于休眠状态
    if (sleepListenerCount == 0) {
    
    
        // 让 speaker 进入休眠状态,等待 listener 唤醒
        sleepSpeakerCount++;
        speakerCondition.sleep();
    } else {
    
    
        // 唤醒一个 listener
        listenerCondition.wake();
        sleepListenerCount--;
    }

    // 释放锁
    lock.release();
}

listen方法如下:

public int listen() {
    
    

    // 获得锁
    lock.acquire();

    // 如果此时没有 speaker 处于休眠状态
    if (sleepSpeakerCount == 0) {
    
    
        // 让 listener 进入休眠状态,等待 speaker 唤醒
        sleepListenerCount++;
        listenerCondition.sleep();
    } else {
    
    
        // 唤醒一个 speaker
        speakerCondition.wake();
        sleepSpeakerCount--;
    }

    // 获取 word
    int word = wordQueue.poll();

    // 释放锁
    lock.release();

    return word;
}

回到nachos/threads/KThread.java中,该题目的测试程序如下:

public static void test4(){
    
    
    System.out.println("\n<--- 题目 4 开始测试 --->\n");

    Communicator communicator = new Communicator();

    // 创建 A 进程
    KThread threadA = new KThread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            // A 线程不断发送信息
            for (int i = 0; i < 5; i++) {
    
    
                System.out.println("A 线程说:" + (i + 1));
                communicator.speak(i + 1);
            }
        }
    });

    // 创建 B 进程
    KThread threadB = new KThread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            // B 线程不断接收信息
            for (int i = 0; i < 5; i++) {
    
    
                System.out.println("B 线程准备听");
                int word = communicator.listen();
                System.out.println("B 线程听到了:" + word);
            }

            System.out.println("\n<--- 题目 4 结束测试 --->\n");
        }
    });

    // 启动线程
    threadA.fork();
    threadB.fork();
}

selfTest方法调用一下test4方法中,即可在运行时执行上述代码。打印信息如下:

nachos 5.0j initializing... config interrupt timer user-check grader

<--- 题目 4 开始测试 --->

A 线程说:1
B 线程准备听
B 线程听到了:1
B 线程准备听
A 线程说:2
A 线程说:3
B 线程听到了:2
B 线程准备听
B 线程听到了:3
B 线程准备听
A 线程说:4
A 线程说:5
B 线程听到了:4
B 线程准备听
B 线程听到了:5

<--- 题目 4 结束测试 --->

Machine halting!

Ticks: total 2450, kernel 2450, user 0
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: page faults 0, TLB misses 0
Network I/O: received 0, sent 0

Process finished with exit code 0

题目 5

实现优先级调度。

实验大纲中给出的提示是:为每一个线程设定一个优先级的值,调度时找出优先级最高的线程进行调度。该题还有一个注意点,便是优先级传递,例如:A 线程需要等待 B 线程执行结束后才能执行,并且 A 线程优先级比 B 线程要高,那么在 A 线程等待 B 线程执行的时候,B 线程的优先级应该提高到与 A 线程相同。在这里提供一个效率不高的思路,在每次调度时,先计算每个线程经过传递后的实际优先级,然后再从每个线程中找出实际优先级最大的线程进行调度。

在之前几个题目,我们选择的调度方式是循环调度,里面的队列是先进先出队列:从就绪队列中取出第一个线程,当它结束运行之后,如果想再次进入等待队列,它需要到队列的末尾。

nachos/threads/RoundRobinScheduler.java,我们可以看到里面队列的实现方式。在内部类FifoQueue中,我们发现它只有四个方法,分别是waitForAccessnextThreadacquireprint。而先进先出队列与优先级队列的不同之处就在于它们对接下来要运行的线程的选择不同,也就是说,我们只需要把关注点放到nextThread中就行了。

优先级队列的代码在同目录下的PriorityScheduler.java中,里面的内部类有两个,一个就是优先级队列,另外一个用来绑定线程和它的优先级。在我们的Kthread类中,它并没有任何变量来表示它的优先级大小(可能是由于在非优先级调度中,优先级没实际作用),而ThreadState的出现有点组合式继承的意思,在Kthread的基础上多了一个表示优先级的整型变量priority

ThreadState中的关键方法是getEffectivePriority,我们按照实验大纲中给出的提示,将某线程的等待队列遍历一遍,获取里面最高的优先级,返回它与该线程的优先级的最高值。

需要注意的是,在题目 1 中,我们在nachos/threads/KThread.java中声明了一个waitThreadList变量用来存放等待线程,当时我们设置它的可见性为private,为了让该类能够访问到它,我们需要把可见性设置为缺省、protectedpublic(只要不是私有就行),或者写一个 get 方法:

// 获取等待线程列表
public LinkedList<KThread> getWaitThreadList() {
    
    
    return waitThreadList;
}

getEffectivePriority的实现如下:

public int getEffectivePriority() {
    
    
    // 有效优先级初始值
    int effectivePriority = getPriority();

    // 遍历该线程的等待线程列表
    for (KThread waitThread : thread.getWaitThreadList()) {
    
    
        // 等待线程的有效优先级
        int targetPriority = getThreadState(waitThread).getEffectivePriority();
        // 如果等待线程的有效优先级更高
        if (effectivePriority < targetPriority) {
    
    
            // 进行优先级传递
            effectivePriority = targetPriority;
        }
    }

    return effectivePriority;
}

按照上述的思路,waitForAccessacquire的实现也可以参考先进先出队列,具体实现如下:

public void waitForAccess(PriorityQueue waitQueue) {
    
    
    Lib.assertTrue(Machine.interrupt().disabled());

    // 将线程加入优先级队列中去
    waitQueue.list.add(this);
}

public void acquire(PriorityQueue waitQueue) {
    
    
    Lib.assertTrue(Machine.interrupt().disabled());

    // 断言此时优先级队列中内容为空
    Lib.assertTrue(waitQueue.list.isEmpty());
}

setPriority方法需要加一步有效性检测:

public void setPriority(int priority) {
    
    
    if (this.priority == priority)
        return;

	// 如果设置的优先级在有效范围值之外的话,打印信息并返回
    if (priority < priorityMinimum || priority > priorityMaximum) {
    
    
        System.out.println("优先级设置失败");
        return;
    }

    this.priority = priority;
}

PriorityQueue类中,对比FifoQueue,链表中的元素由KThread换成ThreadStateprint直接照搬:

public void print() {
    
    
    Lib.assertTrue(Machine.interrupt().disabled());

    // 打印所有线程
    for (Iterator i = list.iterator(); i.hasNext(); ) {
    
    
        System.out.print(i.next() + " ");
    }
}

// 优先级队列中的线程列表
private LinkedList<ThreadState> list = new LinkedList<>();

根据注释,transferPriority表示是否允许优先级从等待线程转移到拥有此队列的线程。在获取下一个要运行的线程的时候,如果transferPriorityfalse,那么就用线程本身的优先级做比较,如果transferPrioritytrue,那么就用经优先级传递后的线程的优先级做比较。nextThreadpickNextThread的实现如下:

public KThread nextThread() {
    
    
    Lib.assertTrue(Machine.interrupt().disabled());

    ThreadState threadState = pickNextThread();
    if (threadState == null) {
    
    
        return null;
    }

	// 将该线程移除后返回
    list.remove(threadState);
    return threadState.thread;
}

protected ThreadState pickNextThread() {
    
    
    ThreadState nextThreadState = null;

    // 当前最大优先级
    int currentMaxPriority = -1;

    // 遍历优先级队列中的每一个线程
    for (ThreadState threadState : list) {
    
    
        // 根据是否可以传递优先级,获取到实际用于比较的优先级
        int threadPriority;
        if (transferPriority) {
    
    
            // 线程包括其等待队列中最高的优先级
            threadPriority = threadState.getEffectivePriority();
        } else {
    
    
            // 线程本身的优先级(未传递)
            threadPriority = threadState.getPriority();
        }

        // 选择优先级更高的线程
        if (threadPriority > currentMaxPriority) {
    
    
            nextThreadState = threadState;
            currentMaxPriority = threadPriority;
        }
    }

    return nextThreadState;
}

至此,PriorityScheduler实现完毕,我们需要对它进行测试。在写测试程序之前,我们需要改一个地方。因为,此时运行程序时,默认的调度程序是RoundRobinScheduler,我们需要修改配置文件,让默认的调度程序改为PriorityScheduler。与nachos同目录下,有一个名为nachos.conf的文件,我们需要修改它的ThreadedKernel.scheduler项:

ThreadedKernel.scheduler = nachos.threads.PriorityScheduler

进入nachos/threads/KThread.java,不允许优先级传递的测试程序如下:

public static void test5() {
    
    
	System.out.println("\n<--- 题目 5 开始测试 --->\n");

    PriorityScheduler scheduler = new PriorityScheduler();

    KThread threadA = new KThread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            System.out.println("A 线程开始运行,初始优先级为 2");
            System.out.println("A 线程尝试让出 CPU");
            yield();
            System.out.println("A 线程重新使用 CPU");
            System.out.println("A 线程结束运行");
        }
    });

    KThread threadB = new KThread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            System.out.println("B 线程开始运行,初始优先级为 4");
            System.out.println("B 线程尝试让出 CPU");
            yield();
            System.out.println("B 线程重新使用 CPU");
            System.out.println("B 线程结束运行");
            
//          // 允许优先级传递时打印下面的语句
//          System.out.println("\n<--- 题目 5 结束测试 --->\n");
        }
    });

    KThread threadC = new KThread(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            System.out.println("C 线程开始运行,初始优先级为 6");
            System.out.println("C 线程尝试让出 CPU");
            yield();
            System.out.println("C 线程重新使用 CPU");
            System.out.println("C 线程等待 A 线程");
            threadA.join();
            System.out.println("C 线程重新使用 CPU");
            System.out.println("C 线程结束运行");

            // 不允许优先级传递时打印下面的语句
            System.out.println("\n<--- 题目 5 结束测试 --->\n");
        }
    });


    scheduler.getThreadState(threadA).setPriority(2);
    scheduler.getThreadState(threadB).setPriority(4);
    scheduler.getThreadState(threadC).setPriority(6);

    threadA.fork();
    threadB.fork();
    threadC.fork();
}

selfTest方法调用一下test5方法中,即可在运行时执行上述代码。打印信息如下:

nachos 5.0j initializing... config interrupt timer user-check grader

<--- 题目 5 开始测试 --->

C 线程开始运行,初始优先级为 6
C 线程尝试让出 CPU
C 线程重新使用 CPU
C 线程等待 A 线程
B 线程开始运行,初始优先级为 4
B 线程尝试让出 CPU
B 线程重新使用 CPU
B 线程结束运行
A 线程开始运行,初始优先级为 2
A 线程尝试让出 CPU
A 线程重新使用 CPU
A 线程结束运行
C 线程重新使用 CPU
C 线程结束运行

<--- 题目 5 结束测试 --->

Machine halting!

Ticks: total 2110, kernel 2110, user 0
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: page faults 0, TLB misses 0
Network I/O: received 0, sent 0

Process finished with exit code 0

要想让优先级能够传递,我们需要修改KThread的构造函数,让传入就绪队列创建方法的参数值为true

public KThread() {
    
    
    if (currentThread != null) {
    
    
        tcb = new TCB();
    } else {
    
    
        // 参数表示是否允许优先级传递
        readyQueue = ThreadedKernel.scheduler.newThreadQueue(true);
        readyQueue.acquire(this);
        ...
    }
}

稍微改动一下test5,将结束语句的打印放到 B 线程的执行程序的末尾,然后运行这个允许优先级传递测试程序,打印信息如下:

nachos 5.0j initializing... config interrupt timer user-check grader

<--- 题目 5 开始测试 --->

C 线程开始运行,初始优先级为 6
C 线程尝试让出 CPU
C 线程重新使用 CPU
C 线程等待 A 线程
A 线程开始运行,初始优先级为 2
A 线程尝试让出 CPU
A 线程重新使用 CPU
A 线程结束运行
C 线程重新使用 CPU
C 线程结束运行
B 线程开始运行,初始优先级为 4
B 线程尝试让出 CPU
B 线程重新使用 CPU
B 线程结束运行

<--- 题目 5 结束测试 --->

Machine halting!

Ticks: total 2110, kernel 2110, user 0
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: page faults 0, TLB misses 0
Network I/O: received 0, sent 0

Process finished with exit code 0

存在的问题:

当我们采用优先级调度的时候,我们每一次切换线程,递归获取有效优先级的方法就要调用一次,这存在大量无意义的计算,我们需要一种缓存机制,当我们不通过joinsetPriority改变线程优先级的时候,有效优先级直接用我们保存的值,而非重新计算。

优化思路 1:

用一个布尔值来表示有效优先级是否有重新计算的必要,用这种方式,我们便能以极小的改动代价来大幅度提升我们的系统性能。

nachos/threads/KThread.java中声明这个布尔值。

// 优先级状态:之前的优先级顺序是否有效,true 表示有效
private static boolean priorityStatus = false;

// 获取优先级状态
public static boolean getPriorityStatus() {
    
    
    return priorityStatus;
}

// 改变优先级状态
public static void setPriorityStatus(boolean status) {
    
    
    priorityStatus = status;
}

join方法中使这个值置为false,表示当前的有效优先级有必要重新计算。

public void join() {
    
    
    Lib.debug(dbgThread, "Joining to thread: " + toString());

    Lib.assertTrue(this != currentThread);

    // 关中断,同时获取到之前的中断状态
    boolean intStatus = Machine.interrupt().disable();

    // 使之前的优先级顺序失效
    this.setPriorityStatus(false);

    // 如果该线程(调用 join 方法的线程)未处于完成状态
    if (this.status != statusFinished) {
    
    
        // 将当前线程(正在运行的线程)添加到等待线程列表
        waitThreadList.add(KThread.currentThread());
        // 让当前线程进入休眠状态
        currentThread.sleep();
    }

    // 恢复中断
    Machine.interrupt().restore(intStatus);
}

进入到nachos/threads/PriorityScheduler.java中的ThreadState类中,声明优先优先级并在构造方法中进行初始化:

// 有效优先级
protected int effectivePriority;

public ThreadState(KThread thread) {
    
    
    this.thread = thread;

    setPriority(priorityDefault);

    // 设置有效优先级
    this.effectivePriority = this.priority;
}

获取有效优先级的方法以及设置有效优先级的方法如下:

public int getEffectivePriority() {
    
    
    // 尝试使用之前保存的数据
    if (KThread.getPriorityStatus()) {
    
    
        return effectivePriority;
    }

    // 重新计算有效优先级
    effectivePriority = priority;

    // 遍历该线程的等待线程列表
    for (KThread waitThread : thread.getWaitThreadList()) {
    
    
        // 等待线程的有效优先级
        int targetPriority = getThreadState(waitThread).getEffectivePriority();
        // 如果等待线程的有效优先级更高
        if (effectivePriority < targetPriority) {
    
    
            // 进行优先级传递
            effectivePriority = targetPriority;
        }
    }

    return effectivePriority;
}

public void setPriority(int priority) {
    
    
    if (this.priority == priority)
        return;

    // 如果设置的优先级在有效范围值之外的话,打印信息并返回
    if (priority < priorityMinimum || priority > priorityMaximum) {
    
    
        System.out.println("优先级设置失败");
        return;
    }

    // 将保存的有效优先级设置为无效
    KThread.setPriorityStatus(false);

    this.priority = priority;
}

同文件下的PriorityQueue类中,我们修改pickNextThread方法。我们在重新计算完有效优先级后,需要将现在的有效优先级设置为可用的,下次调度不需要重新计算。

protected ThreadState pickNextThread() {
    
    
    ThreadState nextThreadState = null;

    // 当前最大优先级
    int currentMaxPriority = -1;

    // 遍历优先级队列中的每一个线程
    for (ThreadState threadState : list) {
    
    
        // 根据是否可以传递优先级,获取到实际用于比较的优先级
        int threadPriority;
        if (transferPriority) {
    
    
            // 线程包括其等待队列中最高的优先级
            threadPriority = threadState.getEffectivePriority();
        } else {
    
    
            // 线程本身的优先级(未传递)
            threadPriority = threadState.getPriority();
        }

        // 选择优先级更高的线程
        if (threadPriority > currentMaxPriority) {
    
    
            nextThreadState = threadState;
            currentMaxPriority = threadPriority;
        }
    }

    if (list.size() != 0) {
    
    
        // 将保存的有效优先级设置为有效
        KThread.setPriorityStatus(true);
    }


    return nextThreadState;
}

优化思路 2:

虽然添加一个表示是否需要重新计算的标志位可以大幅度减少重新计算的次数,但是现在还是存在一些计算的浪费。因为当标志位为false时,我们重新计算了所有线程的有效优先级,但是只有调用joinsetPriority的线程或与它有等待关系的线程的优先级需要计算。

现在的设计存在着一些问题,虽然一个进程知道它所拥有的等待队列中的所有线程,但它不知道它在哪个线程的等待队列中。如果这个问题可以解决的话,那么当我们去改变一个线程的优先级或让它加入到别的线程的等待队列中去的时候,我们只需要递归地去改变等待队列的拥有者的有效优先级即可。

为达到这个目标,一个线程中,我们不仅要声明等待该线程的队列,也要声明该线程所等待的队列。这样的话,题目 1 的join方法也需要做出调整。但是由于做题目 5 之前,一次声明两个队列的操作看上去有点“多余”,为了保证读者有更流畅的阅读体验,这里不对该题目的优化思路 2 进行实现。

题目 6

利用同步工具实现一个过桥游戏的程序。

实验大纲中给出的提示是:与第四题类似,是一个利用信号量或者锁等同步工具实现的一个进程同步问题。

进入到课程官网,我们发现题目要求我们去实现nachos/threads/Boat.java中的方法。游戏规则是:一群大人和小孩(不少于两个)要去一个岛,他们只有一艘小船,小船能承载两个小孩或一个大人。

注意,这个类原先引入了nachos/ag/AutoGrader.java中的方法,这些方法都是打印语句。因为我想自定义打印,所以我把所有与AutoGrader有关的变量、方法都删去了。

全局变量的声明如下:

// 条件变量
private static Lock lock;
private static Condition2 boatCondition;
private static Condition2 adultsCondition;
private static Condition2 startChildrenCondition, endChildrenContidion;

// 起点人数
private static int startAdultsCount, startChildrenCount;
private static int endChildrenCount;
// 船上人数
private static int boatAdultsCount, boatChildrenCount;

// 船是否在起点
private static boolean boatStart;

// 是否都到达目的地
private static boolean success;

AdultItinerary方法的实现如下:

private static void AdultItinerary() {
    
    
    // 获得锁
    lock.acquire();

    // 如果终点没有小孩,等待
    if (boatStart && endChildrenCount == 0) {
    
    
        adultsCondition.sleep();
    }

    // 大人划船去终点(省略上下船过程)
    System.out.println("大人划船去了终点");
    boatAdultsCount --;
    startAdultsCount--;
    boatStart = false;

    // 唤醒一个小孩
    endChildrenContidion.wake();

    // 释放锁
    lock.release();
}

ChildItinerary方法的实现如下:

private static void ChildItinerary() {
    
    
    // 只要没全部到达终点,就一直循环
    while (!success) {
    
    
        // 获得锁
        lock.acquire();

        // 如果船在起点
        if (boatStart) {
    
    
            // 如果船上人满了
            if (boatAdultsCount == 1 || boatChildrenCount == 2) {
    
    
                // 在起点睡眠,等待小孩唤醒
                startChildrenCondition.sleep();
            }
            // 如果船上没有小孩
            else if (boatChildrenCount == 0) {
    
    
                // 上船
                startChildrenCount--;
                boatChildrenCount++;
                // 唤醒一下在起始点睡眠的小孩
                System.out.println("小孩:来个人开船");
                startChildrenCondition.wake();
                // 在船上睡眠,等别的小孩叫我
                boatCondition.sleep();
                // 被叫醒之后,到终点
                System.out.println("小孩坐船去了终点");
                boatChildrenCount--;
                endChildrenCount++;
                // 唤醒一个在终点的小孩
                System.out.println("小孩:检查一下人全了吗?不全的话回去接人");
                endChildrenContidion.wake();
                // 在终点睡眠,需要接人或全员到终点之后被唤醒
                endChildrenContidion.sleep();
            }
            // 如果船上有一个小孩
            else {
    
    
                boatStart = false;
                // 叫醒那个在船上睡眠的小孩
                System.out.println("小孩:我上船了,我带你去终点");
                boatCondition.wake();
                // 到达终点
                System.out.println("小孩划船去了终点");
                startChildrenCount--;
                endChildrenCount++;
                // 在终点睡眠,需要接人或全员到终点之后被唤醒
                endChildrenContidion.sleep();
            }
        }
        // 如果船在终点
        else {
    
    
            // 人员全部转移完毕
            if (startChildrenCount == 0 && startAdultsCount == 0) {
    
    
                success = true;
                System.out.println("小孩:人全了");
                System.out.println("\n<--- 题目 6 结束测试 --->\n");
                // 叫醒所有小孩
                endChildrenContidion.wakeAll();
            }
            // 人员未转移结束,并且现在船上无人
            else if (boatChildrenCount == 0) {
    
    
                // 划船回去(省略上下船过程)
                System.out.println("小孩:现在起点还有 " + startAdultsCount + " 个大人");
                System.out.println("小孩:现在起点还有 " + startChildrenCount + " 个小孩");
                System.out.println("小孩:我得回起点去接他们");
                System.out.println("小孩划船回到了起点");
                endChildrenCount--;
                startChildrenCount++;
                boatStart = true;
                // 有大人还在起始点且终点还有小孩
                if (startAdultsCount != 0 && endChildrenCount != 0) {
    
    
                    // 唤醒一个大人
                    System.out.println("小孩:让大人上船");
                    boatAdultsCount++;
                    adultsCondition.wake();
                    // 让大人划船,这个小孩先睡觉
                    startChildrenCondition.sleep();
                }
            }
        }

        // 释放锁
        lock.release();
    }

测试程序如下:

public static void selfTest() {
    
    
    System.out.println("\n<--- 题目 6 开始测试 --->\n");
    begin(1, 2);
}

public static void begin(int adults, int children) {
    
    
	// 参数校验
    if (adults < 0 || children < 2) {
    
    
        System.out.println("数据异常");
        return;
    }

    // 初始化条件变量
    lock = new Lock();
    boatCondition = new Condition2(lock);
    adultsCondition = new Condition2(lock);
    startChildrenCondition = new Condition2(lock);
    endChildrenContidion = new Condition2(lock);

    // 初始化初始人数
    startAdultsCount = adults;
    startChildrenCount = children;

    // 初始化船的位置
    boatStart = true;

    // 创建并运行大人线程
    for (int i = 0; i < adults; i++) {
    
    
        new KThread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                AdultItinerary();
            }
        }).fork();
    }

    // 创建并运行小孩线程
    for (int i = 0; i < children; i++) {
    
    
        new KThread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                ChildItinerary();
            }
        }).fork();
    }
}

最后,在nachos/threads/KThreads.javaselfTest方法中调用上面的测试程序,然后运行,打印语句如下:

nachos 5.0j initializing... config interrupt timer user-check grader

<--- 题目 6 开始测试 --->

小孩:来个人开船
小孩:我上船了,我带你去终点
小孩划船去了终点
小孩坐船去了终点
小孩:检查一下人全了吗?不全的话回去接人
小孩:现在起点还有 1 个大人
小孩:现在起点还有 0 个小孩
小孩:我得回起点去接他们
小孩划船回到了起点
小孩:让大人上船
大人划船去了终点
小孩:现在起点还有 0 个大人
小孩:现在起点还有 1 个小孩
小孩:我得回起点去接他们
小孩划船回到了起点
小孩:来个人开船
小孩:我上船了,我带你去终点
小孩划船去了终点
小孩坐船去了终点
小孩:检查一下人全了吗?不全的话回去接人
小孩:人全了

<--- 题目 6 结束测试 --->

Machine halting!

Ticks: total 2560, kernel 2560, user 0
Disk I/O: reads 0, writes 0
Console I/O: reads 0, writes 0
Paging: page faults 0, TLB misses 0
Network I/O: received 0, sent 0

Process finished with exit code 0

源码

项目地址:https://gitee.com/GongY1Y/nachos-study

猜你喜欢

转载自blog.csdn.net/weixin_45922876/article/details/121276202