操作系统课程设计实验报告proj1.docx

上传人:b****6 文档编号:8473841 上传时间:2023-01-31 格式:DOCX 页数:22 大小:192.20KB
下载 相关 举报
操作系统课程设计实验报告proj1.docx_第1页
第1页 / 共22页
操作系统课程设计实验报告proj1.docx_第2页
第2页 / 共22页
操作系统课程设计实验报告proj1.docx_第3页
第3页 / 共22页
操作系统课程设计实验报告proj1.docx_第4页
第4页 / 共22页
操作系统课程设计实验报告proj1.docx_第5页
第5页 / 共22页
点击查看更多>>
下载资源
资源描述

操作系统课程设计实验报告proj1.docx

《操作系统课程设计实验报告proj1.docx》由会员分享,可在线阅读,更多相关《操作系统课程设计实验报告proj1.docx(22页珍藏版)》请在冰豆网上搜索。

操作系统课程设计实验报告proj1.docx

操作系统课程设计实验报告proj1

操作系统课程设计报告

班级:

团队成员:

 

目录

目录2

一、Nachos综述3

二、实验要求:

建立线程系统3

2.1Task1.1实现KThread.join()3

2.1.1题目要求3

2.1.2题目分析与实现方案3

2.1.3关键点与难点4

2.1.4实现代码4

2.2Task 1.2实现条件变量5

2.2.1题目要求5

2.2.2题目分析与实现方案5

2.2.3关键点与难点6

2.2.4实现代码7

2.3Task 1.3 完成Alarm类7

2.3.1题目要求7

2.3.2题目分析与实现方案8

2.3.3关键点与难点9

2.3.4实现代码9

2.4Task1.4条件变量实现同步消息10

2.4.1题目要求10

2.4.2题目分析与实现方案10

2.4.3关键点与难点11

2.4.4实现代码11

2.5Task1.5实现优先级调度13

2.5.1题目要求13

2.5.2题目分析与实现方案13

2.5.3关键点与难点14

2.5.4实现代码15

2.6Task1.6条件变量解决过河问题16

2.6.1题目要求16

2.6.2题目分析与实现方案17

2.6.3关键点与难点19

2.6.4实现代码19

三、测试结果23

一、Nachos综述

Nachos是一款教学用的操作系统平台,他的全名叫做“Not Another Completely Heuristic Operating System”,Nachos的运行必须借助于宿主机,它通过软件模拟了各种硬件系统,包括中断系统、存储系统、磁盘文件、网络等。

它的运行是可以跟踪的,因此,我们可以一步一步的观察操作系统是如何运行的。

 Nachos操作系统本身只提供了一套框架,很多地方的实现都需要我们自己来完善,因此我们可以通过修改其源代码,来丰富和增强Nachos操作系统的功能.更可以在完善这些功能的同时,了解操作系统的内部运行机制。

二、实验要求:

建立线程系统

2.1Task1.1:

实现KThread.join()

2.11题目要求 

实现ImplementKthread.join()函数。

其它的线程不必调用join函数,但是如果join()函数被调用的话,也只能被调用一次。

对join()函数第二次调用的执行结果是不被定义的,即使第二次调用的线程与第一次调用的线程不同。

无论有没有被join,一个进程都能够正常结束。

2.1.2题目分析与实现方案

分析:

join函数的作用即为等待调用此函数线程运行完毕。

当前线程A调用另一个线程(处于就绪状态)B的join函数时,即A执行B.join()时(A和B在Nachos中均为Kthread类型对象),A被挂起,直到B运行结束后,join函数返回,A才能继续运行。

如果这个线程已经结束,马上返回。

且join()函数只能被调用一次,第二次调用不能保证返回,且调用join()函数的线程必须不能是当前线程。

Join()函数结束时唤醒在等待队列中的所有线程,因此需要实现join()方法和修改finish()方法。

方案:

(1)Kthread的join()中的Lib.assertTrue(this!

=currentThread)已经实现线程只能调用一次join()方法,根据要求,在调用join()方法时,让当前运行线程休眠,并将当前运行的线程加入到一个阻塞队列中。

(2)在线程结束时,finish()函数循环唤醒所有被阻塞的线程。

Finish()函数在run()函数返回时自动调用,同样可以被直接调用。

但当前唯一运行的线程不能被finish()函数立即结束,只有当其他线程运行时才可结束。

2.1.3关键点与难点 

由于java派生于抽象类的对象无法实例化,在运行时很可能出现空指针异常。

只能直接利用抽象类的对象调用相关方法。

2.1.4实现代码

