1、 操作系统实验指导书 计算机科学与软件学院 李建伟 1实验一 进程控制与描述 一、实验目的 通过对 Windows 2000 编程,进一步熟悉操作系统的基本概念,较好地理解 Windows 2000 的结构。通过创建进程、观察正在运行的进程和终止进程的程序设计和调试操作,进一步熟悉操作系统的进程概念,理解 Windows 2000 进程的“一生”。 二、实验环境 硬件环境:计算机一台,局域网环境; 软件环境:Windows 2000 Professional,Visual C+ 6.0 专业版或企业版。 三、实验内容和步骤 第一部分: Windows 2000 Professional 下的
2、GUI 应用程序,使用 Visual C+编译器创建一个 GUI 应用程序,代码中包括了 WinMain()方法,该方法 GUI 类型的应用程序的标准入口点。 在“开始”菜单中单击“程序”- “附件”- “记事本”命令,将程序键入记事本中,并把代码保存为 1-1.cpp。 程序 1-1 Windows 2000 的 GUI 应用程序 / msgbox 项目 # include / 标准的 include / 告诉连接器与包括 MessageBox API 函数的 user32 库进行连接 # pragma comment(lib, “user32.lib” ) / 这是一个可以弹出信息框然后退
3、出的筒单的应用程序 int APIENTRY WinMain(HINSTANCE /* hInstance */ , HINSTANCE /* hPrevInstance */ , LPSTR /* lpCmdLine */ , int /* nCmdShow */ ) : MessageBox( NULL, / 没有父窗口 “Hello, Windows 2000” , / 消息框中的文本 “Greetings”, / 消息框标题 MB_OK) ; / 其中只有一个 OK 按钮 / 返回 0 以便通知系统不进入消息循环 return(0) ; 也可以利用任何其他文本编辑器键入程序代码,如果这
4、样,例如使用 WORD 来键入和编辑程序,则应该注意什么问题? _ _ 在程序 1-1 的 GUI 应用程序中,首先需要 Windows.h 头文件,以便获得传送给 WinMain() 和MessageBox() API 函数的数据类型定义。 接着的 pragma 指令指示编译器/ 连接器找到 User32.LIB 库文件并将其与产生的 EXE 文件连接起来。这样就可以运行简单的命令行命令 CL MsgBox.CPP 来创建这一应用程序,如果没有pragma 指令,则 MessageBox() API 函数就成为未定义的了。这一指令是 Visual Studio C+ 编译器特有的。 接下来是
5、 WinMain() 方法。其中有四个由实际的低级入口点传递来的参数。 hInstance 参数用来装入与代码相连的图标或位图一类的资源,无论何时,都可用 GetModuleHandle() API 函数将这些资源提取出来。系统利用实例句柄来指明代码和初始的数据装在内存的何处。句柄的数值实际上是 EXE 文件映像的基地址,通常为 0x00400000。下一个参数 hPrevInstance 是为向后兼容而设的,现在系统将其设为 NULL。应用程序的命令行 ( 不包括程序的名称) 是 lpCmdLine 参数。另外,系统利用 nCmdShow 参数告诉应用程序如何显示它的主窗口 ( 选项包括最小
6、化、最大化和2正常) 。 最后,程序调用 MessageBox() API 函数并退出。如果在进入消息循环之前就结束运行的话,最后必须返回 0。 运行结果: _ _ _ _ 进程对象 操作系统将当前运行的应用程序看作是进程对象。利用系统提供的惟一的称为句柄 (HANDLE) 的号码,就可与进程对象交互。这一号码只对当前进程有效。 本实验表示了一个简单的进程句柄的应用。在系统中运行的任何进程都可调用GetCurrentProcess() API 函数,此函数可返回标识进程本身的句柄。然后就可在 Windows 需要该进程的有关情况时,利用这一句柄来提供。 程序 1-2: 获得和使用进程的句柄 /
7、 prochandle 项目 # include # include / 确定自己的优先权的简单应用程序 void main() / 从当前进程中提取句柄 HANDLE hProcessThis = : GetCurrentProcess() ; / 请求内核提供该进程所属的优先权类 DWORD dwPriority = : GetPriorityClass(hProcessThis) ; / 发出消息,为用户描述该类 std : cout ” ; break; std : cout # include # include / 当在用户模式机内核模式下都提供所耗时间时,在内核模式下进行所耗时间
8、的 64 位计算的帮助方法 DWORD GetKernelModePercentage(const FILETIME : GetModuleFileName(NULL, szFilename, MAX_PATH) ; / 格式化用于子进程的命令行并通知其 EXE 文件名和克隆 ID TCHAR szCmdLineMAX_PATH ; : sprintf(szCmdLine, “”%s” %d”, szFilename, nCloneID) ; / 用于子进程的 STARTUPINFO 结构 STARTUPINFO si; : ZeroMemory(reinterpret_cast ( si.c
9、b = sizeof(si) ; / 必须是本结构的大小 / 返回的用于子进程的进程信息 PROCESS_INFORMATION pi; / 利用同样的可执行文件和命令行创建进程,并赋于其子进程的性质 BOOL bCreateOK = : CreateProcess( szFilename, / 产生这个 EXE 的应用程序的名称 szCmdLine, / 告诉其行为像一个子进程的标志 NULL, / 缺省的进程安全性 NULL, / 缺省的线程安全性 FALSE, / 不继承句柄 CREATE_NEW_CONSOLE, / 使用新的控制台 NULL, / 新的环境 NULL, / 当前目录
10、/ 返回的进程信息 / 对子进程释放引用 if (bCreateOK) : CloseHandle(pi.hProcess) ; : CloseHandle(pi.hThread) ; 5int main(int argc, char* argv ) / 确定进程在列表中的位置 int nClone(0) ; if (argc 1) / 从第二个参数中提取克隆 ID : sscanf(argv1 , “%d” , / 显示进程位置 std : cout # include / 利用进程和操作系统的版本信息的简单示例 void main() / 提取这个进程的 ID 号 DWORD dwIdTh
11、is = : GetCurrentProcessId() ; / 获得这一进程和报告所需的版本,也可以发送 0 以便指明这一进程 DWORD dwVerReq = : GetProcessVersion(dwIdThis) ; WORD wMajorReq = (WORD) dwVerReq 16) ; WORD wMinorReq = (WORD) dwVerReq std : cout ( std : cout = 5) / 改变优先级 : SetPriorityClass( : GetCurrentProcess() , / 利用这一进程 HIGH_PRIORITY_CLASS) ; /
12、 改变为 high / 报告给用户 std : cout # include # include static LPCTSTR g_szMutexName = “w2kdg.ProcTerm.mutex.Suicide” ; / 创建当前进程的克隆进程的简单方法 void StartClone() / 提取当前可执行文件的文件名 TCHAR szFilename MAX_PATH ; : GetModuleFileName(NULL, szFilename, MAX_PATH) ; / 格式化用于子进程的命令行,指明它是一个 EXE 文件和子进程 TCHAR szCmdLineMAX_PATH
13、 ; : sprintf(szCmdLine, “” %s “ child” , szFilename) ; / 子进程的启动信息结构 STARTUPINFO si; : ZeroMemory(reinterpret_cast ( si.cb = sizeof(si) ; / 应当是此结构的大小 7/ 返回的用于子进程的进程信息 PROCESS_INFORMATION pi; / 用同样的可执行文件名和命令行创建进程,并指明它是一个子进程 BOOL bCreateOK = : CreateProcess( szFilename, / 产生的应用程序名称 ( 本 EXE 文件) szCmdLin
14、e, / 告诉我们这是一个子进程的标志 NULL, / 用于进程的缺省的安全性 NULL, / 用于线程的缺省安全性 FALSE, / 不继承句柄 CREATE_NEW_CONSOLE, / 创建新窗口,使输出更直观 NULL, / 新环境 NULL, / 当前目录 / 返回的进程信息 / 释放指向子进程的引用 if (bCreateOK) : CloseHandle(pi.hProcess) ; : CloseHandle(pi.hThread) ; void Parent() / 创建“自杀”互斥程序体 HANDLE hMutexSuicide = : CreateMutex( NULL,
15、 / 缺省的安全性 TRUE, / 最初拥有的 g_szMutexName) ; / 为其命名 if (hMutexSuicide != NULL) / 创建子进程 std : cout l else Parent() ; return 0; 程序说明了一个进程从“生”到“死”的整个一生。第一次执行时,它创建一个子进程,其行为如同“父亲”。在创建子进程之前,先创建一个互斥的内核对象,其行为对于子进程来说,如同一个“自杀弹”。当创建子进程时,就打开了互斥体并在其他线程中进行别的处理工作,同时等待着父进程使用 ReleaseMutex() API 发出“死亡”信号。然后用 Sleep() API
16、调用来模拟父进程处理其他工作,等完成时,指令子进程终止。 当调用 ExitProcess() 时要小心,进程中的所有线程都被立刻通知停止。在设计应用程序时,必须让主线程在正常的 C+ 运行期关闭 ( 这是由编译器提供的缺省行为) 之后来调用这一函数。当它转向受信状态时,通常可创建一个每个活动线程都可等待和停止的终止事件。 在正常的终止操作中,进程的每个工作线程都要终止,由主线程调用 ExitProcess()。接着,管理层对进程增加的所有对象释放引用,并将用 GetExitCodeProcess() 建立的退出代码从STILL_ACTIVE 改变为在 ExitProcess() 调用中返回的值
17、。最后,主线程对象也如同进程对象一样转变为受信状态。 等到所有打开的句柄都关闭之后,管理层的对象管理器才销毁进程对象本身。还没有一种函数可取得终止后的进程对象为其参数,从而使其“复活”。当进程对象引用一个终止了的对象时,有好几个 API 函数仍然是有用的。进程可使用退出代码将终止方式通知给调用GetExitCodeProcess() 的其他进程。同时, GetProcessTimes() API 函数可向主调者显示进程的终止时间。 运行结果: 1) _ 表示:_ 2) _ 表示:_ 在熟悉源代码的基础上,利用本实验介绍的 API 函数来尝试改进本程序 ( 例如使用GetProcessTimes
18、() API 函数 ) 并运行。请描述你所做的工作: _ _ _ _ 四、实验总结 请总结一下本次实验的收获、教训和感受,结合课本内容谈一下你对进程的理解。 9实验二 并发与调度 一、实验目的 在本实验中,通过对事件和互斥体对象的了解,来加深对 Windows 2000 线程同步的理解。通过分析实验程序,了解管理事件对象的 API。了解在进程中如何使用事件对象,在进程中如何使用互斥体对象,线程如何通过文件映射对象发送数据。 二、实验环境 硬件环境:计算机一台,局域网环境; 软件环境:Windows 2000 Professional,Visual C+ 6.0 专业版或企业版。 三、实验内容和
19、步骤 第一部分:互斥体对象 本程序中显示的类 CCountUpDown 使用了一个互斥体来保证对两个线程间单一数值的访问。每个线程都企图获得控制权来改变该数值,然后将该数值写入输出流中。创建者实际上创建的是互斥体对象,计数方法执行等待并释放,为的是共同使用互斥体所需的资源 ( 因而也就是共享资源) 。 1、利用互斥体保护共享资源 / mutex项目 # include # include / 利用互斥体来保护同时访问的共享资源 class CCountUpDown public: / 创建者创建两个线程来访问共享值 CCountUpDown(int nAccesses) : m_hThread
20、lnc(INVALID_HANDLE_VALUE) , m_hThreadDec(INVALID_HANDLE_VALUE) , m_hMutexValue(INVALID_HANDLE_VALUE) , m_nValue(0) , m_nAccess(nAccesses) / 创建互斥体用于访问数值 m_hMutexValue = : CreateMutex( NULL, / 缺省的安全性 TRUE, / 初始时拥有,在所有的初始化结束时将释放 NULL) ; / 匿名的 m_hThreadInc = : CreateThread( NULL, / 缺省的安全性 0, / 缺省堆栈 IncT
21、hreadProc, / 类线程进程 reinterpret_cast (this) , / 线程参数 0, / 无特殊的标志 NULL) ; / 忽略返回的 id m_hThreadDec = : CreateThread( NULL, / 缺省的安全性 0, / 缺省堆栈 DecThreadProc, / 类线程进程 reinterpret_cast (this) , / 线程参数 0, / 无特殊的标志 NULL) ; / 忽略返回的 id / 允许另一线程获得互斥体 : ReleaseMutex(m_hMutexValue) ; / 解除程序释放对对象的引用 10virtual CCo
22、untUpDown() : CloseHandle(m_hThreadInc) ; : CloseHandle(m_hThreadDec) ; : CloseHandle(m_hMutexValue) ; / 简单的等待方法,在两个线程终止之前可暂停主调者 virtual void WaitForCompletion() / 确保所有对象都已准备好 if (m_hThreadInc != INVALID_HANDLE_VALUE : WaitForSingleObject(m_hThreadDec, INFINITE) ; protected: / 改变共享资源的简单的方法 virtual v
23、oid DoCount(int nStep) / 循环,直到所有的访问都结束为止 while (m_nAccess 0) / 等待访问数值 : WaitForSingleObject(m_hMutexValue, INFINITE) ; / 改变并显示该值 m_nValue += nStep; std : cout (lpParam) ; / 调用对象的增加方法并返回一个值 pThis - DoCount(+1) ; return(0) ; static DWORD WINAPI DecThreadProc(LPVOID lpParam) / 将参数解释为 this 指针 CCountUpDo
24、wn* pThis = reinterpret_cast (lpParam) ; / 调用对象的减少方法并返回一个值 pThis - DOCount(-1) ; return(0) ; protected: HANDLE m_hThreadInc; HANDLE m_hThreadDec; HANDLE m_hMutexValue; int m_nValue; int m_nAccess ; ; void main() 11 CCountUpDown ud(50) ; ud.WaitForCompletion() ; 分析程序的运行结果,可以看到线程 ( 加和减线程 ) 的交替执行 ( 因为
25、Sleep() API 允许Windows 切换线程) 。在每次运行之后,数值应该返回初始值 (0) ,因为在每次运行之后写入线程在等待队列中变成最后一个,内核保证它在其他线程工作时不会再运行。 1) 请描述运行结果 ( 如果运行不成功,则可能的原因是什么?) : _ _ 2) 根据运行输出结果,对照分析程序,可以看出程序运行的流程吗?请简单描述: _ _ 第二部分:线程通过文件对象发送数据 Windows 2000 提供的线程间通讯类内核对象允许同一进程或跨进程的线程之间互相发送信息,包括文件、文件映射、邮件位和命名管道等,其中最常用的是文件和文件映射。这类对象允许一个线程很容易地向同一进程
26、或其他进程中的另一线程发送信息。 1、演示线程通过文件对象发送数据 # inc1ude # include / 要使用的文件名 static LPCTSTR g_szFileName = “w2kdg.Fileobj.file.data.txt ” ; / 在数据文件中读取当前数据的简单线程时将传递来的该数据增加,并写回数据文件中 static DWORD WINAPI ThreadProc (LPVOID lpParam) / 将参数翻译为长整数 LONG nAdd = reinterpret_cast (lpParam) ; / 建立完全的指定文件名 ( 包括路径信息) TCHAR szF
27、ullName MAX_PATH ; : GetTempPath(MAX_PATH, szFullName) ; / 取得路径 : strcat(szFullName, g_szFileName) ; / 打开文件对象 HANDLE hFile = : CreateFile( szFullName, / 文件的完全名称 GENERIC-READ | GENERIC_WRITE, / 具有所有的访问权 FILE_SHARE_READ, / 允许其他线程读取 NULL, / 缺省的安全性 OPEN_ALWAYS, / 创建或打开文件 FILE_ATTRIBUTE_NORMAL, / 普通文件 NU
28、LL) ; / 无模板文件 if (hFile != INVALID_HANDLE_VALUE) / 读取当前数据 LONG nValue(0) ; DWORD dwXfer(0) ; : ReadFile( hFile, / 要读取的文件 reinterpret_cast ( / 无重叠 I/O if (dwXfer = sizeof(nValue) ) std : cout ( / 无重叠 I/O if (dwXfer = sizeof(nValue) ) std : cout 0; -nTotal) / 启动线程 HANDLE hThread = : CreateThread( NULL
29、, / 缺省的安全性 0, / 缺省的堆栈 ThreadProc, / 线程函数 reinterpret_cast (1) , / 增量 0, / 无特殊的创建标志 NUL) ; / 忽略线程 id / 等待线程完成 : WaitForSingleObject(hThread, INFINITE) ; : Sleep(500) ; / 放慢显示速度,方便观察 / 释放指向线程的句柄 : CloseHandle(hThread) ; hThread = INVALID_ HANDLE_VALUE; 运行结果 (如果运行不成功,则可能的原因是什么?) : _ _ 阅读和分析程序,请回答问题: 1)
30、 程序中启动了多少个单独的读写线程? _ 2) 使用了哪个系统 API 函数来创建线程例程? _ 3) 文件的读和写操作分别使用了哪个 API 函数? _ _ 每次运行进程时,都可看到程序中的每个线程从前面的线程中读取数据并将数据增加,文件中的数值连续增加。这个示例是很简单的通讯机制。可将这一示例用作编写自己的文件读/ 写代码的模板。 请注意程序中写入之前文件指针的重置。重置文件指针是必要的,因为该指针在读取结束时将处于前四个字节之后,同一指针还要用于向文件写入数据。如果函数向该处写入新数值,则下次进程运行时,只能读到原来的数值。那么: 4) 在程序中,重置文件指针使用了哪一个函数? _ 5)
31、 从输出结果,对照分析程序,可以看出程序运行的流程吗?请简单描述: _ _ 132、演示使用映射文件的内存交换数据的线程 # include # include / 仲裁访问的互斥体 static HANDLE g_hMutexMapping = INVALID_HANDLE_VALUE; / 增加共享内存中的数值的简单线程 static DWORD WINAPI ThreadProc(LPVOID lpParam) / 将参数看作句柄 HANDLE hMapping = reinterpret_cast (IpParam) ; / 等待对文件的访问 : WaitForSingleObject
32、(g_hMutexMapping, INFINITE) ; / 映射视图 LPVOID pFile = : MapViewOfFile( hMapping, / 保存文件的对象 FILE_MAP_ALL_ACCESS, / 获得读写权限 0, / 在文件的开头处 ( 高 32 位) 开始 0, / . (低 32 位) 0) ; / 映射整个文件 if (pFile != NULL) / 将数据看作长整数 LONG * pnData = reinterpret_cast (pFile) ; / 改动数据 +(* pnData) ; / 显示新数值 std : cout 0 ; - - nTot
33、al) / 启动线程 HANDLE hThread = : CreateThread( NULL, / 缺省的安全性 0, / 缺省堆栈 ThreadProc, / 线程函数 reinterpret_cast (hMapping) , / 增量 0, / 无特殊的创建标志 NULL) ; / 忽略线程 id / 等待最后的线程释放 if (nTotal = l) std : cout #include #include #include #include /定义一些常量; /本程序允许的最大临界区数; #define MAX_BUFFER_NUM 10 /秒到毫秒的乘法因子; #define
34、INTE_PER_SEC 1000 /本程序允许的生产和消费线程的总数; #define MAX_THREAD_NUM 64 /定义一个结构,记录在测试文件中指定的每一个线程的参数 struct ThreadInfo 17int serial; /线程序列号 char entiy; /是 P 还是 C double delay; /线程延迟 int thread_requestMAX_THREAD_NUM; /线程请求队列 int n_request; /请求个数 ; /全局变量的定义 /临界区对象的声明, 用于管理缓冲区的互斥访问; CRITICAL_SECTION PC_CriticalM
35、AX_BUFFER_NUM; int Buffer_CriticalMAX_BUFFER_NUM; /缓冲区声明,用于存放产品; HANDLE h_ThreadMAX_THREAD_NUM; /用于存储每个线程句柄的数组; ThreadInfo Thread_InfoMAX_THREAD_NUM; /线程信息数组; HANDLE empty_semaphore; /一个信号量; HANDLE h_mutex; /一个互斥量; DWORD n_Thread = 0; /实际的线程的数目; DWORD n_Buffer_or_Critical; /实际的缓冲区或者临界区的数目; HANDLE h_
36、SemaphoreMAX_THREAD_NUM; /生产者允许消费者开始消费的信号量; /生产消费及辅助函数的声明 void Produce(void *p); void Consume(void *p); bool IfInOtherRequest(int); int FindProducePositon(); int FindBufferPosition(int); int main(void) /声明所需变量; DWORD wait_for_all; ifstream inFile; /初始化缓冲区; for(int i=0;i n_Buffer_or_Critical; inFile.
37、get(); printf(“输入文件是:n“); /回显获得的缓冲区的数目信息; printf(“%d n“,(int) n_Buffer_or_Critical); 18/提取每个线程的信息到相应数据结构中; while(inFile) inFile Thread_Infon_Thread.serial; inFile Thread_Infon_Thread.entity; inFile Thread_Infon_Thread.delay; char c; inFile.get(c); while(c!=n inFile.get(c); n_Thread+; /回显获得的线程信息,便于确认
38、正确性; for(j=0;jserial; m_delay = (DWORD)(ThreadInfo*)(p)-delay *INTE_PER_SEC); Sleep(m_delay); 20/开始请求生产 printf(“生产者 %2d 发送生产请求信号.n“,m_serial); /确认有空缓冲区可供生产,同时将空位置数 empty 减 1;用于生产者和消费者的同步; wait_for_semaphore = WaitForSingleObject(empty_semaphore,-1); /互斥访问下一个可用于生产的空临界区,实现写写互斥; wait_for_mutex = WaitFo
39、rSingleObject(h_mutex,-1); int ProducePos = FindProducePosition(); 6请填空 (h_mutex); /生产者在获得自己的空位置并做上标记后,以下的写操作在生产者之间可以并发执行; /核心生产步骤中,程序将生产者的 ID 作为产品编号放入,方便消费者识别; printf(“生产者 %2d 开始在缓冲区 %2d 生产产品.n“,m_serial,ProducePos); Buffer_CriticalProducePos = m_serial; printf(“生产者 %2d 完成生产过程 :n “,m_serial); print
40、f(“ 缓冲区 %2d :%3d n“ ,ProducePos,Buffer_CriticalProducePos); /使生产者写的缓冲区可以被多个消费者使用,实现读写同步; 7 请填空 (h_Semaphorem_serial,n_Thread,NULL); /消费者进程 void Consume(void * p) /局部变量声明; DWORD wait_for_semaphore,m_delay; int m_serial,m_requestNum; /消费者线程的序列号和请求的数目; int m_thread_requestMAX_THREAD_NUM;/本消费者线程的请求队列; /
41、提取本线程的信息到本地; m_serial = (ThreadInfo*)(p)-serial; m_delay = (DWORD)(ThreadInfo*)(p)-delay *INTE_PER_SEC); m_requestNum = (ThreadInfo *)(p)-n_request; for (int i = 0;ithread_requesti; Sleep(m_delay); /循环进行所需产品的消费 for(i =0;ithread_requesti =-1; if(!IfInOtherRequest(m_thread_requesti) Buffer_CriticalBufferPos = -1;/标记缓冲区为空; printf(“消费者 %2d 成功消费 %2d:n “,m_serial,m_thread_requesti); printf(“ 缓冲区 %2d :%3d n“ ,BufferPos,B uffer_CriticalBufferPos); 9 请填空 (empty_semaphore,1,NULL); else printf(“消费者 %2d 成功消费产品 %2dn “,m_serial,m_thread_requesti); /离开临界区