Reference
nachos-java Task1.1 Join 包括后面几篇,有proj1的5个task讲解
Nachos Project2思路、代码
操作系统nachoes一些问题与解决方法
代码主要参考,但也需要修改 soohyunc/nachos
我的实现代码 https://download.csdn.net/download/weixin_42127182/12105905
(proj3运行成功但好像还是有点问题,没有放上去)
文章目录
1 建立线程系统
1.1 Kthread.join()
1.1.1 题目要求
实现KThread.join()。注意,另一个线程不必调用join(),但是如果调用了它,则必须只调用它一次。在同一个线程上第二次调用join()的结果是未定义的,即使第二个调用者与第一个调用者是不同的线程。无论是否联接,线程都必须正常执行。
1.1.2 解决方案
当A线程调用B线程的join
方法时,B线程阻塞A线程并获得控制权,将A线程加入阻塞队列,在B线程完成后A继续运行。详细过程见注释。
1.1.3 代码实现
public void join() {
Lib.debug(dbgThread, "Joining to thread: " + toString());
Lib.assertTrue(this != currentThread);
Lib.assertTrue(joinCount == 0);
//关中断;获取当前线程的状态
boolean intStatus = Machine.interrupt().disable();
if(status!=statusFinished){
joinCount += 1;
//将当前运行中的线程加到阻塞队列中
waitQueue.waitForAccess(currentThread);
waitQueue.acquire(this);
readyQueue.waitForAccess(this);
//当前线程睡眠,放弃占用CPU
sleep();
}
//保存当前的状态,等到中断结束回到当前线程时可以接下去执行
Machine.interrupt().restore(intStatus);
}
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;
//调用阻塞(等待)队列中的第一个线程准备执行
KThread waitThread;
do {
waitThread = currentThread.waitQueue.nextThread();
if (waitThread != null)
waitThread.ready();
} while (waitThread != null);
sleep();
}
1.1.4 测试
public static void joinTest() {
System.out.println("\n任务phase1.1");
Lib.debug(dbgThread, "开始join方法测试");
KThread bThread = new KThread(new JoinTest());
bThread.setName("新线程").fork();
System.out.println("A线程运行中");
System.out.println("A线程暂停");
bThread.join();
System.out.println("A线程恢复运行");
}
任务phase1.1
A线程运行中
A线程暂停
B线程运行中
B线程结束运行
A线程恢复运行
Machine halting!
1.2 Condition
1.2.1 题目要求
直接实现条件变量,通过使用中断启用和禁用来提供原子性。系统提供了一个使用信号量的示例实现;要求在不直接使用信号量的情况下提供等效的实现(当然,可以使用锁,即使它们间接使用信号量)。条件变量的第二个实现必须驻留在类nachos.threads.Condition2中。
1.2.2 解决方案
使用Lock
和thread
的waitQueue
实现。
sleep
方法中将当前线程加入waitQueue
并睡眠。
wake
方法中取出waitQueue
中的一个进程并启动。
wakeAll
方法中以依次取出waitQueue
中所有的进程并启动。
期间注意开关中断,防止数据结构变化不一致,详细过程见注释。
1.2.3 代码实现
public Condition2(Lock conditionLock) {
this.conditionLock = conditionLock;
waitQueue = new LinkedList<>();
}
public void sleep() {
// Lib.assertTrue(conditionLock.isHeldByCurrentThread());
conditionLock.release();
boolean preState = Machine.interrupt().disable();//关中断
waitQueue.add(KThread.currentThread());//将当前线程加入到waitQueue中
KThread.currentThread().sleep();//让当前线程睡眠
Machine.interrupt().restore(preState);//恢复中断
conditionLock.acquire();
}
public void wake() {
Lib.assertTrue(conditionLock.isHeldByCurrentThread());
boolean preState = Machine.interrupt().disable();//关中断
if(!waitQueue.isEmpty()){//唤醒waitQueue中的一个线程
KThread a = waitQueue.removeFirst();//取出waitForQueue中一个线程
a.ready();//将取出的线程启动
}
Machine.interrupt().restore(preState);//恢复中断
}
1.2.4 测试
将所有Condition
替换为Condition2
后,join
正常运行即可证明编写正确
1.3 Alarm
1.3.1 题目要求
实现waitUntil(long x)方法来完成Alarm类,线程调用waitUntil暂停自己的执行,直到时间至少提前到现在+ x。
1.3.2 解决方案
WaitForAlarmThread
类维护线程和他所要等待的时间
Waituntil
方法先计算出唤醒线程的时间,在将线程加入等待链表中并睡眠
Nachos系统每隔500个ticks
调用一次timerInterrupt
方法,遍历等待链表中的线程,判断是否到达等待时间,如果到达,则线程移出并唤醒。
(注:对于ticks
的详细解读,见之后的拓展部分)
1.3.3 代码实现
class WaitForAlarmThread{
long wakeTime;
KThread thread;
public WaitForAlarmThread(long wakeTime,KThread thread){
this.wakeTime=wakeTime;
this.thread=thread;
}
}
public void waitUntil(long x)
{
// for now, cheat just to get something working (busy waiting is bad)
boolean preState = Machine.interrupt().disable();//关中断
long wakeTime = Machine.timer().getTime()+x;//计算唤醒的时间
WaitForAlarmThread waitForAlarmThread = new WaitForAlarmThread(wakeTime, KThread.currentThread());
waitForAlarmThreadList.add(waitForAlarmThread);//将线程加入到等待链表中
KThread.sleep();//让该线程睡眠
Machine.interrupt().restore(preState);//恢复中断
}
public void timerInterrupt()
{
boolean preState = Machine.interrupt().disable();//关中断
for(WaitForAlarmThread t : waitForAlarmThreadList){
if(t.wakeTime<=Machine.timer().getTime()){//如果达到唤醒时间,将其从链表中移除并唤醒该线程
waitForAlarmThreadList.remove(t);
t.thread.ready();
}
}
KThread.currentThread().yield();
Machine.interrupt().restore(preState);//恢复中断
}
1.3.4 测试
public static void AlarmTest() {
KThread a = new KThread(new Runnable() {
public void run() {
System.out.println("线程a启动");
for (int i = 0; i < 5; i++) {
if (i == 2) {
System.out.println("线程a sleep,now:" + Machine.timer().getTime() + ", expect: after 1000 clicks");
Alarm.alarm().waitUntil(1000);
System.out.println("线程a wake,now:" + Machine.timer().getTime());
}
System.out.println(" thread 1 looped " + i + " times");
// KThread.currentThread().yield();
}
}
});
a.fork();
System.out.println("\n测试Alarm:");
for (int i = 0; i < 5; i++) {
if (i == 2) {
System.out.println("thread main sleep,now:" + Machine.timer().getTime() + ", expect: after 8000 clicks");
Alarm.alarm().waitUntil(8000);
System.out.println("thread wake, now:" + Machine.timer().getTime());
}
System.out.println(" thread 0 looped " + i + " times");
KThread.currentThread().yield();
}
}
测试Alarm:
thread 0 looped 0 times
线程a启动
thread 1 looped 0 times
thread 1 looped 1 times
线程a sleep,now:30, expect: after 1000 clicks
thread 0 looped 1 times
thread main sleep,now:50, expect: after 8000 clicks
线程a wake,now:1540
thread 1 looped 2 times
thread 1 looped 3 times
thread 1 looped 4 times
thread wake, now:8080
thread 0 looped 2 times
thread 0 looped 3 times
thread 0 looped 4 times
Machine halting!
1.4 Communicator
1.4.1 题目要求
使用条件变量(不要使用信号量!)实现一个单词消息的同步发送和接收(也称为ada风格的会合)。使用操作、void speak(int word)和int listen()实现通信器类。
1.4.2 解决方案
用继承自Condition2
类的ComunicatorCondition
类作为条件变量,并重写了sleep
方法:判断是否存在对应的speaker或listener完成通讯,没有则睡眠
speaker调用speak方法,speaker数目+1,先检查有无listener,如果没有则睡眠,有listener或者被唤醒后,唤醒一个listener,完成通讯,listener数目-1
listener调用listen方法,listener数目+1,先检查有无speaker,如果没有则睡眠,有speaker或者被唤醒后,唤醒一个speaker,完成通讯,speaker数目-1
1.4.3 代码实现
public void sleep() {
if (getNum() == 0) {
String header = isSpeaker ? "speaker" : "listener";
System.out.println(header + "Num=" + communicator.getListenerNum() + " , " + header + " wait");
condition.sleep();
}
}
public int getNum() {
if(isSpeaker)
return communicator.getSpeakerNum();
else
return communicator.getListenerNum();
}
public void speak(int word) {
boolean preState = Machine.interrupt().disable();
lock.acquire();
speakerNum++;
canSpeak.sleep();
canListen.wake();
this.word = word;
listenerNum--;
lock.release();
Machine.interrupt().restore(preState);
}
public int listen() {
boolean preState = Machine.interrupt().disable();
lock.acquire();
listenerNum++;
canListen.sleep();
canSpeak.wake();
speakerNum--;
lock.release();
Machine.interrupt().restore(preState);
return word;
}
1.4.4 测试
public static void SpeakTest() {
System.out.println("\n测试Communicator类:");
Communicator c = new Communicator();
new KThread(new Speaker(c)).setName("Speaker").fork();
for (int i = 0; i < 5; ++i) {
System.out.println("listener listening " + i);
int x = c.listen();
System.out.println("listener listened, word = " + x);
KThread.yield();
}
}
测试Communicator类:
listener listening 0
listener listened, word = -1
speaker speaking word:0
listener listening 1
listener listened, word = -1
listener listening 2
listener listened, word = -1
listener listening 3
listener listened, word = -1
listener listening 4
listener listened, word = -1
Machine halting!
1.5 PriorityScheduler
1.5.1 题目要求
通过完成PriorityScheduler类在Nachos中实现优先级调度。
1.5.2 解决方案
ThreadState
储存线程和他的优先级,用一个hashset
来记录等待的线程
calculateEffectivePriority
方法计算线程的有效优先级,遍历等待队列中的线程,找出队列中所有线程中最大的有效优先级,即为线程的有效优先级
waitForAccess
方法将线程加入等待队列,并重新计算有效优先级
acquire
设置队列的队列头
nextThread
获得下一个线程
1.5.3 代码实现
public KThread nextThread() {
Lib.assertTrue(Machine.interrupt().disabled());
// implement me
ThreadState x = pickNextThread();//下一个选择的线程
if(x == null)//如果为null,则返回null
return null;
acquire(x.thread);
return x.thread;
}
public int calculateEffectivePriority() {
int res = priority;
for(PriorityQueue acquired : acquiredQueues) {
//比较acquired中的所有等待队列中的所有线程的优先级
ThreadState ts = acquired.wait.peek();
if(ts != null && ts.effectivePriority > res) {
res = ts.effectivePriority;
break;
}
}
res = priority >= res ? priority : res;
return res;
}
public int getEffectivePriority() {
if(waitQueue != null && !waitQueue.transferPriority)
return priority;
// implement me
int res = calculateEffectivePriority();
if(waitQueue!=null && res != effectivePriority) {
(waitQueue).wait.remove(this);
(waitQueue).wait.add(this);
}
effectivePriority = res;
if(waitQueue!=null && waitQueue.lockholder != null)
if(waitQueue.lockholder.effectivePriority < res)
waitQueue.lockholder.effectivePriority = effectivePriority;
return res;
}
public void waitForAccess(PriorityQueue waitQueue) {
// implement me
Lib.assertTrue(Machine.interrupt().disabled());
if(this.waitQueue != waitQueue) {
release(waitQueue);
this.waitQueue = waitQueue;
waitQueue.add(this);
if(waitQueue.lockholder != null)
waitQueue.lockholder.getEffectivePriority();
}
}
public void acquire(PriorityQueue waitQueue) {
// implement me
Lib.assertTrue(Machine.interrupt().disabled());
if(waitQueue.lockholder != null)
waitQueue.lockholder.release(waitQueue);
waitQueue.wait.remove(this);//如果这个队列中存在该线程,删除
waitQueue.lockholder = this;
acquiredQueues.add(waitQueue);
this.waitQueue = null;
getEffectivePriority();
// Lib.assertTrue(waitQueue.isEmpty());
}
1.5.4 测试
public static void PriorityTest() {
boolean status = Machine.interrupt().disable();//关中断,setPriority()函数中要求关中断
System.out.println();
final KThread a = new KThread(new KThread.PingTest(1)).setName("thread1");
new PriorityScheduler().setPriority(a, 2);
System.out.println("thread1的优先级为:" + new PriorityScheduler().getThreadState(a).priority);
KThread b = new KThread(new KThread.PingTest(2)).setName("thread2");
new PriorityScheduler().setPriority(b, 4);
System.out.println("thread2的优先级为:" + new PriorityScheduler().getThreadState(b).priority);
KThread c = new KThread(new Runnable() {
public void run() {
for (int i = 0; i < 5; i++) {
if (i == 2)
a.join();
System.out.println(" thread 3 looped " + i + " times");
// KThread.currentThread().yield();
}
}
}).setName("thread3");
new PriorityScheduler().setPriority(c, 6);
System.out.println("thread3的优先级为:" + new PriorityScheduler().getThreadState(c).priority);
a.fork();
b.fork();
c.fork();
Machine.interrupt().restore(status);
}
线程3在循环到第2次时,join线程1,于是线程1先执行,线程1执行完之后,就绪的进程有线程2和线程3,因为线程3优先级比线程2高,故先执行线程3
thread1的优先级为:2
thread2的优先级为:4
thread3的优先级为:6
thread 3 looped 0 times
thread 3 looped 1 times
thread 1 looped 0 times
thread 1 looped 1 times
thread 1 looped 2 times
thread 1 looped 3 times
thread 1 looped 4 times
thread 3 looped 2 times
thread 3 looped 3 times
thread 3 looped 4 times
thread 2 looped 0 times
thread 2 looped 1 times
thread 2 looped 2 times
thread 2 looped 3 times
thread 2 looped 4 times
Machine halting!
1.6 Boat
1.6.1 题目要求
一些夏威夷成人和儿童正试图从Oahu到Molokai。不幸的是,他们只有一艘船,可以最大限度地携带两个孩子或一个成年人(但不是一个孩子和一个成年人)。这条船可以划回瓦胡岛,但它需要一名驾驶员这样做。
安排一个解决方案,把所有人从Oahu胡转移到Molokai。可以假设至少有两个孩子。
1.6.2 解决方案
根据题意,Adult和Child并没有什么区别,需要注意的是,不允许一个船上出现1+1的情形,所以应该先考虑2+0,再考虑2+1,如果先考虑1+1,可能没有剩余的等待者。另外M岸必须至少有一个Adult和一个Child等待,以备返程接人。
流程比较复杂,我画了活动图见下方,child同理。
图中没有相似说明的是,要注意各岸各类人数目的加减,必须一致,并且需要在其他线程被唤醒并继续运行之前执行。另外,因为有waitOnO和canGo两道condition,如果wake了waitOnO,但没有释放当前线程的控制权,那么接下来即便wake了canGo,sleep在wait上的线程因为没有继续执行,并没有“上船”,这样有些数目的加减就有问题,所以要注意各个需要yield的地方。
1.6.3 代码实现
Adult、Child同理
static void AdultSailOnce() {
adultWaitAtO.sleep();
if (empty) {
AdultPilot();
if (childNumO > 0 || adultNumO > 0) {
if (childNumO == 1 && adultNumO == 0) {
childNumM--;
childWaitAtM.wake();
adultNumM++;
adultWaitAtM.sleep();
} else if (adultNumM > 0) {
adultWaitAtM.wake();
adultWaitAtM.sleep();
}
}
} else {
canGo.sleep();
bg.AdultRideToMolokai();
if(adultNumM == 0) {
adultNumM++;
adultWaitAtM.sleep();
}
}
//回
if ((childNumO > 0 || adultNumO > 0) && !(childNumO == 1 && adultNumO == 0)) {
bg.AdultRowToOahu();
adultNumO++;
if (adultNumO > 0)
adultWaitAtO.wake();
else
childWaitAtO.wake();
empty = true;
AdultSailOnce();
}
}
static void AdultPilot() {
empty = false;
adultNumO--;
if (adultNumO >= 1) {
adultNumO--;
wake(adultWaitAtO);
if (childNumO >= 1) {
childNumO--;
wake(childWaitAtO);
} else if (adultNumO >= 1) {
adultNumO--;
wake(adultWaitAtO);
}
} else if (childNumO >= 2) {
wake(childWaitAtO);
wake(childWaitAtO);
childNumO -= 2;
}
canGo.wakeAll();
bg.AdultRowToMolokai();
lock.release();
KThread.yield();
lock.acquire();
}
static void wake(Condition condition) {
condition.wake();
lock.release();
KThread.yield();
lock.acquire();
}
1.6.4 测试
public static void selfTest() {
BoatGrader b = new BoatGrader();
// System.out.println("\n Testing Boats with only 2 children");
// begin(0, 2, b);
// System.out.println("\n Testing Boats with 2 children, 1 adult");
// begin(1, 2, b);
System.out.println("\n Testing Boats with 3 children, 3 adults");
begin(3, 3, b);
}
Testing Boats with only 2 children
A child has forked.
A child has forked.
Child rowing to Molokai.
Child arrived on Molokai as a passenger.
Testing Boats with 2 children, 1 adult
An adult as forked.
A child has forked.
A child has forked.
Adult rowing to Molokai.
Child arrived on Molokai as a passenger.
Child arrived on Molokai as a passenger.
Testing Boats with 3 children, 3 adults
An adult as forked.
An adult as forked.
An adult as forked.
A child has forked.
A child has forked.
A child has forked.
Adult rowing to Molokai.
Adult arrived on Molokai as a passenger.
Child arrived on Molokai as a passenger.
Adult rowing to Oahu.
Adult rowing to Molokai.
Adult arrived on Molokai as a passenger.
Child arrived on Molokai as a passenger.
Child rowing to Oahu.
Child rowing to Molokai.
Child arrived on Molokai as a passenger.
Machine halting!
1.7 拓展:Timer
Alarm
中有提到Ticks
,那么Nachos是怎么模拟时钟周期的?
Stats
类中以整数保存了TimerTicks=500
(中断检查间隔)、UserTick=1
(用户级别单位tick)、KernelTick=10
(内核级别单位tick)、totalTicks
、kernelTicks
、userTicks
(后三个记录累计ticks
);
在Timer
类的scheduleInterrupt
方法中,
private void scheduleInterrupt() {
int delay = Stats.TimerTicks;
delay += Lib.random(delay / 10) - (delay / 20);
privilege.interrupt.schedule(delay, "timer", timerInterrupt);
}
根据中断间隔,(正态分布地)模拟生成了一个下一个interrupt
检查的时间,
private void timerInterrupt() {
scheduleInterrupt();
scheduleAutoGraderInterrupt();
lastTimerInterrupt = getTime();
if (handler != null)
handler.run();
}
当到了设置好的时间点(total=last+delay)时,调用timerInterrupt
方法,其中run
之前设置好的handler
(在Alarm
类中我们设置过)。
那么ticks又是怎么累加的呢?
public void run() {
Lib.debug(dbgProcessor, "starting program in current thread");
registers[regNextPC] = registers[regPC] + 4;
Machine.autoGrader().runProcessor(privilege);
Instruction inst = new Instruction();
while (true) {
try {
inst.run();
} catch (MipsException e) {
e.handle();
}
privilege.interrupt.tick(false);
}
}
在Processor
类的run
方法死循环中不断调用tick
方法,追溯下去
private void tick(boolean inKernelMode) {
Stats stats = privilege.stats;
if (inKernelMode) {
stats.kernelTicks += Stats.KernelTick;
stats.totalTicks += Stats.KernelTick;
} else {
stats.userTicks += Stats.UserTick;
stats.totalTicks += Stats.UserTick;
}
if (Lib.test(dbgInt))
System.out.println("== Tick " + stats.totalTicks + " ==");
enabled = false;
checkIfDue();
enabled = true;
}
我们就可以看到累加的地方了。
2 多道程序设计
2.1 实现文件系统调用
2.1.1 题目要求
实现文件系统调用(syscall.h中记录的create,open,read,write,close和 unlink)。最好在UserProcess.java中和halt代码放到一起。请注意没有实现文件系统,相反只是在为用户进程提供访问已经被实现的文件系统的能力。
2.1.2 解决方案
首先,按照题目要求,halt只有系统第一个进程可以调用,所以每个用户进程维护一个记录PID
的变量,UserProcess
类维护静态变量nextPID
,构造函数中nextPID
赋予新进程PID
值,然后自增1,需要注意这里要加锁。
其次,第一个进程初始化文件描述符存储表openFiles
,位置0和1分别对应控制台的输入和输出。
文件系统调用,统一不抛出异常而以返回-1替代。
对于create,首先从内存中获取文件名,然后判断文件表中是否已存在该文件,不存在再试图获得一个空的文件描述符位置,如果有可用的,open一个文件,并根据该位置修改文件表;
对于read和write,首先判断给的文件描述符是否合法(0~ MAX_FILE_OPEN,并且文件表对应的没有被占用),然后建立缓冲区,调用读写虚拟内存的方法,返回读或写的字节数;
对于close,同样判断是否合法,合法关闭对应文件,并将文件表中对应位置置空;
对于unlink,获得文件名,文件不存在不必删除,文件存在调用remove方法直接删除该文件,经过尝试,不需要写额外的文件打开数的判断,remove会自动去做。
2.1.3 代码实现
public UserProcess() {
sharedStateLock.acquire();
PID = nextPID++;
runningProcesses++;
sharedStateLock.release();
if (openFiles == null) {
openFiles = new OpenFile[MAX_FILE_OPEN];
setStandardIO();
}
// Exit/Join syncronization
waitingToJoin = new Condition(joinLock);
}
public void setStandardIO() {
openFiles[0] = UserKernel.console.openForReading();
openFiles[1] = UserKernel.console.openForWriting();
}
public void initRegisters()
{
Processor processor = Machine.processor();
// by default, everything's 0
for (int i = 0; i < processor.numUserRegisters; i++)
processor.writeRegister(i, 0);
// initialize PC and SP according
processor.writeRegister(Processor.regPC, initialPC);
processor.writeRegister(Processor.regSP, initialSP);
// initialize the first two argument registers to argc and argv
processor.writeRegister(Processor.regA0, argc);
processor.writeRegister(Processor.regA1, argv);
}
private int getUnusedFileDescriptor() {
for (int i = 0; i < this.openFiles.length; ++i)
if (this.openFiles[i] == null)
return i;
return -1;
}
private int handleCreate(final int pName) {
final String fileName = readVirtualMemoryString(pName, MAX_FILENAME_LENGTH);
for (int i = 0; i < this.openFiles.length; i++) {
if (this.openFiles[i] != null && this.openFiles[i].getName().equals(fileName)) {
return i;
}
}
final int fileDescriptor = this.getUnusedFileDescriptor();
if (fileDescriptor == -1)
return -1;
final OpenFile file = ThreadedKernel.fileSystem.open(fileName, true);
this.openFiles[fileDescriptor] = file;
return fileDescriptor;
}
private int handleOpen(final int pName) {
final int fileDescriptor = this.getUnusedFileDescriptor();
if(fileDescriptor == -1)
return -1;
final String fileName = readVirtualMemoryString(pName, MAX_FILENAME_LENGTH);
final OpenFile file = ThreadedKernel.fileSystem.open(fileName, false);
this.openFiles[fileDescriptor] = file;
return fileDescriptor;
}
private int handleRead(final int fileDescriptor, final int pBuffer, final int count) {
if (fileDescriptor < 0 || fileDescriptor >= MAX_FILE_OPEN || openFiles[fileDescriptor] == null)
return -1;
final OpenFile file = this.openFiles[fileDescriptor];
final byte[] tmp = new byte[count];
final int numBytesRead = file.read(tmp, 0, count);
final int numBytesWritten = writeVirtualMemory(pBuffer, tmp, 0, numBytesRead);
return numBytesWritten;
}
private int handleWrite(final int fileDescriptor, final int pBuffer, final int count) {
if (fileDescriptor < 0 || fileDescriptor >= MAX_FILE_OPEN || openFiles[fileDescriptor] == null)
return -1;
final OpenFile file = this.openFiles[fileDescriptor];
final byte[] tmp = new byte[count];
final int numBytesToWrite = readVirtualMemory(pBuffer, tmp);
return file.write(tmp, 0, numBytesToWrite);
}
private int handleClose(final int fileDescriptor) {
if (fileDescriptor < 0 || fileDescriptor >= MAX_FILE_OPEN || openFiles[fileDescriptor] == null)
return -1;
openFiles[fileDescriptor].close();
openFiles[fileDescriptor] = null;
return 0;
}
private int handleUnlink(final int fileAddress) {
String fileName = readVirtualMemoryString(fileAddress, MAX_FILENAME_LENGTH);
if(fileName==null)
return 0; //文件不存在,不必删除
return ThreadedKernel.fileSystem.remove(fileName) ? 0: -1;
}
private int handleHalt(){
// halt() is noop if not root process
if (PID != 0)
return 0;
Machine.halt();
Lib.assertNotReached("Machine.halt() did not halt machine!");
return 0;
}
2.2 多用户进程
2.2.1 题目要求
实现对多程序的支持。我们提供给您的代码仅限于一次运行一个用户进程。您的工作是使其可用于多个用户进程。
2.2.2 解决方案
首先,UserKernel
里维护一个全局队列freePages
,存放当前空闲的物理页号,初始化的时候,使freePages
包含所有的页号,向用户进程提供申请空闲页的接口acquirePages
;loadSections()
中尝试获取空闲页,并且将每个段对应的只读状态赋予页表对应页,每个段也需要载入相应的物理页号。
用户进程需要保存自己拥有的页数numPages
,并且维护pageTable
将用户的虚拟地址映射到物理地址。 TranslationEntry
类代表一个单一的虚拟到物理页映射。
之后读写内存时需要检查虚拟初始页号和结束页号是否合法,写的时候还要判断该页是否为readOnly
。
读写内存时,实现要获取processor
的内存地址,然后要获取要读写的字节数,并且要获得要读写的物理地址,然后进行字节数组的copy覆盖。要注意,读写的时候,页表对应页use
位要置为1,写的时候dirty
位也要置为真。
同样需要注意锁的获取和释放。
2.2.3 代码实现
public void initialize(String[] args) {
super.initialize(args);
console = new SynchConsole(Machine.console());
allocateMemoryLock = new Lock();
//初始化的时候,使memoryLinkedList包含所有的页号
freePages = new LinkedList<TranslationEntry>();
for (int currentPageIndex = 0; currentPageIndex < Machine.processor().getNumPhysPages(); currentPageIndex++)
freePages.add(new TranslationEntry(0, currentPageIndex, false, false, false, false));
Machine.processor().setExceptionHandler(() -> exceptionHandler());
}
public static TranslationEntry[] acquirePages(int numPages){
TranslationEntry[] returnPages = null;
allocateMemoryLock.acquire();
if (!freePages.isEmpty() && freePages.size() >= numPages) {
returnPages = new TranslationEntry[numPages];
for (int i = 0; i < numPages; ++i) {
returnPages[i] = freePages.remove();
returnPages[i].valid = true;
}
}
allocateMemoryLock.release();
return returnPages;
}
public static void releasePages(TranslationEntry[] pageTable) {
allocateMemoryLock.acquire();
for (TranslationEntry te : pageTable) {
freePages.add(te);
te.valid = false;
}
allocateMemoryLock.release();
}
protected boolean loadSections(){
System.out.println("numPages is " + numPages + " free pages is " + UserKernel.getFreePages().size());
pageTable = UserKernel.acquirePages(numPages);
if (pageTable == null) {
coff.close();
Lib.debug(dbgProcess, "\tinsufficient physical memory");
UserKernel.getAllocateMemoryLock().release();
return false;
}
// load sections(段),一个段是由很多页组成
for (int s = 0; s < coff.getNumSections(); s++)
{
CoffSection section = coff.getSection(s);
Lib.debug(dbgProcess, "\tinitializing " + section.getName()
\+ " section (" + section.getLength() + " pages)");
int firstVPN = section.getFirstVPN();
for (int i = 0; i < section.getLength(); i++) {
pageTable[firstVPN + i].readOnly = section.isReadOnly();
section.loadPage(i, pageTable[firstVPN + i].ppn);
}
}
return true;
}
2.3 实现系统调用
2.3.1 题目要求
实现系统调用(exec, join和exit,也记录在syscall.h中)。
2.3.2 解决方案
对于exec,先判断地址和coff文件名是否合法,然后再从虚拟内存页中读完整的所有参数(根据参数数目判断是否都读到了),读到的是参数的地址,还需要再读出字符串内容,然后创建子进程,让子进程执行相应的coff(最开始的进程应该是控制台)。
对于join,每个进程要保存父进程的地址,以及他的所有子进程的地址,两个都要维护。按照要求,进程只能join自己的子进程,因此join时要先判断是不是自己的子进程,是的话sleep在子进程的condition上,子进程退出时唤醒自己的condition,该进程继续执行,将该子进程从自己维护的队列中移除,最后子进程异常返回0,否则将子进程的返回值写入内存,并返回1。
对于exit,先告知父进程要将自己置空,并且让所属的子进程的父进程为空,然后关闭所有打开的文件,释放虚拟内存中占用的段,然后唤醒所有正在等待它的进程,另外如果是唯一一个进程就halt,最后调用KThread.finish()
终止自己。
最后handleSyscall
方法中需要根据调用参数,执行相应的系统调用。
2.3.3 代码实现
private int handleExec(int fileNamePtr, int argc, int argvPtr) {
// Verify that passed pointers are valid
if (!validAddress(fileNamePtr) || !validAddress(argv))
return terminate();
// Read filename from virtual memory
String fileName = readVirtualMemoryString(fileNamePtr, maxSyscallArgLength);
if (fileName == null || !fileName.endsWith(".coff"))
return -1;
// Gather arguments for the new process
String arguments[] = new String[argc];
// Read the argv char* rray
int argvLen = argc * 4 // Number of bytes in the array
byte argvArray[] = new byte[argvLen];
if (argvLen != readVirtualMemory(argvPtr, argvArray)) {
// Failed to read the whole array
return -1;
}
// Read each argument string from the char* aray
for (int i = 0; i < argc; i++) {
// Get char* pinter for next position in array
int pointer = Lib.bytesToInt(argvArray, i4);
// Verify that it is valid
if (!validAddress(pointer))
return -1;
// Read in the argument string
arguments[i] = readVirtualMemoryString(pointer, maxSyscallArgLength);
}
// New process
UserProcess newChild = newUserProcess()
newChild.parent = this;
// Remember our children
children.put(newChild.PID, new ChildProcess(newChild));
// Run and be free!
newChild.execute(fileName, arguments);
return newChild.PID;
}
private int handleExit(Integer status) {
joinLock.acquire();
// Attempt to inform our parent that we're exiting
if (parent != null)
parent.notifyChildExitStatus(PID, status);
// Disown all of our running children
for (ChildProcess child : children.values())
if (child.process != null)
child.process.disown();
children = null;
// Loop through all open files and close them, releasing references
for (int fileDesc = 0; fileDesc < openFiles.length; fileDesc++)
if (openFiles[fleDesc] == null)
handleClose(fileDesc);
// Free virtual memory
unloadSections();
// Wakeup anyone who is waiting for us to exit
exited = true;
waitingToJoin.wakeAll();
joinLock.release();
// Halt the machine if we were the last process
sharedStateLock.aquire();
if (--runningProcesses = 0)
Kernel.kernel.terminate();
sharedStateLock.rlease();
// Terminate current thread
KThread.finish()
return 0;
}
protected void notifyChildExitStatus(int childPID, Integer childStatus) {
ChildProcess child = children.get(childPID);
if (child == null)
return;
// Remove reference to actual child so it can be garbage collected
child.process = null;
// Record child's exit status for posterity
child.returnValue = childStatus;
}
protected void disown() {
parent = null;
}
private int handleJoin(int pid, int statusPtr) {
if (!validAddress(statusPtr))
return terminate();
ChildProcess child = children.get(pid);
// Can't join on non-child!
if (child == null)
return -1;
// Child still running, try to join
if (child.process != null)
child.process.joinProcess();
// We can safely forget about this child after join
children.remove(pid);
// Child will have transfered return value to us
// Child exited due to unhandled exception
if (child.returnValue == null)
return 0;
// Transfer return value into status ptr
writeVirtualMemory(statusPtr, Lib.bytesFromInt(cild.returnValue));
// Child exited cleanly
return 1;
}
private void joinProcess() {
joinLock.acquire();
while (!exited)
waitingToJoin.sleep();
joinLock.release();
}
public int handleSyscall(int syscall, int a0, int a1, int a2, int a3) {
switch (syscall) {
case syscallHalt:
return handleHalt();
case syscallExit:
return handleExit(a0);
case syscallExec:
return handleExec(a0, a1, a2);
case syscallJoin:
return handleJoin(a0, a1);
case syscallCreate:
return handleCreate(a0);
case syscallOpen:
return handleOpen(a0);
case syscallRead:
return handleRead(a0, a1, a2);
case syscallWrite:
return handleWrite(a0, a1, a2);
case syscallClose:
return handleClose(a0);
case syscallUnlink:
return handleUnlink(a0);
default:
Lib.debug(debugProcess, Unknown syscall " + syscall);
Lib.assertNotReached("nknown system call!");
}
return 0;
}
2.4 彩票调度
2.4.1 题目要求
实现彩票调度程序(将其放置在thread / LotteryScheduler.java中)。注意,该类扩展了PriorityScheduler,您应该能够重用该类的大多数功能;彩票调度程序不应包含大量附加代码。唯一的主要区别是用于从队列中选择线程的机制:持有彩票,而不只是选择具有最高优先级的线程。您的彩票调度员应实施优先捐赠。(请注意,由于这是彩票调度程序,因此优先级倒置实际上不会导致饥饿!但是,无论如何,您的调度程序必须进行优先级捐赠。)
2.4.2 解决方案
与Phase1已经实现过的优先级调度相比,彩票调度的唯一区别在于选择下一个就绪进程和优先级的计算上。
彩票调度选择下一个进程pickNextThread
时,需要计算所有等待它的线程的优先级之和,然后以他为最大数随机一个整数,再遍历一遍等待队列,累计计算优先级,当累加和大于随机到的整数时,就是要选中的线程。(就是所有线程一次排在数轴上,优先级为其宽度,然后在这个数轴上随机取一个点,落在谁那取谁)。
彩票调度的优先级是所有等待线程的优先级之和,之前PriorityScheduler
是取最大值。
避免重复造轮子,LotteryScheduler
可以继承PriorityScheduler
,他的内部类PriorityQueue
继承PriorityScheduler.PriorityQueue
,ThreadState
同理,这样就可以复用和重写了。
2.4.3 代码实现
public class LotteryScheduler extends PriorityScheduler{
public LotteryScheduler() { super(); }
public ThreadQueue newThreadQueue(boolean transferPriority) {
return new PriorityQueue(transferPriority);
}
protected class PriorityQueue extends PriorityScheduler.PriorityQueue {
PriorityQueue(boolean transferPriority) {
super(transferPriority);
}
protected PriorityScheduler.ThreadState pickNextThread() {
if (wait.isEmpty())
return null;
int totTicket = 0;
PriorityScheduler.ThreadState[] list = wait.toArray(new PriorityScheduler.ThreadState[wait.size()]);
for (PriorityScheduler.ThreadState ts : list) {
totTicket += ts.getEffectivePriority();
}
Random random = new Random();
int randomLottery = random.nextInt(totTicket);
int t = 0;
PriorityScheduler.ThreadState toPick = null;
for (PriorityScheduler.ThreadState ts : list) {
t += ts.getEffectivePriority();
if (t >= randomLottery) {
toPick = ts;
break;
}
}
wait.remove(toPick);
return toPick;
}
}
protected class ThreadState extends PriorityScheduler.ThreadState {
public ThreadState(KThread thread) { super(thread); }
public int calculateEffectivePriority() {
int res = priority;
for (PriorityScheduler.PriorityQueue acquired : acquiredQueues)
for (PriorityScheduler.ThreadState ts : acquired.wait)
res += ts.effectivePriority;
return res;
}
}
}
2.5 拓展:底层实现逻辑
UserKernel
开启时进行初始化,initialize
方法中除了实例化控制台console
、分配空闲页之外还做了一件事,设置exceptionHandler
,每次控制台执行命令其实就是抛出了异常.
public void initialize(String[] args) {
super.initialize(args);
cosole =new SynchConsole(Machine.cosole());
allocateMemoryLock =new Lock();
//初始化的时候,使memoryLinkedList包含所有的页号
freePages =new LinkedList<TranslationEntry>();
for (int currentPageIndex = 0; currentPageIndex < Machine.Processor()getNumPhysPages(); currentPageIndex++)
freePages.add(new TranslationEntry(0, currentPageIndex, false, false, false, false));
Machine.Processor()setExceptionHandler(() -> exceptionHandler());
}
在UserProcess. handleException
中读取寄存器内容,调用之前我们完成的handleSyscall
方法,然后讲调用结果写入寄存器,PC+1
public void handleException(int cause) {
Processor processor = Machine.Processor()
switch (cause) {
case Processor.exeptionSyscall:
int result = handleSyscall(processor.readRegister(Processor.reV0)
processor.readRegister(Processor.reA0),
processor.readRegister(Processor.reA1),
processor.readRegister(Processor.reA2),
processor.readRegister(Processor.reA3)
);
processor.writeRegister(Processor.reV0, esult);
processor.advancePC();
break;
default:
Lib.deug(bProcess, Unexpected exception: " +
Processor.exeptionNames[cuse]);
Lib.assertNotReached("nexpected exception");
}
}
那么异常在哪抛出的呢?
在Processor
类的run
方法中,
public void run() {
Lib.debug(bProcessor, "starting program in current thread");
registers[reNextPC] = registers[rePC] + 4;
Machine.auoGrader()runProcessor(privilege);
Instruction inst = new Instruction();
while (true) {
try {
inst.run();
} catch (MipsException e) {
e.handle();
}
privilege.interrupt.tick(false);
}
}
处理器实例化Instruction
并让其在无限循环中run
,该run
方法中依次调用了fetch、decode、execute、writeBack四个函数,分别是从寄存器中取PC、解析、执行和将执行结果写回。其中,除了decode都会抛出MipException
,尤其注意execute
中取得的操作码不是Mips中的命令,那么就是用户进程指令,抛出异常。
于是,MipsException
被catch到并被handle
,将调用之前提到的exceptionHandler
方法,然后handleException
,进行系统调用。
还有问题,寄存器里的PC
是谁写的?
UserKernel
在开始run
的时候实例化了一个shell
进程,
public void run() {
super.run();
UserProcess process = UserProcess.newUserProcess();
String shellProgram = Machine.geShellProgramName();
Lib.assertTrue(pocess.execute(shellProgram, new String[]{}));
KThread.curentThread().finish()
}
execute
中将调用load
方法,根据名称打开coff文件并实例化,然后为PC赋值,并将参数写入内存。
coff文件在test文件夹中,关联对应c文件的编译结果,我们在sh.c
中可以看到在while(1)循环中读取写入的字符串,并尝试执行。在runline
函数中,对于命令的多种情况进行判断,如果是exit、halt、join则执行相应的同名方法,如果都不是,执行exec
。
在syscall.h
文件中,我们可以看到所有的指令符和调用方法;start.s
由汇编语言写成,它为java和c中相应函数建立了存根stub
,由此java程序可以调用c编译好的方法。
2.6 测试截图
系统调用没有测试代码,几个命令调用使用成功,即可说明代码实现正确;
首先在Machine.java中修改naochos路径;
nachosDirectory = new File(baseDirectory, “Nachos\src\nachos\test”);
在test文件夹下新建一个文本,随便输入几个字符。
make & nachos后输入q,退出selfTest到shell
// 这里注意字符编码格式不能是gbk,否则编译报错,另外我记得换行格式也得是Unix的LF格式
cat命令:
// 可以看到cat命令这正常执行;
mv命令:
// 1.txt复制到2.txt, 1.txt被删除
cp命令
// 2.txt复制出1.txt
rm命令:
// 2.txt被删除
exit命令:
彩票调用在conf中修改完,用PriorityScheduler的测试方式运行正确,也说明成功(因为随机的原因没有固定的结果)。
3 Cache和虚拟内存
3.1 TLB
3.1.1 题目要求
Implement software-management of the TLB, with software translation via an inverted page table.
3.1.2 解决方案
这个实验中第一部分需要关注的是一个软件实现的转换检测缓冲区(TLB)。Phase 2中使用的页表来简化内存分配,并很好的隔离了地址空间的错误,以防影响到其他程序。这个实验中,进程不关心页表的细节,只处理由软件实现的页表条目缓存,也就是TLB。当给定一个内存地址时,进程先从TLB中查看是否已经存在了地址对应的虚拟内存页到物理内存页的映射,如果存在的话就直接使用,否则将陷入系统内核进行调页操作。
3.1.3 代码实现
主要的实现代码还是写在VMKernel和VMProcess两个类中。
在VMKernel中定义两个变量coremap和invertedPageTable,分别表示物理内存页号标号的内存条目和反转内存页表。coremap负责存放TLB条目和pid的对应关系,invertedPageTable负责存放页表键(虚拟内存地址和pid组成)与内存条目的映射关系。
public TranslationEntry retrievePage(int vpn) {
TranslationEntry entry;
if ((entry = kernel.pinIfExists(vpn, PID)) == null) {
entry = kernel.pageFault(vpn, PID);
}
Lib.assertTrue(etry != null);
return entry;
}
当进程需要访问一个内存地址时,通过retrievePage这个方法来获取内存页条目。先从内核中判断虚拟内存页号是否已经存在在页表中。如果存在,就标记占用并返回,若不存在则页错误,抛出异常,并在processor.regBadVAddr寄存器中读到发生页错误的虚拟内存页号,进入TLB未命中的方法处理。
TranslationEntry pinIfExists(int vpn, int pid) {
MemoryEntry entry;
memLock.acquire();
if ((entry = invertedPageTable.get(new TableKey(vpn, pid))) != null) {
if (!entry.pinned) {
entry.pinned = true;
pinnedCount++;
}
}
memLock.release();
if (entry == null) {
return null;
} else {
return entry.translationEntry;
}
}
如果TLB中存在物理地址和需求vaddr对应物理地址相同或者已经失效的页表项,则写入新的页表项。如果都不满足条件,就使用随机替换的方法。
private void handleTLBMiss(int vaddr) {
if (!validAddress(vaddr)) {
return;
}
TranslationEntry retrievedTranslationEntry = retrievePage(Processor.pageFromAddress(vddr));
boolean unwritten = true;
for (int i = 0; i < Machine.Processor()getTLBSize() && unwritten; i++) {
TranslationEntry entry = Machine.processor()readTLBEntry(i);
if (entry.ppn == retrievedTranslationEntry.ppn) {
Machine.processor()writeTLBEntry(i, retrievedTranslationEntry);
unwritten = false;
} else if (!entry.valid) {
Machine.processor()writeTLBEntry(i, retrievedTranslationEntry);
unwritten = false;
}
}
// 采用随机替换
if (unwritten) {
int randomIndex = Lib.random(Machine.processor()getTLBSize());
TranslationEntry oldEntry = Machine.Processor()readTLBEntry(randomIndex);
if (oldEntry.dirty || oldEntry.used) {
kernel.propagateEntry(oldEntry.ppn, oldEntry.used, oldEntry.dirty);
}
Machine.Processor().writeTLBEntry(randomIndex, retrievedTranslationEntry);
}
kernel.unpin(retrievedTranslationEntry.ppn);
}
3.2 Demand Paging
3.2.1 题目要求
Implement demand paging of virtual memory. For this, you will need routines to move a page from disk to memory and from memory to disk. You should use the Nachos stub file system as backing store.
3.2.2 解决方案
第二部分是关于交换空间。交换空间允许物理内存页被交换到硬盘中,来实现理论上无限内存空间的效果。如果发生了页错误,内核会检查自己的页表来判断需求页是否在物理内存中。如果不在,会先从硬盘中调页,页表项指向新调的页,装入页表项并恢复程序运行。当然内核也要在物理内存中找到可以写入的区域,必要时会将修改过的内存页写回到硬盘中。这个机制的性能极大依赖了调页策略,经常使用的是最近访问的页不被调出的算法。并且如果内存页没有被修改过,就没必要写回硬盘,这样可以节省很多时间。
3.2.3 代码实现
我们在Kernel中定义一个Swap类,负责处理交换文件的创建、调入、调出等操作,交换文件通过StubFileSystem来创建名为”swapfile”的文件,存放在/test文件夹下。
private class Swap {
Swap() {
swapFile = fileSystem.oen("swapfile", true);
}
void swapOut(MemoryEntry entry) {
if (entry.translationEntry.valid) {
SwapEntry swapEntry = null;
TableKey key = new TableKey(entry.translationEntry.vpn, entry.pid);
swapLock.acquire();
if (entry.translationEntry.dirty || !swapTable.containsKey(key)) {
if (freeList.size() > 0) {
swapEntry = freeList.removeFirst();
swapEntry.readOnly = entry.translationEntry.readOnly;
} else {
swapEntry = new SwapEntry(maxTableEntry++, entry.translationEntry.readOnly);
}
swapTable.put(key, swapEntry);
}
swapLock.release();
if (swapEntry != null) {
Lib.assertTrue(
swapFile.write(swapEntry.swapPageNumber * Processor.pageSize,
Machine.Processor()getMemory(),
entry.translationEntry.ppn * Processor.pageSize,
Processor.pageSize
) == Processor.pageSize
);
}
}
}
void swapIn(int pid, int vpn, int ppn) {
swapLock.acquire();
SwapEntry swapEntry = swapTable.get(new TableKey(vpn, pid));
swapLock.release();
if (swapEntry != null) {
// 保证从虚拟内存中读出的数据读满一个页面大小
Lib.assertTrue(sapFile.read(
swapEntry.swapPageNumber * Processor.pageSize,
Machine.Processor()getMemory(),
ppn * Processor.pageSize,
Processor.pageSize
) == Processor.pageSize);
coreMap[ppn].translationEntry.readOnly = swapEntry.readOnly;
}
}
boolean pageInSwap(int vpn, int pid) {
swapLock.acquire();
boolean inSwap = swapTable.containsKey(new TableKey(vpn, pid));
swapLock.release();
return inSwap;
}
void freePages(int maxVpn, int pid) {
swapLock.acquire();
SwapEntry entry;
for (int i = 0; i < maxVpn; i++) {
if ((entry = swapTable.get(new TableKey(i, pid))) != null) {
freeList.add(entry);
}
}
swapLock.release();
}
void cleanup() {
swapFile.close();
fileSystem.rmove(swapFile.getName());
}
private OpenFile swapFile;
// 当前交换文件中可用的地址
private LinkedList<SwapEntry> freeList = new LinkedList<>();
// 进程页号和虚拟内存中地址的映射表
private HashMap<TableKey, SwapEntry> swapTable = new HashMap<>();
// 保证对swapTable的原子性操作
private Lock swapLock = new Lock();
private int maxTableEntry = 0;
private class SwapEntry {
SwapEntry(int swapPageNumber, boolean readOnly) {
this.swapPageNumber = swapPageNumber;
this.readOnly = readOnly;
}
int swapPageNumber;
boolean readOnly;
}
}
在发生页错误时,内核会现在交换页表中查询进程所需的内存页是否存在,如果存在则从交换页表中读取交换页表项。
使用clockAlgorithm进行页选择调出时,会选择clockHand指向的下一个最近未使用或失效页面,将其使用swapOut()调出。
private MemoryEntry clockAlgorithm() {
memLock.acquire();
while (pinnedCount == coreMap.length) {
// 等待有空位
allPinned.sleep();
}
propagateAndFlushTLB(false);
MemoryEntry entry;
// 找到一个未被锁的页
while (true) {
idx = (idx + 1) % coreMap.length;
entry = coreMap[idx];
if (entry.pinned) {
continue;
}
// 失效的页面可以使用
if (entry.pid == -1 || !entry.translationEntry.valid) {
break;
}
// 最近使用过,可能会再次被使用
if (entry.translationEntry.used) {
entry.translationEntry.used = false;
} else {
break;
}
}
// 锁住该页
entry.pinned = true;
pinnedCount++;
invalidateTLBEntry(idx);
MemoryEntry entry1 = null;
if (entry.pid > -1) {
entry1 = invertedPageTable.remove(new TableKey(entry.translationEntry.vpn, entry.pid));
}
memLock.release();
if (entry1 != null) {
swap.swapOut(entry);
}
return entry;
}
TranslationEntry requestFreePage(int vpn, int pid) {
MemoryEntry page = clockAlgorithm();
// 初始化这块内存区域
int pagePos = Processor.makeAddress(pge.translationEntry.ppn, 0);
Arrays.fill(Mchine.Processor()getMemory(), pagePos, pagePos + Processor.pageSize, byte) 0);
page.translationEntry.vpn = vpn;
page.translationEntry.valid = true;
page.pid = pid;
// 加进页表
insertIntoTable(vpn, pid, page);
return page.translationEntry;
}
3.3 测试截图
进行测试时,可以使用/test中提供的matmult.coff来执行测试调页过程。分析matmult可知这是个矩阵乘法的程序,会用到大量内存,因此有频繁访存的过程。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fULoiL8N-1579134618334)(C:\Users\Stranded\AppData\Roaming\Typora\typora-user-images\image-20191225150123769.png)]
执行成功,说明调用正确。