publicvoidjoin(){

Lib.debug(dbgThread,“Joiningtothread:

“+toString());

//判断要调用的进程是否为当前进程(线程只能调用一次join()方法)

Lib.assertTrue(this!

=currentThread);

//系统关中断,当前线程休眠

_ooleanintStatus=Machine.interrupt().disable();

if(status!

=statusFinished){

//调用另一个要调用的进程,将当前运行的线程加入到一个阻塞队列中。

waitForJoin.waitForAccess(currentThread);

//当前进程睡眠等待被调用进程结束

sleep();

}

Machine.interrupt().restore(intStatus);

}

publicstaticvoidfinish(){

Lib.debug(dbgThread,“Finishingthread:

“+currentThread.toString());

//系统关中断

Machine.interrupt().disable();//当前进程运行结束的标志

Machine.autoGrader().finishingCurrentThread();

Lib.assertTrue(toBeDestroyed==null);

//表示当前进程要结束了

toBeDestroyed=currentThread;

//当前进程的状态被修改为运行结束

currentThread.status=statusFinished;

//调用等待队列上的第一个进程

KthreadwaitThread;

while((waitThread=currentThread.waitForJoin.nextThread())!

=null){//唤醒等待队列上所有被阻塞的进程

waitThread.ready();

}

sleep();

}

2.2Task 1.2实现条件变量 

2.2.1题目要求

利用中断有效和无效所提供的原子性直接实现条件变量。

我们已经提供类似的例子实例实现信号量。

你要按此提供类似的条件变量的实现,不能直接利用信号量来实现(你可以使用lock,虽然它间接地调用了信号量)。

在你完成时要提供条件变量的两种实现方法。

你的第二种条件变量实现要放在nachos.threads.Condition2中。

2.2.2题目分析与实现方案

分析:

条件变量使我们可以睡眠等待某种条件出现,是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:

一个线程等待”条件变量的条件成立”而挂起;另一个线程使”条件成立”(给出条件成立信号)。

为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

方案:

threads.Lock类提供了锁以保证互斥。

在临界代码区的两端执行Lock.acquire()和Lock.release()即可保证同时只有一个线程访问临界代码区。

条件变量建立在锁之上,由threads.Condition实现,用来保证同步。

每一个条件变量拥有一个锁变量。

(1)sleep()在条件变量的控制下sleep()函数自动释放相关锁并进入睡眠状态,直到令一个线程用wake()唤醒它,需要在函数的开始和结尾处关或允许中断保证原子性。

当前线程必须拥有相关锁,阻塞进程在sleep()函数返回之前线程将会自动再次拥有锁。

即调用这个方法的线程将自己挂起到等待队列,阻塞自己的同时将自己拥有的锁交出去,之后等待锁的分配。

拿到锁的线程再次拥有权限接受检验。

(2)wake()唤醒条件变量队列中第一个线程,在试验中利用从线程队列中取出线程用Kthread.ready()实现唤醒,加入就绪队列。

(同样要在wake函数利用中断保证原子性)

(3)wakeall()函数的实现依赖于wake()。

只需不断地wake挂在条件变量上的线程直到队列为空为止。

2.2.3关键点与难点

题目要求直接实现条件变量,不能直接利用信号量。

所以互斥锁必须要有,它是线程们有秩序的接受条件变量检验的保证。

等待队列必须要有,不满足条件的线程都要挂在上面。

2.2.4实现代码

publicstaticvoidsleep(){

Lib.debug(dbgThread,“Sleepingthread:

“+currentThread.toString());

Lib.assertTrue(Machine.interrupt().disabled());

if(currentThread.status!

=statusFinished)

//锁住当前线程

currentThread.status=statusBlocked;

//运行下一个线程

runNextThread();

}

publicvoidwake(){

Lib.assertTrue(conditionLock.isHeldByCurrentThread());

if(!

waitQueue.isEmpty())

((Semaphore)waitQueue.removeFirst()).V();

}

publicvoidwakeAll()

{

Lib.assertTrue(conditionLock.isHeldByCurrentThread());

while(!

waitQueue.isEmpty())

wake();

}

2.3 Task 1.3 完成Alarm类

2.3.1题目要求

完成Alarm类,通过waitUntil(long x)方法实现。

一个线程通过调用waitUntil(long x)方法将自己挂起,一直到经过x时间再被唤醒。

这对现实操作很有用,例如,光标的周期闪烁。

线程经过x时间后唤醒,但不需要立刻运行,而是加入readyqueue中。

不建立任何多余的线程实现waitUntil(),只需要修改waitUntil()中断处理程序和计时器。

waitUntil()不受限于任何一个线程,任何线程可以调用它实现被挂起。

2.3.2题目分析与实现方案

分析:

Alarm类使用硬件定时器提供抢占,并允许线程挂起到某个时间。

