天津科技大学操作系统实验.docx
《天津科技大学操作系统实验.docx》由会员分享,可在线阅读,更多相关《天津科技大学操作系统实验.docx(43页珍藏版)》请在冰豆网上搜索。
天津科技大学操作系统实验
2012-2013学年第一学期
计算机操作系统实验报告
专业:
软件工程
班级:
111033
学号:
11103325
姓名:
孟翔
提交日期:
2013年11月27日
实验一Windows多线程..................................................1实验二Windows线程同步机制...........................................5
实验三Windows线程通信................................................9
实验四银行家算法模拟.................................................15
实验五页面置换算法模拟...............................................22
实验一Windows多线程
【开发语言及实现平台或实验环境】
C++/C#
MicrosoftVisualStudio6.0/MicrosoftVisualStudio.NET
【实验目的】
(1)进一步理解操作系统的并发性;
(2)了解Windows线程创建方法,并通过查阅资料理解各参数的含义;
(3)了解多线程程序设计方法,并进行简单应用。
【实验要求】
(1)逐程序进行简要分析、运行各程序并仔细阅读注释;
(2)查阅MSDN或其他资料,掌握相关系统调用使用方法和参数含义;
(3)完成实验报告。
【相关知识】
一、核对象
(一)核对象的概念
核对象是核分配的一个存块,这种存块是一个数据结构,表示核对象的各种特征。
并且只能由核来访问。
应用程序若需要访问核对象,需要通过操作系统提供的函数来进行,不能直接访问核对象(Windows 从安全性方面来考虑的)。
核对象通过 Create* 来创建,返回一个用于标识核对象的句柄,这些句柄(而不是核对象)可在创建进程围使用,不能够被传递到其他进程中被使用。
(二)核对象使用的计数
因为核对象的所有者是核,而不是进程,所以何时撤销核对象由核决定,而核做这个决定的依据就是该核对象是否仍然被使用。
那么如何判断核对象是否被使用呢?
可以通过核对象的“使用计数”属性,一旦这个值变成0了,核就可以释放该对象了。
(三)创建核对象
1、进程与句柄表
每个进程在初始化的时候,将被分配一个句柄表,该句柄表中只存储核对象的句柄,不存储用户对象的句柄。
句柄表的详细结构微软没有公布,但是大致包含三个容:
核对象句柄,核对象地址,访问屏蔽标志。
2、创建核对象及操作系统部机制
利用 CreateSomeObject 的函数来创建核对象。
调用该函数的时候核就为该对象分配一个存块,并进行初始化,然后核再扫描该进程的句柄表,初始化一条记录并放在句柄表中。
3、进程中使用核对象的部机制
假设函数 F 使用某个核对象,其参数为 Handle1,则该函数部需要查找该进程的句柄表,找出参数句柄对应的记录,然后才能使用该核对象。
(四)关闭核对象
无论进程怎样创建核对象,在不使用该对象的时候都应当通过boolCloseHandle(HANDLEhobj) 来向操作系统声明结束对该对象的访问。
为什么叫声明呢?
是因为此时也许还有其他进程对该对象的访问,操作系统可能并不立即释放该对象。
操作系统需要做的是:
从进程的句柄表中删除该核对象的记录,另外再考察该核对象的使用计数以决定是否需要释放该对象。
(五) 核对象的共享
说到共享,与之孪生的就是共享权限。
Windows 核对象的共享有三种方式:
1、继承式共享(父子进程间)
只有当进程是父子关系的时候,才能使用此种方式的共享。
特别要注意的是继承的是核对象的句柄,核对象本身是不具备继承性。
要达到这种继承的效果需要做以下几件事:
在进程创建核对象的时候,需要一个安全结构 sa ( SECURITY_ATTRIBUTES 类型,以向 OS 声明对象的访问方式)作为参数。
继承式共享需要将结构的成员 sa.bInheritHandle 设置为 TRUE 。
此时 OS 部的处理式将进程的句柄表中的该对象的访问屏蔽字段设置成“可继承”。
在创建子进程( CreateProcess 函数)时,设置创建参数 bInheritHandles 为 TRUE 。
表示被创建的子进程可以继承父进程中的所有可继承核对象。
OS 部的处理是:
复制父进程句柄表中的记录到子进程的句柄表中,并使用相同的句柄值;为核对象的使用计数器加 1 。
特别说明:
子进程能够继承的的核对象仅局限于父进程创建它的时候所拥有的可继承核对象。
子进程诞生后,父进程再搞出什么可继承的东西,子进程是不能用的。
这就需要在子进程中使用继承的核对象的时候需要慎重,以确定核对象是否已被继承了。
利用 SetHandleinformation 方法可以随时修改核对象句柄的一些属性,目前公开的句柄属性有两种,一种是该句柄是否能被继承,另一种是该句柄是否能被关闭。
2、同名共享
同名共享,不需要共享进程之间存在父子关系。
但局限于核对象是否支持这种共享方式。
创建核对象的Create 函数中是否包含 pszName 是该核对象是否支持同名共享的标志。
方法一:
当 Process1 通过 CreateObject(…”someName”)创建了一个名字为 someName 的核对象后, Process2 也调用了 CreateObject(…”someName”),此时核的动作是:
在全局中查询发现已经存在 someName1 的对象;为 Process2 的句柄表添加一条 Ojbect 的记录,使用的句柄不确定;为 someName这个 Object 的引用计数器加 1 。
方法二:
Process2 使用 OpenObject(…”someName” )的方式来获得对名someName的 Object 的句柄。
用这种 Open 方法的时候,需要提供一个参数让 OS 鉴权,以判定是否能够以参数指定的方式来访问核对象。
3、复制核对象的句柄的方式共享
跨进程边界的核对象共享的另外一个方法是通过 DuplicateHandle 来复制核对象句柄。
如果要将 ProcessS 中的对象拷贝到 ProcessT 中则调用 DuplicateHandle 的进程一定要有对这两个进程的访问权,即句柄表中拥有这两个进程核对象的句柄记录。
二、线程的一般概念
进程只是线程的容器,从来不执行任何东西;线程总是在某个进程中被创建;线程在进程的地址空间中执行代码;线程们共享进程中的所有核对象。
三、线程的创建
HANDLECreateThread(
PSECURITY_ATTRIBUTESpsa,
DWORDcbStack,
PTHREAD_START_ROUTINEpfnStartAddr,
PVOIDpvParam,
DWORDfdwCreate,
PDWORDpdwThreadID);
调用CreateThread后,OS 进行如下几个动作:
生成一个线程核对象;在进程空间为线程分配堆栈空间。
因为线程的环境同于其所在进程的环境,所以创建的线程可以访问进程中的所有资源,包括线程中所有的核对象。
四、线程销亡
(一) 终止线程的方式
线程函数返回(最好使用这个方式,可以保证:
线程中创建的 C++ 对象正常析构; OS 释放线程堆栈存; OS 将线程的退出码设置为线程函数的返回值;系统将递减该线程核对象的的使用计数器【如果此时还有其他引用 …… ,见下面说明】。
)
调用 ExitThread (不能释放 C++ 对象,所以最好不要使用这个方式。
另外,如果非要调用也应当调用编译器推荐的,如 _endThread 【 Windows 核心编程 P127 】)
同进程的其他线程(包括主线程)调用 TerminateThread (被撤销线程得不到通知,不能释放资源,尽量避免这种方式。
另外这个函数是个异步函数,返回时,线程不保证已经被撤销,如果要观察线程是否被撤销,应当使用 WaitForSingleObject )包含线程的进程终止(应当避免这种方式)
(二) 线程退出时 OS 的行为
线程的所有用户对象被释放。
线程的退出码从 STILL_ACTIVE 改为传递给 ExitThread 或 TerminateThread 的代码
线程核对象的状态改为“已通知”
如果线程为进程中的最后一个线程,则 OS 将进程当作已终止运行
线程核对象的引用计数器减 1 (一旦线程终止了,其他引用改线程核对象将不能够处理改线程的句柄,但是可以通过调用 GetExitcodeThread 来检查 hThread 代表的线程是否已经终止运行了。
)
【实验步骤】
(1)阅读和理解1-1.cpp文件中的程序,多次运行1-1.cpp,认真观察结果。
然后将main函数中注释掉的Sleep语句让其可用,即将其前面的注释号删掉,再多次运行,认真观察结果。
比较修改程序前后运行结果发生的变化,并分析其原因。
(2)阅读和理解1-2.cpp文件中的程序,多次运行1-2.cpp,认真观察结果。
思考为什么1-2.cpp中可以不需要Sleep语句就可看到各线程都被调度了。
(3)阅读和理解1-3.cpp文件中的程序,运行1-3.cpp,认真观察结果。
然后将main函数中注释掉的Sleep语句让其可用,即将其前面的注释号删掉,再多次运行,认真观察结果。
再将两个子函数中注释掉的Sleep语句让其可用,再多次运行,认真观察结果,可能会出现销售出0号票的情况。
比较修改程序前后运行结果发生的变化,并分析其原因。
【实验结果与分析】
(1)阅读和理解1-1.cpp文件中的程序
运行一次1-1.cpp的结果如下:
运行多次1-1.cpp的结果如下:
将main函数中注释掉的Sleep语句让其可用,运行结果为:
分析原因:
Sleep(0)的作用为语句可观察线程1和主线程并发执行。
输出结果“mainthreadisrunning/thread1isrunning”。
没有添加的线程1运行结束只输出“mainthreadisrunning”
(2)阅读和理解1-2.cpp文件中的程序
运行一次1-2.cpp文件的结果如下:
运行多次1-2.cpp文件的结果如下:
分析原因:
1-2.cpp文件使用的是时间片轮转方法调度的主线程、线程1、线程2.因此不需要sleep语句就可将主线程调度。
因为在两个线程中存在共享变量,因此执行结果出现不可再现性。
(3)阅读和理解1-3.cpp文件中的程序
运行一次1-3.cpp的结果为:
将主进程的sleep可用,运行结果为:
将两个子函数中注释掉的Sleep语句让其可用,再多次运行:
多次运行修改后的程序结果为:
分析原因:
子函数添加sleep语句后会在之前结果上多出销售0号票的情况,这是因为程序中只要票号大于0便继续执行线程。
实验二Windows线程同步机制
【开发语言及实现平台或实验环境】
C++/C#
MicrosoftVisualStudio6.0/MicrosoftVisualStudio.NET
【实验目的】
(1)了解Windows线程同步机制;
(2)了解互斥体,并通过查阅资料理解互斥体对象的使用方法;
(3)了解事件,并通过查阅资料理解事件对象的使用方法;
(4)了解关键区,并通过查阅资料理解关键区对象的使用方法;
(5)了解信号量,并通过查阅资料理解信号量对象的使用方法;
(6)利用Windows线程同步机制,模拟生产者消费者问题。
【实验要求】
(1)逐程序进行简要分析、运行各程序并仔细阅读注释;
(2)查阅MSDN或其他资料,掌握相关系统调用使用方法和参数含义;
(3)完成实验报告。
【相关知识】
一、Windows线程同步机制
Windows 下提供了多种核对象实现线程、进程间的同步和互斥,常用的有:
1、关键区(临界区CriticalSection)
关键区不是核对象,在用户态实现了同一进程中线程的互斥。
由于使用时不需要从用户态切换到核心态,所以速度很快( X86 系统上约为 20 个指令周期),但其缺点是不能跨进程同步,同时不能指定阻塞时的等待时间,只能无限等待。
使用关键区的方法则使同步管理的效率更高。
使用时先定义一个CRITICALSECTION结构的排斥区对象,在进程使用之前调用如下函数对对象进行初始化:
VOIDInitializeCriticalSection(LPCRITICAL_SECTION);
当一个线程使用排斥区时,调用函数:
EnterCriticalSection或者TryEnterCriticalSection;
当要求占用、退出排斥区时,调用函数LeaveCriticalSection,释放对排斥区对象的占用,供其他线程使用。
2、互斥体( Mutex )
互斥体实现了和关键区类似的互斥功能,但区别在于:
互斥体是核对象,可以实现跨进程互斥,但需要在用户态和核心态之间切换,速度比关键区慢得多(X86 系统上约为 600 个指令周期),同时可以指定阻塞时的等待时间。
Mutex对象的状态在它不被任何线程拥有时才有信号,而当它被拥有时则无信号。
Mutex对象很适合用来协调多个线程对共享资源的互斥访问。
可按下列步骤使用该对象:
首先,建立互斥体对象,得到句柄:
HANDLECreateMutex();
然后,在线程可能产生冲突的区域前(即访问共享资源之前)调用WaitForSingleObject,将句柄传给函数,请求占用互斥对象:
dwWaitResult=WaitForSingleObject(hMutex,5000L);
共享资源访问结束,释放对互斥体对象的占用:
ReleaseMutex(hMutex);
互斥体对象在同一时刻只能被一个线程占用,当互斥体对象被一个线程占用时,若有另一线程想占用它,则必须等到前一线程释放后才能成功。
3、事件(Event)
事件也是核对象,具有“信号态”和“无信号态”两种状态。
当某一线程等待一个事件时,如果事件为信号态,将继续执行,如果事件为无信号态,那么线程被阻塞。
线程能够指定阻塞时的等待时间。
例如:
只有在通信端口缓冲区收到数据后,监视线程才被激活。
事件对象是用CreateEvent函数建立的。
该函数可以指定事件对象的类和事件的初始状态。
如果是手工重置事件,那么它总是保持有信号状态,直到用ResetEvent函数重置成无信号的事件。
如果是自动重置事件,那么它的状态在单个等待线程释放后会自动变为无信号的。
用SetEvent可以把事件对象设置成有信号状态。
在建立事件时,可以为对象命名,这样其他进程中的线程可以用OpenEvent函数打开指定名字的事件对象句柄。
4、信号量(Semaphore)
信号量是一个资源计数器,当某线程获取某信号量时,信号量计数首先减 1 ,如果计数小于 0 ,那么该线程被阻塞;当某县城释放某信号量时,信号量计数首先加 1 ,如果计数小于或等与 0,那么唤醒某被阻塞的线程并执行之。
对信号量的总结如下:
●如果计数器 m 大于 0 ,表示还有 m 个资源可以访问,此时信号量线程等待队列中没有线程被阻塞,新的线程访问资源也不会被阻塞;
●如果计数器 m 等与 0 ,表示没有资源可以访问,此时信号量线程等待队列中没有线程被阻塞,但新的线程访问资源会被阻塞;
●如果计数器 m 小于 0 ,表示没有资源可以访问,此时信号量线程等待队列中有 abs ( m )个线程被阻塞,新的线程访问资源会被阻塞;
信号量常被用于保证对多个资源进行同步访问。
可按下列步骤使用该对象:
首先,创建信号对象:
HANDLECreateSemaphoreQ;
或者打开一个信号对象:
HANDLEOpenSemaphoreQ;
然后,在线程访问共享资源之前调用:
WaitForSingleObject;
共享资源访问完成后,应释放对信号对象的占用:
ReleaseSemaphoreQ;
二、生产者-消费者模型
生产者-消费者模型是指:
●生产者进行生产将物品放入仓库,同一时间只能有一个生产者将物品放入仓库,如果仓库满,生产者等待。
●消费者从仓库中取出物品,同一时间只能有一个消费者取出物品,如果仓库空,消费者等待;
●生产者将物品放入仓库时消费者不能同时取;
●消费者取物品时生产者不能放入物品;
总之,就是生产者群体或消费者群体部是互斥的,两个群体之间是同步的。
当只有一个生产者、消费者时,由于同一群体部不需要互斥,所以只需在群体之间实 现同步即可。
例如可以使用两个 Event/CriticalSection/Mutex/Semaphore 实现同步;
如果有多个生产者和消费者,那么情况会复杂些,需要一个 Event/CriticalSection/Mutex 实现线程之间的互斥,需要两个 Semaphore 实现两个线程群体间的同步。
【实验步骤】
(1)阅读和理解2-1(mutex).cpp文件中的程序,运行2-1(mutex).cpp,认真观察结果。
然后将两个子函数中注释掉的Sleep语句让其可用,再多次运行,认真观察结果,不会出现销售出0号票的情况。
比较修改程序前后运行结果发生的变化,并分析其原因。
(2)2-2(event).cpp、2-3(critical_section).cpp的处理方式同
(1)。
分析:
修改之前,在指定暂停的时间sleep(1000)Sleep(1000);//要保证售完40票之前线程不退出,thread1和thread2随机售票,出现多种情况;将两个子函数中注释掉的sleep
(1)语句让其可用后,thread1和thread2交替售票,即thread1在其暂停的时间sleep
(1),thread2获得了对共享对象hmutex的所有权,开始售票,同理当thread2在其暂停的时间sleep
(1),thread1获得了对共享对象hmutex的所有权,开始售票,从而实现了交替售票。
(3)阅读和理解2-4(Producer_Consumer).cpp文件中的程序,运行2-4(Producer_Consumer).cpp,认真观察结果,先生产后消费。
然后将两个子函数中注释掉的while语句让其可用,再多次运行,认真观察结果,生产者和消费者保持同步。
比较修改程序前后运行结果发生的变化,并分析其原因。
分析:
修改之前,在指定暂停时间sleep(20),producer和consumer只能执行一次;将两个子函数中注释掉的while语句让其可用后,producer和consumer在指定暂停时间sleep(20),随机循环获得共享对象的所有权,进行生产或消费,从而出现多种结果。
(4)阅读和理解2-4(Producer_Consumer)1.cpp文件中的程序,运行2-4(Producer_Consumer)1.cpp,认真观察结果。
实验三Windows线程通信
【开发语言及实现平台或实验环境】
C++/C#
MicrosoftVisualStudio6.0/MicrosoftVisualStudio.NET
【实验目的】
(1)了解Window线程通信方法;
(2)了解匿名管道,并通过查阅资料理解匿名管道的使用方法;
(3)了解命名管道,并通过查阅资料理解命名管道的使用方法;
【实验要求】
(1)逐程序进行简要分析、运行各程序并仔细阅读注释;
(2)查阅MSDN或其他资料,掌握相关系统调用使用方法和参数含义;
(3)完成实验报告。
【相关知识】
Windows应用程序间数据通讯的基本方式有四种。
最简单的是利用剪切板;另一种是DDE(DynamicDataExchange动态数据交换),它利用一种公共的协议实现两个或多个应用程序之间的通讯;再者是通过存映射文件,存映射可以将一个进程的一段虚拟地址映射为一个文件,然后其它的进程可以共享该段虚拟地址;最后就是通过管道与邮路实现进程间数据通信。
管道(pipe)是进程用来通讯的共享存区域。
一个进程往管道中写入信息,而其它的进程可以从管道中读出信息。
如其名,管道是进程间数据交流的通道。
管道的类型有两种:
匿名管道和命名管道。
匿名管道是不命名的,它最初用于在本地系统中父进程与它启动的子进程之间的通信。
命名管道更高级,它由一个名字来标识,以使客户端和服务端应用程序可以通过它进行彼此通信。
而且,Win32命名管道甚至可以在不同系统的进程间使用,这使它成为许多客户/服务器应用程序的理想之选。
就像水管连接两个地方并输送水一样,软件的管道连接两个进程并输送数据。
一个一个管道一旦被建立,它就可以象文件一样被访问,并且可以使用许多与文件操作同样的函数。
可以使用CreateFile函数获取一个已打开的管道的句柄,或者由另一个进程提供一个句柄。
使用WriteFile函数向管道写入数据,之后这些数据可以被另外的进程用ReadFile函数读取。
管道是系统对象,因此管道的句柄在不需要时必须使用CloseHandle函数关闭。
匿名管道只能单向传送数据,而命名管道可以双向传送。
管道可以以比特流形式传送任意数量的数据。
命名管道还可以将数据集合到称为消息的数据块中。
命名管道甚至具有通过网络连接多进程的能力。
但遗憾的是Windows9X不支持创建命名管道,它只能在WindowsNT系列(如WindowsNT,Windows2000,WindowsXP)的操作系统上创建。
当讨论管道时,通常涉及到两个进程:
客户进程和服务进程。
服务进程负责创建管道。
客户进程连接到管道。
服务进程可以创建一个管道的多个实例,以此支持多个客户进程。
匿名管道用以下函数创建:
BOOLCreatePipe(
PHANDLEhReadPipe,//用于读操作的句柄
PHANDLEhWritePipe,//用于写操作的句柄
LPSECURITY_ATTRIBUTESlpPipeAttributes,//描述安全信息的一个结构
DWORDnSize//管道大小
);
命名管道用以下函数创建:
HANDLECreateNamedPipe(
LPCTSTRlpName,//管道名
DWORDdwOpenMode,//管道打开方式
DWORDdwPipeMode,//管道模式
DWORDnMaxInstances,//该管道最大的实例数量
DWORDnOutBufferSize,//输出缓冲区大小
DWORDnInBufferSize,//输入缓冲区大小
DWORDnDefaultTimeOut,//指定默认的超时时间
LPSECURITY_ATTRIBUTESlpSecurityAttributes//描述安全信息的一个结构
);
管道由以下函数删除:
BOOLCloseHandle(
HANDLEhObject//管道句柄
);
其它的