分配的新闹钟设置机器的定时器中断处理程序实现本闹钟的回调,同时Nachos只在有一个计时器的情况下正常运行。

定时器中断处理程序被称为机器计时器定期(大约每500时钟周期)。

当前线程产生后,如果有另一个必须运行的线程,则强制上下文切换。

当前线程睡眠至少x时间周期,在定时器中断处理程序中将其唤醒。

当现在时间(currenttime)>=(WaitUntilcalledtime)+(x)时,线程必须被唤醒(在调度准备集)在第一个定时器中断的产生时。

方案:

(1)与Alarm类有关的是machine.Timer类,它在大约每500个时钟滴答使调用回调函数(由Timer.setInterruptHandler函数设置)。

因此,Alarm类的构造函数中首先要设置该回调函数Alarm.timerInterrupt()。

timerInterrupt()方法在每一次timer产生时间中断时遍历队列,检查队列中的时间状态,当线程到了等待的时间就把线程从队列中取出放入就绪队列。

(2)waitUntil()方法使用了一个队列可以存放线程以及唤醒时间,这个队列以时间为序的有序队列。

每次调用时,把当前线程和唤醒时间加入队列,等待唤醒。

在调用waitUntil(x)函数时,首先得到关于该线程的信息(线程:

当前线程,唤醒时间:

当前时间+x),该线程设定唤醒时间并阻塞,并调用sleep操作使当前线程挂起,放入队列中。

在时钟回调函数中(大约每500个时钟间隔调用一次)有一次中断,中断时则依次检查队列中的每个对象,将队列中此时应当唤醒的进程加入就绪队列。

如果唤醒时间大于当前时间,则将该对象移出队列并执行wake操作将相应线程唤醒。

(3)Waiter()是一个用来管理和存储多个Kthread及其唤醒时间的内部类,属性分别为该Kthread与其唤醒时刻waketime。

2.3.3关键点与难点

线程调用waitUntil方法之后会终止,该线程设定唤醒时间并阻塞,直到传入的时间之后才可以执行。

线程只是放入就绪队列,等待分配。

可以使用一个线程队列,但是不能产生额外的线程。

 

Timer每过500个clock ticks会产生一个timeInterrupt。

timeInterupt时,会运行handle()线程,将handler线程功能设置为将队列中此时应当唤醒的进程加入就绪队列。

2.3.4实现代码

classWaiter{

Waiter(longwakeTime,Kthreadthread){

this.wakeTime=wakeTime;

this.thread=thread;

}

privatelongwakeTime;//唤醒时间

privateKthreadthread;//等待的线程

}

privateLinkedListwaitlist;//存放线程以及唤醒时间

}

publicvoidwaitUntil(longx){

//系统关中断

_ooleanintStatus=Machine.interrupt().disable();

//fornow,cheatjusttogetsomethingworking(busywaitingisbad)

//确定唤醒的时间

longwakeTime=Machine.timer().getTime()+x;

Waiterwaiter=newWaiter(wakeTime,Kthread.currentThread());

//将线程加入到等待队列上

waitlist.add(waiter);

System.out.println(“线程:

”+Kthread.currentThread().getName()

+“\t休眠时间:

”+Machine.timer().getTime()+“\t下次唤醒时间:

+wakeTime);//当前线程设定唤醒时间并阻塞,挂起

Kthread.sleep();//开中断

Machine.interrupt().restore(intStatus);

}

2.4Task1.4条件变量实现同步消息

2.4.1题目要求

使用条件变量来实现一个字长信息的发送和接收同步。

使用voidspeak(intword)和intlisten()函数来实现通讯(Communicator)类的通讯操作。

Speak函数具有原子性,在相同地Communicator类中等待listen函数被调用,然后将此字发生给listen函数。

一旦传送完毕,两个函数都返回(listen函数返回此字)。

2.4.2题目分析与实现方案

解决方案要满足使用同一个通讯对象实例中多个speaker和listener能够相互通讯。

(注意:

这种情况等于0字长的缓冲区;既然缓冲区没用空间,那么需要生产者和消费者直接进行交互,要求他们相互等待)。

每一个通讯实例只能使用一个lock类,如果使用多于一个lock类,会将事情复杂化。

每个Communicator拥有的锁(保证操作的原子性)和与该锁联系的两个条件变量用于保证speaker和listener间的同步。

在speak函数中,首先检查是否已经有speaker在等待(speaknum>0)或无listener等待,满足这两种情况就挂起。

若不满足,则设置变量,准备数据并唤醒一个listener。

在listen函数中,增加一个listener后,首先,然后这个问题其实是一个缓冲区长度为0的生产者/消费者问题。

Speak():

先获得锁,然后进行判断,如果没有听者等待,就要把说者放入队列然后睡眠。

如果有听者等待,就要唤醒一个听者,然后传递消息,最后释放锁。

Listen():

先获得锁,然后进行判断尝试唤醒speaker,如果没有说者等待,就要把听者放入队列然后睡眠。

如果有说者等待,就要唤醒一个说者,将自己挂起以等待speaker准备好数据再将自己唤醒,然后传递消息,最后释放锁。

2.4.3关键点与难点

当有多个speaker和listener时,它们也只能是一对一的,即一个speaker只能将数据发送到一个listener,一个listener也只能接收来自一个speaker的数据,其余的speakers和listeners都需要等待。

多个speaker阻塞时需要保存每一个speaker的word,阻塞的speaker挂在Condition对象的Queue队列上。

因为无缓冲区,listener唤醒说者后要先挂起,消息在speaker准备好后才传递。

2.4.4实现代码

publicclassCommunicator{

publicCommunicator(){

lock=newLock();

speaker=newCondition2(lock);

listener=newCondition2(lock);

}

privateLocklock;//互斥锁

privateintword=0;

privatestaticintspeakercount=0;//说者数量

privatestaticintlistenercount=0;//听者数量

LinkedListWordqueue=newLinkedList();//保存说者话语的队列

Condition2speaker;//说者条件变量

Condition2listener;//听者条件变量

/*先获得锁,然后进行判断,如果没有听者等待,就要把说者放入队列然后睡眠。

如果有听者等待,就要唤醒一个听者,然后传递消息,最后释放锁*/

publicvoidspeak(intword){//系统关中断

_ooleanintStatus=Machine.interrupt().disable();

//拿到锁

lock.acquire();//没有听者,说者睡眠,队列存储说者的话

if(listenercount==0){

speakercount++;

Wordqueue.offer(word);

speaker.sleep();//尝试唤醒听者

listener.wake();//说者队列人数减一

speakercount--;

}else{//准备消息,唤醒听者

Wordqueue.offer(word);

listener.wake();

}//释放锁

lock.release();//系统开中断

Machine.interrupt().restore(intStatus);

System.out.println(Kthread.currentThread().getName()+“发出信息“+word);

return;

}

publicintlisten(){//系统关中断

_ooleanintStatus=Machine.interrupt().disable();//获得锁

lock.acquire();

if(speakercount!

=0){

speaker.wake();

listener.sleep();

}else{

listenercount++;

listener.sleep();

listenercount--;

}

System.out.println(Kthread.currentThread().getName()+“收到信息“

+Wordqueue.getFirst());

System.out.println();

lock.release();

Machine.interrupt().restore(intStatus);

returnWordqueue.poll();

}

2.5Task1.5实现优先级调度

2.5.1题目要求

通过完成实现PriorityScheduler优先级调度策略。

所有的调度程序都是继承自Scheduler类,所以必须实现getPriority(),getEffectivePriority()和setPriority()方法。

2.5.2题目分析与实现方案

分析:

Nachos系统已经提供了一个简单的轮转调度器,采用简单的FIFO队列进行调度。

优先级调度的传统算法如下:

每个线程拥有一个优先级(在Nachos中,优先级是一个0到7之间的整数,默认为1)。

在线程调度时,调度程序选择一个拥有最高优先级的处于就绪状态的线程运行。

这种算法的问题是可能出现“饥饿”现象。

为解决上述优先级反转问题,需要实现一种“让出”优先级的机制(PriorityDonation)。

getEffectivePriority()就是为了解决优先级翻转而设的,也就是说当出现一个优先级高的线程等待低优先级线程时,若存在另外的高优先级线程而导致低优先级线程永远不能执行的情况,该低优先级线程调用该函数返回它持有的锁上等待的所有线程中优先级中最高的一个给该优先级线程,以确保它执行。

方案:

在引入优先级的同时就需要引入一个ThreadState对象,它用来把线程和优先级绑定在一起。

而且内部还有一个队列用来记录等待它的线程。

在ThreadState类中增加两个LinkedList<>类表,存放PriorityQueue。

一个表用来记录该线程所占用资源的优先队列,另一个表用来记录该线程所想占有的资源的优先队列。

占用资源的队列作为发生优先级反转时,捐献优先级计算有效优先级的来源依据,想占有的资源的队列用来为线程声明得到资源做准备。

getEffectivePriority():

计算有效优先级时,遍历等待队列中所用线程的有效优先级,找出最大的优先级。

首先要在PriorityQueue类下创建个装有Kthread的LinkedList,即等待队列waitQueue,声明一个effectivePriority,遍历waitQueue,找出priority最大的那个Kthread,将它的priority赋给effectivePriority,然后返回即可。

而在队列类中最重要的就是nextThread()方法,它

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高等教育 > 工学

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1