是
“操作系统原理”课内上机实验指导书
适用专业:计算机科学与技术
计算机软件技术
电子与信息工程学院计算机系
2012年6月
前 言
操作系统原理是计算机专业本科学生的必修课程,它具有较强的理论性和实践性。为了强化实践教学环节,培养学生的实践能力,促进理论与实践的结合,在30学时独立开设的操作系统原理上机实验的基础上,我们又增加了8学时的课内实验。本实验指导通过Windows API提供一些编程实例,使学生熟悉对Windows操作系统编程接口的使用,并了解如何模拟操作系统原理的实现,加深对操作系统设计原理和实现方法的理解,使学生在程序设计和团队合作方面得到基本的训练。
本上机实验指导从操作系统基本原理出发,提供了不同类型的上机实验题,每个实验题都有参考源程序代码,同时对实验题的设计进行了必要的讲解和指导。在这些上机实验中,重点放在对Windows的应用程序接口API的使用上。利用这些与操作系统直接相关的API,编写一些实践操作系统基本概念和基本原理的实例,便于学生对抽象概念的理解和感性化;通过阅读本实验指导书提供的实例程序代码,能使学生得到编程方面的体验和训练。
为了能比较全面覆盖操作系统课程的知识点,本实验指导书提供了九个上机实验,具体包括如下五个方面的内容:
1. 进程管理:包括“上机实验一 进程的创建”、“上机实验二 线程的创建和并发执行”、“上机实验三 进程同步”和“上机实验四 进程通信”。受课内实验时间的限制,对上述四个题目,每个学生一般可选一个实验题目进行上机实验。
2. 进程调度和死锁:包括“上机实验五 进程调度”和“上机实验六 演示银行家算法”。每个学生可任选一题,建议选做进程调度的题目。
3. 存储器管理:包括“上机实验七 模拟页面置换算法”。参考程序提供的是FIFO页面置换算法,有兴趣的学生可以编程实现LRU或Clock算法。
4. 设备管理:包括“上机实验八 磁盘I/O”。
5. 用户操作接口:包括“上机实验九 命令解释程序”。
以上5种类型的九个实验题目共22学时(包括每个实验项目的基本要求各2学时,共18学时;“进程调度”和“页面置换算法”的进阶要求各2学时),每个学生可任选4题(每种类型限选1题),共完成8学时的课内实验计划。由于操作系统原理课程独立开设的30学时的实验内容是文件系统的设计,故文件系统的实验没有包含在本上机实验指导书中。
本上机实验指导的“进程调度”、“磁盘I/O”和“命令解释程序”等实验,主要参考了任爱华等编著的《操作系统实用教程(第三版)实验指导》,但都作了重大修改或重新设计。“进程的创建”、“线程的创建和并发执行”利用MSDN(Microsoft Developet Network)提供的函数CreateProcess和CreateThread实现的;“进程同步”、“进程通信”、“银行家算法”、“页面置换算法”等实验是编者独立设计的,其中“进程同步”所用的信号量对象和“进程通信”所用的Pipe(管道)技术参阅了MSDN的相关资料。
本上机实验指导涉及的Win32 API函数、数据结构和数据类型,可参阅配套的Word文档“OS课内上机实验使用的相关API函数介绍.doc”、“OS实验涉及的API函数中使用的几个数据结构介绍.doc”和“Win32 Simple Data Types.doc”。
限于编者的水平和编写时间仓促,本实验指导一定有许多错误和不妥之处,恳请读者批评指正。
编者
2012年6月
目 录
上机实验一 进程的创建 (2学时)
1.1 上机实验要求
本实验要求设计一个实现进程创建的程序,使所创建的进程在处理机上并发执行,要求程序能对出现的异常进行报告。通过本上机实验,学会在Win32程序中使用操作系统提供的API创建进程,并通过所创建的进程的运行情况获得对于进程并发的感性认识。
【注】在Windows系统中创建一个进程实质上就是启动执行一个可执行程序(文件)。
1.2 设计方案介绍
1.2.1 程序的总体框架
程序将“test.txt”文件作为输入,该文本文件的每一行都是有一个应用程序名以及该应用程序的参数构成(应用程序名和参数由空格隔开)。程序从“test.txt”文件中依次读出每一行存入cmdline字符串中,再以cmdline为函数实参数,调用NewProc( )函数,通过CreateProcess( )这个Win32 API函数来建立一个新进程,在该进程中运行对应的应用程序。“cmdline.txt”文件是在记事本中预先编制好的一个文本文本,其文件内容如下:
Process 1
Process 2
Process 3
Process1 4
命令行“Process 1”中的“Process”是指当前目录下的可执行文件“Process.exe”,命令行中的“1”是该可执行文件的输入参数。该命令行也可写成“Process.exe 1”。Process.exe是为了测试进程的并发执行而预先编制的VC++源程序编译而成的,其源代码如下面介绍。上述4个命令行中,第4个命令是为了测试出错信息的,其错误原因是应用程序Process1.exe不存在。用来演示的可执行文件Process.exe的VC++源代码如下:
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <windows.h> //Sleep()
void main(int argc,char **argv) //执行时可带参数的程序
{
unsigned int wt,x=1;
if (argc>=2)
x=1+atoi(argv[1]);
x=x*(unsigned int)time(NULL);
srand(x); // 使用当前时间为随机序列的"种子"
char Title[30]="Process ";
if (argc>=2)
{
strcat(Title,argv[1]);
SetConsoleTitle(Title); //设置标题栏
}
for (int i=1;i<=30;i++)
{
//显示进程正在运行的信息,其中命令参数argv[1]作为进程的编号
printf(" %2d : Process ",i);
if (argc>=2)
printf("%s ",argv[1]);
printf("正在运行!\n");
wt = 200 + rand() % 500 + rand() % 1000;
wt= rand() % wt + 300;
Sleep(wt); //睡眠等待200~2000ms
}
}
需要说明的是,本实验的用来创建进程的命令行的可执行程序名,也可以是GUI的应用程序,例如,命令行“c:\\windows\\notepad.exe 1.txt”将打开“记事本”窗口,同时在记事本窗口中打开文本文件“1.txt”(如果1.txt文件存在的话)。
1.2.2 程序所用Win32 API函数介绍
操作系统所能完成的每一个特殊功能都有一个函数与其对应,即操作系统把它所能完成的功能以函数的形式提供给应用程序使用。应用程序对这些函数的调用叫系统调用。这些函数的集合就是Windows操作系统提供给应用程序的编程接口(Application Programming Interface),简称Windows API或Win32 API。所有在Win32平台上运行的应用程序都可以调用这些函数。
使用Windows API,应用程序可以充分挖掘Windows的32位操作系统潜力。Microsoft的所有32位平台都支持统一的API。
Windows的相关API的说明都可以在MSDN(Microsoft Developet Network)中查到,包括定义、使用方法等。下面简单介绍本实验中涉及的Windows API。
1.GetLastError( )函数
功能:返回最近一次发生的错误代码。
格式:DWORD GetLastError(VOID)
【注】此处的DWORD就是unsugned long,VOID就是void。要更多地了解Win32 API中的数据类型,可参阅配套文档“Win32 Simple Data Types.doc”。
2.FormatMessage( )函数
功能:生成具体的出错信息。在本实验程序中,用它将由GetLastError( )得到的最近一次发生的错误代码号转换成具体的出错信息。调用此功能需要一个消息定义作为输入,该消息定义可以在本函数的一个缓冲中设定,也可以是系统定义的消息表,或者是另一个已装载模块中定义的消息表。此函数若调用成功,则返回得到的消息的字节数,否则返回0.
格式:
DWORD FormatMessage (
DWORD dwFlags,
LPVOID lpSource,
WORD dwMessageId,
WORD dwLanguageId,
PTSTR lpBuffer,
WORD nSize,
Va_list *Arguments
)
参数说明:
dwFlags:一个关于后面的lpSource参数的使用及有关输出格式等方面的参数。参数值如下:
n FORMAT_MESSAGE_ALLOCATE_BUFFER:表示系统将自动分配足够大的内存缓冲区存储得到的消息,此时lpBuffer指向该缓冲区的首部(lpvoid型),nSize指明分配给该缓冲区的最少字节数;这时,进程在后面需要通过调用LocalFree( )函数去释放系统分配的缓冲区。
n FORMAT_MESSAGE_FROM_SYSTEM、FORMAT_MESSAGE_FROM_HMODVLE与FORMAT _ MESSAGE_FROM_STRING:前者表明利用错误代码dwMessageId到系统消息定义表去找对应的出错信息,它可以和FORMAT_MESSAGE_FROM_HMODVLE合用,表示先到该模块中定义的消息表查找对应消息,找不到再转向系统消息定义表,但二者不能和FORMAT_MESSAGE_FROM_STRING合用,后者表示表示消息定义在由lpSource指针指向的串中。
n FORMAT_MESSAGE_IGNORE_INSERT:指出在消息定义中的插入值在输出时不被处理,此时后面的ARGUMENTS参数被忽略;除了上面提到的功能,这个参数还可以规定输出消息的行宽格式。
lpSource:只有上一参数含FORMAT_MESSAGE_FROM_HMODVLE或FORMAT_MESSAGE _FROM_STRING时才有意义,用于指出所指模块的句柄或者具体消息定义表的首址;否则本参数被忽略;特别是当上一参数含FORMAT_MESSAGE_FROM_HMODVLE而本参数为NULL,默认为当前进程句柄。
dwMessageId:一个32位系统错误代号,和以一个参数一起来定位目标消息,当dwFlags包含FORMAT_MESSAGE_FROM_STRING时,此参数不起作用。当dwFlags包含FORMAT_MESSAGE _FROM_SYSTEM时,本id可以通过GetLastError( )指定。
dwLanguageId:选择所用语言代号,但若dwFlags包含FORMAT_MESSAGE_FROM_STRING,则此参数不起作用;也可以指定为0,表示用默认的几种语言以一定的顺序去寻找满足要求的消息,直到找到一个为止。
lpBuffer:输出消息的存放处,当dwFlags包含FORMAT_MESSAGE_ALLOCATE_BUFFER时,系统动态分配内存,此时就不必预先分配内存,而只传一个指针进来。
nSize:当dwFlags包含FORMAT_MESSAGE_ALLOCATE_BUFFER时,表示需要为缓冲区分配的最小字节数(ANSI版),否则表示可输出消息的最大字节数。
Va_list *Arguments:一组消息插入值(具体用法参见MSDN),本例中取值为NULL。
3.CreateProcess( )函数
功能:创建一个新进程和它的主线程,这个新进程运行指定的可执行文件。
格式:
BOOL CreateProcess (
LPCTSTR lpApplicationName, // 指定可执行模块的字符串
LPCTSTR lpCommandLine, // 指定要运行的命令行
LPSECURITY_ATTRIBUTES lpProcessAttributes, //决定返回句柄能否被继承,
//它定义了进程的安全性
LPSECURITY_ATTRIBUTES lpThreadAttributes, //决定返回句柄能否被继承,
//它定义了进程的主线程的安全性
BOOL bInheritHandles, //表示新进程是否从调用进程处继承了句柄
DWORD dwCreationFalgs, //控制优先类和进程的创建标志
LPVOID lpEnvironment, //指向一个新进程的环境块
LPCTSTR lpCurrentDirectory, //指定子进程的工作路径
LPSTARTUPINFO lpStartupInfo, //决定新进程的主窗体显示方式的结构
LPPROCESS_INFORMATION lpProcessInformation //接收新进程的标识信息
)
下面在对此函数的参数做进一步的说明:
lpApplicationName:用于指定可执行模块的字符串,这个字符串可以是可执行模块的绝对路径,也可以是相对路径。在后一种情况下,函数使用当前驱动器和目录建立可指向模块的路径。这个参数可以设置为NULL(本程序就是如此),在这种情况下,可执行模块的名称必须处于lpCommandLine参数的最前面并由空格与后面的字符分开。可执行模块可以是基于Win32的,也可以是MS-DOS或OS/2下的模块,建议带上扩展名和使用全路径名。
lpCommandLine:指向一个指定要执行的命令行字符串。这个参数可以为NULL,但它不能和第一个参数同时为空。如果lpApplicationName和lpCommandLine都不为空,那么lpApplicationName指定将要运行的模块,lpCommandLine参数指定将要被运行的模块的命令行。
lpProcessAttributes:SECURITY_ATTRIBUTES型指针,指出返回的句柄是否可以被子进程继承,取值为NULL时,表示不可继承。本程序用NULL。
lpThreadAttributes:同上,指出返回的主线程句柄是否可以被子进程继承,取值为NULL时,表示不可继承。本程序用NULL。
bInheritHandles:决定子进程是否可以继承当前打开的句柄,FALSE表示不可继承。本程序中用的是FALSE。
dwCreationFalgs:关于新进程的其他一些环境参数的设定(例如是否另外开辟控制台)和新进程的优先级等问题。程序中创建前台进程时此值为0(可理解为不考虑环境参数的设定和新进程的权限问题),而创建后台进程时此值为CREATE_NEW_CONSOLE(即整数16),表示另外开辟控制台(程序中还另加语句将它开辟的控制台隐藏)。此参数的各种取值和相应含义的详细介绍可参考MSDN。
lpEnvironment:新进程环境变量指针,为NULL则继承父进程的环境变量。本程序用NULL。
lpCurrentDirectory:指定子进程的工作目录(只支持全路径名),本程序用NULL表示是当前目录。
lpStartupInfo:指向STARTUPINFO结构的指针,决定了子进程主窗体的外观,包括窗口的位置、大小,是否有输入和输出机错误输出(详见MSDN的参数说明)。其中输出句柄可以用于进程的管道通信。值得注意的是,该结构第一个成员表示此结构的大小,使用这个结构体时要注意先要初始化它的大小。当进程创建时可以用GetStartupInfo来获得STARTUPINFO结构体(请看本实验的程序)。
lpProcessInformation:指向一个用来接收新进程识别信息的PROCESS_INFORMATION的结构体。其中包含了新进程的多个信息。例如进程句柄、进程主线程的句柄、进程ID、主线程ID。通过获得的进程信息即可对该进程进行进一步操作。本程序未使用这个结果。
4.GetForegroundWindow( )函数
功能:获取当前活动窗口的句柄。
格式:
HWND GetForegroundWindow(void)
5.MoveWindow( )函数
功能:改变指定窗口的位置和大小。
格式:
BOOL MoveWindow(
HWND hWnd, // handle to window
int X, // horizontal position
int Y, // vertical position
int nWidth, // width
int nHeight, // height
BOOL bRepaint // repaint option
);
参数说明:
bRepaint:指出是否重画窗口。TRUE,重画;FALSE,不重画。
1.3 源程序代码与运行结果分析
1.3.1 程序参考源代码
CreateProcess.cpp源程序:
#include <fstream.h> //它已经包含了iostream.h
#include <windows.h> //Win32 API
#define MAX_LINE_LEN 128
void NewProc(char*); // 声明函数
char cmdline[MAX_LINE_LEN];
void main()
{
char ch;
ifstream fid;
HWND hWindow;
hWindow=GetForegroundWindow();//获取本程序运行窗口的句柄
//改变本程序运行窗口的位置和大小使之与子进程窗口整齐排列
MoveWindow(hWindow,10,10,425,320,TRUE);
fid.open("cmdline.txt",ios::nocreate);//打开存储命令行的文件
if (!fid) //若文件不存在,报错
{
cout<<"Can't open cmdline.txt !"<<endl;
ch=cin.get();
exit(0);
}
//从文件逐条读出命令行,用于创建进程
while (fid.getline(cmdline,MAX_LINE_LEN)) {
cout<<"cmdline="<<cmdline<<endl;//输出所读命令行
NewProc(cmdline);//创建新进程
}
cout<<"\nEnd"<<endl;
}
void NewProc(char* cmdline)
{
static int counter=1;
STARTUPINFO si={sizeof(STARTUPINFO)};
//使成员dwX,dwY和dwXSize,dwYSize有效
//目的是为了能调整各进程窗口位置和大小
si.dwFlags=STARTF_USEPOSITION|STARTF_USESIZE;
si.dwXSize=400; //窗口宽
si.dwYSize=260; //窗口高
PROCESS_INFORMATION pi;
DWORD dwx=10,dwy=10; //窗口左上角位置
switch (counter%4)
{
case 0 : si.dwX=dwx; //指定第四个子进程的窗口的左上角位置
si.dwY=dwy;
break;
case 1 : si.dwX=dwx+425;//指定第一个子进程的窗口的左上角位置
si.dwY=dwy;
break;
case 2 : si.dwX=dwx; //指定第二个子进程的窗口的左上角位置
si.dwY=dwy+310;
break;
case 3 : si.dwX=dwx+425;//指定第三个子进程的窗口的左上角位置
si.dwY=dwy+310;
break;
}
counter++;
BOOL result=CreateProcess(
NULL, //没指定可执行模块名
cmdline, //路径和可执行模块名
NULL, //子进程不能继承此函数返回的句柄
NULL, //子进程的线程不能继承此函数返回的句柄
FALSE, //新进程不能从调用此函数进程继承此句柄
NORMAL_PRIORITY_CLASS|CREATE_NEW_CONSOLE,
//创建一个没有特殊调度需求的普通进程、开辟新窗口
NULL, //新进程使用调用此函数进程(父进程)的环境块
NULL, //新进程使用父进程的当前目录作为自己的当前目录
&si, //STARTUPINFO结构决定新进程主窗口如何出现(显示)
&pi //PROCESS_INFORMATION结构用于接收新进程的标识信息
);
//显示详细的创建进程出错信息
if (GetLastError()) {
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
GetLastError(),
MAKELANGID(LANG_NEUTRAL,SUBLANG_DEFAULT),
(LPSTR) &lpMsgBuf,
0,
NULL
);
//弹出出错窗口指示错误(该信息由FormatMessage存入了lpMsgBuf中)
MessageBox(NULL,(LPSTR)lpMsgBuf,"Error",MB_OK | MB_ICONINFORMATION);
LocalFree(lpMsgBuf);
//释放在FormatMessage中动态分配的空间
}
}
1.3.2 程序运行结果与分析
(1) 程序运行结果
程序运行时,按照依次从Test.txt文件中读入的一行,逐个创建进程。程序运行输出如图1-1所示。
程序运行后会创建3个进程(连同主进程共4个进程)。图1-1中,左上角的黑底窗口是主进程窗口,右上角的黑底窗口是命令行“Process 1”创建的进程窗口,下面两个黑底窗口分别是第2、3命令行创建的进程窗口。出错信息框(“Error”框)是用第4个命令行“Process1 4”创建进程时产生的出错信息,原因是可执行文件Process1.exe找不到,即该可执行文件不存在。
(2) 程序运行结果分析
程序运行后,从3个新窗口的显示过程看,新创建的3个进程(它们的主线程)是交替运行的。因为它们被父进程(本程序进程)先后创建后,便独立地去竞争处理机,它们在不同情况下并发运行的结果是不同的。
重复执行本上机实验程序,可以发现,每次运行时,3个新进程的推进速度和完成次序可能是不一样的。
打开Windows的任务管理器,可以看到除了3个新进程以及它们的主进程(本实验程序对应的进程)外,还有一个Error进程(出错信息进程)。
程序运行过程中,若关闭图2-1中左上角的窗口(需要先关闭“Error”框后才能关闭主进程窗口),即终止主进程,可以看到其余三个窗口并不跟着关闭,即子进程并不终止。由此可知,在Windows中,终止一个进程不会引起它的子进程终止。
注意:主进程的线程与3个新进程(子进程)的线程实际上也是并发执行的,但由于主进程在创建第4个进程时因命令行中的可执行文件不存在而出错,需要单击出错信息框中的“确定”按钮后,主进程才能继续往下运行。为了观察主进程与子进程的并发执行情况,同学们可在main函数的while循环后面增加一段循环显示某种信息的程序,在程序运行出现“Error”消息框时立即单击该框中的“确定”按钮,就可以看到主进程和3个子进程并发执行的情况。
例如可以在while循环后增加如下一段程序:
for (int i=1;i<=30;i++)
{
cout.width(2);
cout<<i<<" : Parent process is running !"<<endl;
Sleep(600);
}
上机实验二 线程的创建和并发执行 (2学时)
2.1 上机实验要求
本实验要求编写一个程序,完成多个线程的创建,使得所创建的多线程在处理机上并发执行。通过本实验,学习在Win32程序中利用操作系统提供的API创建线程,并通过所创建线程的运行情况来获得关于多线程并发的感性认识,以及加深对临界资源(本实验中临界资源是屏幕)互斥访问的理解。
2.2 设计方案介绍
2.2.1 程序总体框架
程序启动时由系统系统创建一个进程来执行程序,此时进程只包含一个主线程,该主线程执行程序的main代码。
主线程运行时首先等待用户键入要创建的线程数(赋予变量iThread),以及每个线程运行的时间(秒数,赋予变量wRunTime),接着调用API函数GetSystemTime( )获得当前的系统时间(格林威治时间),计算出新线程的生命结束点时间(均转换成北京时间);然后循环调用函数CreateThread( )来创建iThread个子线程,这些子线程执行同一段代码(threadwork( ) 函数);最后,主线程根据系统时间判断这些子线程的生命周期是否结束,当到达子线程结束时刻,则关runFlag(全局变量)。
各子线程:在每个子线程执行的这段代码(函数threadwork( )的代码)中,首先给出本线程开始运行的提示信息(不同线程用线程号区别,线程号是主线程创建子线程时传递给子线程的,程序中传送的是循环控制变量i的值作为子线程号的),然后循环判断runFlag是否被主线程关闭,未关则显示“Thread x 第 n 次被调度运行”的提示(此处x代表子线程编号,n是某个自然数),睡眠一段随机时间。如此不断循环;当runFlag关时,子线程结束循环,显示“几号线程已结束”字样,然后调用return语句结束本线程。
2.2.2 程序所用API函数介绍
1.CreateThread( )函数
功能:创建一个新线程,返回与线程相关的句柄。若调用失败,则返回NULL。(注:线程句柄与线程标识不同)
格式:
HANDLE CreateThread (
PSECURITY_ATTRIBUTES psa,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE pfnStartAddr,
PVOID pvparam, //是指向“传递给新线程单一参数”的指针
DWORD dwcreationFlags,
PDWORD pdwthreadId //是指向“返回给创建者的线程id”的指针
)
参数说明
psa:指向SECURITY_ATTRIBUTES结构的指针,指出返回的句柄是否可被子线程继承。本程序中调用此函数时设置为NULL,表示不可继承。
dwStackSize:指定新线程所用的堆栈大小(字节数),值为0时表示使用默认值,即其大小与当前线程一样。
pfnStartAddr:指明想要新线程代码地址(即指向的线程函数地址),调用此函数时,此参数使用的实参是线程函数名,线程函数必须声明为_stdcall标准(例如函数类型为DWORD WINAPI),具体做法见程序实现部分的说明。若线程函数未声明为_stdcall标准,例如其返回值类型是普通的void、int等类型,则必须在线程函数名前加强制类型转换运算符(LPTHREAD_START_ROUTINE)。
pvparam:指定一个32-bit的参数传递给新创建的线程,它对应于线程要执行的函数的参数。该参数必须是LPVOID(即void *)类型的。【注】为使主线程能传递多个值给子线程,可以用结构型变量(或对象)。
dwcreationFlags:指定线程的初始状态,0表示运行态,CREATE_SUSPEND表示挂起(暂停)状态。
pdwthreadId:指向32-bit变量的指针,该变量接收新线程的标识号。在Windows 2000/XP中可以设置为NULL,但在Windows 95/98中必须设置为DWORD的有效地址。
2.GetSystemTime( )函数
功能:获得系统时间(时、分、秒等),它是格林威治时间,它通过参数返回的是SYSTEMTIME结构型数据。
格式:
VOID GetSystemTime(
LPSYSTEMTIME lpst // LPSYSTEMTIME是指向SYSTEMTIME结构的指针类型
) ;
参数说明:lpst是指针型参数,用于接收函数返回的系统时间。
LPSYSTEMTIME结构的成员请参看Word文档“OS试验涉及的API函数中使用的几个数据结构介绍.doc”的介绍。
3.InitializeCriticalSection( )函数
功能:初始化临界区对象(相当于信号量初始化)。临界区对象定义后需调用此函数进行初始化。
格式:
InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
参数说明:
lpCriticalSection:指向临界区对象(CRITICAL_SECTION类型对象)的指针。
4.EnterCriticalSection函数
功能:等待指定临界区对象的所有权。当对象被赋予所有权时,该函数返回。此函数用作访问临界区的“进入代码”,相当于信号量的wait操作。
格式:
void EnterCriticalSection (LPCRITICAL_SECTION lpCriticalSection );
参数说明:
lpCriticalSection:指向临界区对象(CRITICAL_SECTION类型对象)的指针。
5.LeaveCriticalSection函数
功能:释放指定临界区对象的所有权。此函数用作离开临界区的“退出代码”,相当于信号量的signal操作。
格式:
void LeaveCriticalSection (LPCRITICAL_SECTION lpCriticalSection );
参数说明:lpCriticalSection:指向临界区对象的指针。
6.Sleep( )函数
功能:当前线程休眠(挂起suspend)一段时间,时间长短由函数参数指定。
格式:
void Sleep ( DWORD dwMilliseconds );
参数说明
dwMilliseconds:休眠的时间,以ms为单位。
7.ExitThread ( )函数 ——本程序没有使用此函数
功能:强行退出(主动结束)线程。【注】线程函数运行结束执行返回语句时也会退出线程。
格式:
VOID ExitThread( DWORD dwExitCode ); // exit code for this thread
参数说明
dwExitCode:此线程的退出码。可以调用GetExitCodeThread( )获得该线程的退出码。
8.GetExitCodeThread ( )函数
BOOL GetExitCodeThread(
HANDLE hThread, // handle to the thread
LPDWORD lpExitCode // address to receive termination status
);
若线程是调用ExitThread( )函数退出的,则调用GetExitCodeThread ( )可从参数lpExitCode获得当初调用ExitThread( )时设置的退出码;若线程是因运行结束执行return语句退出的,则此函数获得的是return语句的返回值。
2.3 程序源代码和运行结果
2.3.1 程序源代码
// ************* Mult-Thread *************
#include <windows.h> //API
#include <time.h> //rand()用
#include <iostream.h>
#include <conio.h> //_getch()
//设立一个全局开关,在规定时刻(新创建线程生命期结束时刻)通知它们
int runFlag=true;
//各线程所要执行的模块
DWORD WINAPI threadwork(LPVOID ThreadNo);
CRITICAL_SECTION cs_Screen; //因多线程竞争屏幕,故设此临界区控制变量
//主函数main,也是本进程的主线程执行的代码
void main(void)
{
WORD wRunTime; // 线程的生命周期(秒),WORD是32位有符号整型,即int
SYSTEMTIME now; // 时间结构(时、分、秒)
DWORD ThreadID[100]; // 线程id,假设最多创建100个线程
HANDLE hThread[100]; // 线程句柄
int iThread; // 创建的线程数
int ctime; // 当前时间(时、分、秒)折合成的秒数
int etime; // 新线程结束时间(时、分、秒)折合成的秒数
int bhour; // 新线程开始执行时间的小时数
int chour; // 当前时间的小时数
int temp; // 暂存变量
char ch;
InitializeCriticalSection(&cs_Screen); //初始化临界区控制变量
srand((unsigned int)time(NULL)); // 使用当前时间为随机序列的“种子”
//输入新线程数iThread和新线程运行的时间wRunTime,为了便于观
//察程序的输出结果,建议线程数和线程运行时间一般都不要超过5
cout<<"请输入要创建的新线程数:";
cin>>iThread;
cout<<"新线程运行的时间(秒数):";
cin>>wRunTime;
//计算线程的生命期(结束时间),所有新线程共用
GetSystemTime(&now); //调用返回格林威治时间,存于now
if (now.wHour>16)
now.wHour -= 16; //转化为北京时间
else
now.wHour += 8;
bhour=now.wHour; //记下开始的小时值
temp=now.wHour*3600+now.wMinute*60+now.wSecond; //把当前时间化成秒数
etime=temp+wRunTime; //线程结束的秒数
//循环创建新线程,数量有参数指定
cout.fill('0');
cout.width(2); cout<<"新线程开始时间:"<<now.wHour<<':';
cout.width(2); cout<<now.wMinute<<':';
cout.width(2); cout<<now.wSecond<<"(主线程输出信息)"<<endl;
for (int i=0; i<iThread; i++) {
/* 线程函数的参数类型必须是LPVOID(即void *),故用
强制类型转换运算符(LPVOID)将整型i转换成LPVOID */
hThread[i]=CreateThread(NULL,0,threadwork, (LPVOID)i,0,&ThreadID[i]);
Sleep(100); // 主线程让出时间,使新建的线程运行
}
// 在子线程工作时不断循环,判断这些线程生命是否到期
while (runFlag)
{
EnterCriticalSection(&cs_Screen); //准备进入临界区
cout<<"************ 主线程正在运行 ………… "<<endl;
LeaveCriticalSection(&cs_Screen); // 退出临界区
GetSystemTime(&now);
if (now.wHour>16)
now.wHour -= 16; //转化为北京时间
else
now.wHour += 8;
if (now.wHour<bhour) //处理时间过“00::00”的情况
chour=now.wHour+24;
else
chour=now.wHour;
ctime=chour*3600+now.wMinute*60+now.wSecond; //当前时间的秒数
if (ctime>=etime) //若已到新线程结束时间
{
runFlag = FALSE;
EnterCriticalSection(&cs_Screen); //准备进入临界区
cout.width(2); cout<<"新线程结束时间:"<<now.wHour<<':';
cout.width(2); cout<<now.wMinute<<':';
cout.width(2); cout<<now.wSecond<<"(主线程输出信息)"<<endl;
LeaveCriticalSection(&cs_Screen); // 退出临界区
}
Sleep(1000); //主线程睡眠100ms
}
//在整个进程(主线程)结束前留出一段时间好让各子线程完成自己
//的工作(也便于我们从它们结束前的输出来观察线程同步问题)
Sleep(2000);
GetSystemTime(&now);
if (now.wHour>16)
now.wHour -= 16; //转化为北京时间
else
now.wHour += 8;
//由于子线程已经结束,屏幕显示不再需要互斥
cout.width(2); cout<<"主线程结束时间:"<<now.wHour<<':';
cout.width(2); cout<<now.wMinute<<':';
cout.width(2); cout<<now.wSecond<<"(主线程输出信息)"<<endl;
cout.fill(' ');
ch=_getch(); //为便于观察程序不在开发环境运行的输出结果
}
//每个新线程执行的代码
DWORD WINAPI threadwork(LPVOID ThreadNO) //LPVOID即(void *)
{
int napTime; //睡眠时间(毫秒)
int iThreadNO=(int)ThreadNO; // 线程编号
int count=0;
//提示创建的相应线程已经启动
EnterCriticalSection(&cs_Screen); //准备进入临界区
cout.width(2);
cout<<"Thread "<<iThreadNO<<" 开始运行。"<<endl;
LeaveCriticalSection(&cs_Screen); // 退出临界区
//以下线程用于使线程睡眠一段时间(让出CPU共其它线程动
//作,并保证主线程能更及时获得CPU以改变runFlag标志)
while (runFlag)
{
napTime=100+rand()%200; //子线程睡眠时间在100~300ms之间
count++;
EnterCriticalSection(&cs_Screen); //准备进入临界区
cout<<"Thread "<<iThreadNO<<" 第";
cout.width(2);cout<<count<<" 次被调度运行 …… ";
cout<<"(====== 这是线程 "<<iThreadNO<<" 输出的信息 ======)"<<endl;
LeaveCriticalSection(&cs_Screen); // 退出临界区
Sleep(napTime); //睡眠一随机时间更能观察线程并发执行情况
}
EnterCriticalSection(&cs_Screen); //准备进入临界区
cout<<"###### Thread "<<iThreadNO<<" 结束 ######"<<endl;
LeaveCriticalSection(&cs_Screen); // 退出临界区
return (DWORD)iThreadNO;
}
2.3.2 运行结果和分析
(1) 运行结果示例
本程序在输入条件相同的情况下,每次运行各线程的输出顺序可能不同,这是由于各线程并发执行的缘故。下面是某次运行输出结果的示例:
请输入要创建的新线程数:5
新线程运行的时间(秒数):3
新线程开始时间:18:26:21(主线程输出信息)
Thread 0 开始运行。
Thread 0 第 1 次被调度运行 …… (====== 这是线程 0 输出的信息 ======)
Thread 1 开始运行。
Thread 1 第 1 次被调度运行 …… (====== 这是线程 1 输出的信息 ======)
Thread 2 开始运行。
Thread 2 第 1 次被调度运行 …… (====== 这是线程 2 输出的信息 ======)
Thread 0 第 2 次被调度运行 …… (====== 这是线程 0 输出的信息 ======)
Thread 3 开始运行。
Thread 3 第 1 次被调度运行 …… (====== 这是线程 3 输出的信息 ======)
Thread 1 第 2 次被调度运行 …… (====== 这是线程 1 输出的信息 ======)
Thread 4 开始运行。
Thread 4 第 1 次被调度运行 …… (====== 这是线程 4 输出的信息 ======)
Thread 0 第 3 次被调度运行 …… (====== 这是线程 0 输出的信息 ======)
************ 主线程正在运行 …………
Thread 1 第 3 次被调度运行 …… (====== 这是线程 1 输出的信息 ======)
Thread 2 第 2 次被调度运行 …… (====== 这是线程 2 输出的信息 ======)
Thread 3 第 2 次被调度运行 …… (====== 这是线程 3 输出的信息 ======)
Thread 1 第 4 次被调度运行 …… (====== 这是线程 1 输出的信息 ======)
Thread 4 第 2 次被调度运行 …… (====== 这是线程 4 输出的信息 ======)
Thread 0 第 4 次被调度运行 …… (====== 这是线程 0 输出的信息 ======)
Thread 1 第 5 次被调度运行 …… (====== 这是线程 1 输出的信息 ======)
Thread 3 第 3 次被调度运行 …… (====== 这是线程 3 输出的信息 ======)
Thread 4 第 3 次被调度运行 …… (====== 这是线程 4 输出的信息 ======)
Thread 2 第 3 次被调度运行 …… (====== 这是线程 2 输出的信息 ======)
Thread 1 第 6 次被调度运行 …… (====== 这是线程 1 输出的信息 ======)
Thread 0 第 5 次被调度运行 …… (====== 这是线程 0 输出的信息 ======)
Thread 1 第 7 次被调度运行 …… (====== 这是线程 1 输出的信息 ======)
Thread 3 第 4 次被调度运行 …… (====== 这是线程 3 输出的信息 ======)
Thread 4 第 4 次被调度运行 …… (====== 这是线程 4 输出的信息 ======)
Thread 2 第 4 次被调度运行 …… (====== 这是线程 2 输出的信息 ======)
Thread 0 第 6 次被调度运行 …… (====== 这是线程 0 输出的信息 ======)
************ 主线程正在运行 …………
Thread 4 第 5 次被调度运行 …… (====== 这是线程 4 输出的信息 ======)
Thread 3 第 5 次被调度运行 …… (====== 这是线程 3 输出的信息 ======)
Thread 1 第 8 次被调度运行 …… (====== 这是线程 1 输出的信息 ======)
Thread 0 第 7 次被调度运行 …… (====== 这是线程 0 输出的信息 ======)
Thread 4 第 6 次被调度运行 …… (====== 这是线程 4 输出的信息 ======)
Thread 1 第 9 次被调度运行 …… (====== 这是线程 1 输出的信息 ======)
Thread 2 第 5 次被调度运行 …… (====== 这是线程 2 输出的信息 ======)
Thread 0 第 8 次被调度运行 …… (====== 这是线程 0 输出的信息 ======)
Thread 1 第10 次被调度运行 …… (====== 这是线程 1 输出的信息 ======)
Thread 3 第 6 次被调度运行 …… (====== 这是线程 3 输出的信息 ======)
Thread 4 第 7 次被调度运行 …… (====== 这是线程 4 输出的信息 ======)
Thread 1 第11 次被调度运行 …… (====== 这是线程 1 输出的信息 ======)
Thread 2 第 6 次被调度运行 …… (====== 这是线程 2 输出的信息 ======)
Thread 2 第 7 次被调度运行 …… (====== 这是线程 2 输出的信息 ======)
Thread 0 第 9 次被调度运行 …… (====== 这是线程 0 输出的信息 ======)
************ 主线程正在运行 …………
新线程结束时间:18:26:24(主线程输出信息)
###### Thread 0 结束 ######
###### Thread 4 结束 ######
###### Thread 1 结束 ######
###### Thread 3 结束 ######
###### Thread 2 结束 ######
主线程结束时间:18:26:27(主线程输出信息)
(2) 运行结果分析
从程序(进程)的运行结果(输出信息)看,该进程的主线程(执行主函数main( )的代码)和5个子线程(它们都执行threadwork( )函数代码)是并发执行的,且它们被调度执行的次序是不规则的,原因是各子线程睡眠的时间长度是随机的,程序中调用Sleep( )函数使调用者睡眠,相当于调用阻塞原语使调用者阻塞,由于“阻塞”时间的随机性,造成了线程调度执行的顺序的不规则性。
另外,本实验中由于多个线程共享临界资源显示器,故需考虑访问临界资源(即屏幕输出)的互斥性问题。在VC++中单一进程的多个线程实现对某临界资源互斥访问的方法步骤可以是:
① 为该临界资源设置一个互斥变量(相当于互斥信号量),例如程序中的语句:
CRITICAL_SECTION cs_Screen;
此处CRITICAL_SECTION 是临界区类型,它定义了一个临界区对象cs_Screen。
② 对临界区对象初始化,相当于给信号量赋初值1,例如程序中的语句:
InitializeCriticalSection(&cs_Screen);
③ 要访问临界资源时(对本程序来说是要屏幕输出时),调用如下API函数:
EnterCriticalSection(&cs_Screen);
它的功能相当于信号量机制的wait(S)操作(原语)。注:VC++中另有关于信号量的API。
④ 离开临界区时(对本程序来说是屏幕输出语句完成时),调用如下API函数:
LeaveCriticalSection(&cs_Screen);
它的功能相当于信号量机制的signal(S)操作(原语)。
【注意】 上述3个API函数的参数类型都是LPCRITICAL_SECTION,即指向CRITICAL_SECTION型对象的指针型(地址型变量),因此调用时需要在对象名cs_Screen前面加地址运算符“&”。
为了体会互斥访问临界资源的重要性,我们做如下试验:
在新线程执行的代码threadwork( )的while循环体内,注释掉“进入临界区”和“退出临界区”的系统调用函数的语句,重新编译程序,执行程序。以下是这样处理后的某次运行的输出结果中出现错误(输出混乱)的片段:
请输入要创建的新线程数:5
新线程运行的时间(秒数):3
……
Thread 4 第 2 次被调度运行 …… (====== 这是线程 4 输出的信息 ======)
Thread 1 第Thread 4 次被调度运行 …… (====== 这是线程 1 输出的信息 ======)
Thread 1 第Thread 4 次被调度运行 …… (====== 这是线程 1 输出的信息 ======)
2 第 4 次被调度运行 …… (====== 这是线程 2 输出的信息 ======)
Thread 1 第Thread 4 次被调度运行 …… (====== 这是线程 1 输出的信息 ======)
2 第 4 次被调度运行 …… (====== 这是线程 2 输出的信息 ======)
Thread 0 第 4 次被调度运行 …… (====== 这是线程 0 输出的信息 ======)
……
Thread 4 第 6 次被调度运行 …… (====== 这是线程 4 输出的信息 ======)
Thread 1 第 6 次被调度运行 …… (====== 这Thr蟖d?程 1 输出的信息 ======)
Thread 1 第 6 次被调度运行 …… (====== 这Thr蟖d?程 1 输出的信息 ======)
2 第 7 次被调度运行 …… (====== 这是线程 2 输出的信息 ======)
Thread 3 第 5 次被调度运衦ea?…… (====== 这是线程 3 输出的信息 ======)0
赥hread 3 第 5 次被调度运衦ea?…… (====== 这是线程 3 输出的信息 ======)0
?7 次被调度运行 …… (====== 这是线程 0 输出的信息 ======)
Thread 2 第 8 次被调度运行 …… (====== 这是线程 2 输出的信息 ======)
……
上述程序运行结果中标出“输出异常”的部分,是由于子线程访问屏幕(屏幕输出)没有实现互斥,造成了一个线程输出还没有结束,因线程调度另一线程执行,另一线程也进行屏幕显示,从而使多个线程的输出结果混杂在同一行中,造成混乱(错误)。
2.4 进一步要求
1.问题:主线程如何向所创建的子线程传递多个值?以及主线程如何从子线程获得多个数值?
在使用CreateThread( )函数创建线程时可以发现,该函数只允许父线程(调用CreateThread( )的线程)向子线程(新线程)传递一个参数,具体讲是通过该函数的第4个参数pvparam传递的。而且参数pvparam必须是LPVOID(即void*)型的,因此,子线程的执行函数也必须是一个参数且是LPVOID类型的(虽然从语法上讲子线程的执行函数允许有多个参数,但从第2个参数开始,子线程已无法使用它)。既然只能传递一个参数值,那么主线程如何向子线程传递多个值呢?除了进程(主线程main)的全局变量,子线程可以共享之外,解决的办法之一可以是传递结构类型变量或对象给子线程。例如定义如下结构类型SThread:
struct SThread {
char ThreadName[20]; //线程名字(字符串)
DWORD ThreadNO; //线程编号,含义同程序中的iThreadNO
int runTime; //线程计划运行时间(秒数)
int runFlag; //线程运行开关,含义同程序中的runFlag
int exitFlag; //用于子线程告诉主线程,子线程是否已结束
//…… 若需要还可有其它成员
};
在主线程(主函数)中定义如下结构型变量sthread:
SThread sthread;
并给sthread的各成员赋初值,然后用如下语句调用Createthread( )函数:
hThread=CreateThread(NULL,0,threadfunc,(LPVOID)& sthread,0,NULL);
这样就能将sthread中的各个数据成员传递给子线程。而子线程函数可以采用如下形式:
DWORD WINAPI threadfunc(LPVOID lp)
{
SThread *pp=( SThread*)lp;
DWORD iThreadNO=pp->ThreadNO; //获得线程编号
char threadname[20];
strcpy(threadname,pp-> ThreadName); //获得线程名字
……
while (pp-> runFlag) //根据获得的runFlag开关,判断线程是否要结束
{
……
Sleep(napTime);
}
……
return iThreadNO;
}
另外,若子线程是使用return语句终止的而不是被函数ExitThread( )或调用函数TerminateThread( )终止的,则在主线程中,可以调用函数GetExitCodeThread( hThread[i],&ExitCode)借助参数ExitCode获得子线程的返回值(这实际上就是获得子线程返回值的方法)。值得注意的是,函数TerminateThread( )是一个危险的函数,一般不提倡使用它来终止线程。
2.进一步要求
修改程序,允许创建的线程的运行时间不相同,主线程判断各个子线程的生命期是否结束,若某个子线程时间结束,则向该子线程发“关”的信息;子线程收到该信息后将自己的exitFlag置为“真”以通知主线程,然后执行“return iThreadNO;”语句终止自己。主线程收到exitFlag消息后,调用CloseHandle( )关闭该子线程的句柄。
提示:实现方法如下:
可以利用上面的传递多值的思想,首先在主函数中定义如下三个数组:
SThread sthread[100]; //主线程传递给子线程一个数组元素
HANDLE hThread[100]; //线程句柄,假设最多创建100个线程
DWORD ThreadID[100]; //线程id。DWORD即是unsigned long
而后根据输入的线程数iThread,通过循环对sthread[i]的各数据成员赋初值,其中各sthread[i]的成员runTime允许赋不同的值;成员sthread[i].runFlag赋初值1、sthread[i].exitFlag=0。再通过循环创建子线程,语句形式为(i是循环控制变量):
hThread[i]=CreateThread(NULL,0,threadfunc,(LPVOID)& sthread[i],0,& ThreadID[i]);
该函数返回的线程句柄赋给hThread[i],主线程还从ThreadID[i]获得新线程的标识符(id),这两个值对于主线程而言是输入;同时主线程输出参数sthread[i]给新线程。
由于各子线程的运行时间不再相同,因此主线程中判断各线程的生命期需根据各个sthread[i]. runTime分别处理,然后分别对各个sthread[i]. runFlag 执行“关”操作。
子线程中检测到自己的runFlag关后则置位exitFlag,并执行return ExitCode语句结束运行。
主线程中检测到对应的sthread[i].exitFlag标志为“真”后,执行CloseHandle(hThread[i])关闭对应线程的句柄。
实现上述要求的参考源程序可查看电子文档“MulThread_C.cpp”,此处从略。
上机实验三 进程同步(2学时)
3.1 上机实验要求和目的
编写一个程序,实现生产者-消费者问题(P-C问题)的同步算法。通过程序运行时的输出信息,可以清楚地看到在“生产者-消费者”进程(线程)模型中,各进程(线程)的活动情况,从而加深对进程同步问题的理解。本上机实验可使学生了解(掌握)利用Win32 API编写程序实现同步算法的一些方法。
由于同一进程的多个线程之间的通信问题实现比较简单,故本实验采用由主线程创建多个生产者线程和多个消费者线程,然后在这些线程之间实现同步,这使得本实验的程序设计相对比较简单。因此本实验中的进程同步问题实际上是线程同步问题。
实验拓展:通过本次上机实验的训练,应该较容易地用本次上机实验的设计思路,编程实现读者-写者问题和哲学家进餐问题等进程同步问题的算法。读者-写者问题的程序请参看文件夹R_W中程序。
【说明】本实验实际上实现的是同一进程的多个线程之间的同步问题,由于同一进程的多个线程可以共享所属进程的变量,因此实现线程同步的程序比较简单。若要真正实现多个应用程序(进程)之间的P-C问题的同步算法,需要解决多个应用程序之间共享变量(即共享存储区)的问题,包括共享循环缓冲区、缓冲区指针in和out、缓冲区的个数n、信号量empty,full,mutex。共享信号量问题比较简单(Win32 API中,semaphore对象本身是可以供多个application共享的),共享变量(存储区)比较复杂,它实际上属于进程通信问题。解决共享变量最简单的方法之一是采用File Mapping(参看文件夹P_C2中的程序Monitor.cpp、Producer.cpp和Consumer.cpp)。
关于信号量对象(semaphore object)请参阅下面的介绍或者查阅MSDN;关于文件映射(File Mapping)的介绍请参阅MSDN或者参阅文件夹P_C2中的源程序。
3.2 Windows2000/XP的同步和互斥机制
在Windows 2000/XP中,提供了互斥对象、信号量对象、事件对象三种同步对象和相应的系统调用,用于进程和线程的同步。这些同步对象都有一个用户指定的对象名称,不同进程用同样的对象名称来建立或打开对象,从而获得该对象在本进程的句柄。从本质上讲,这些同步对象的能力是相同的,其区别在于适用场合和效率有所不同。
1.互斥对象Mutex就是互斥信号量,在一个时刻只能被一个线程使用。它的相关API包括:
· CreateMutex 创建一个互斥对象,返回对象句柄。
· OpenMutex 打开并返回一个已存在的互斥对象句柄,用于后续访问。
· ReleaseMutex 释放对互斥对象的占用,使之成为可用。
2.信号量对象Semaphore就是资源信号量,初值所取范围在0到指定最大值之间,用于限制并发访问的线程数。它的相关API包括:
· CreateSemaphore 创建一个信号量对象,在输入参数中指定初始值和最大值,返回对象句柄。
· OpenSemaphore 打开并返回一个已存在的信号量对象句柄,用于后续访问。
· releaseSemaphore释放对信号量对象的占用,使之成为可用。
本实验中互斥信号量和资源信号量都采用Semaphore,由主线程main调用CreateSemaphore分别创建三个信号量对象,其初始值分别是1、10和0;生产者和消费者进程(线程)调用WaitForSingleObject作为临界区的进入区代码(相当于wait操作),调用releaseSemaphore作为临界区的退出区代码(相当于signal操作)。详见源程序。
3.事件对象Event相当于触发器,可用于通知一个或多个线程某事件的出现,它的相关API包括:
· CreateEvent 创建一个事件对象,返回对象句柄。
· OpenEvent 打开并返回一个已存在的事件对象句柄,用于后续访问。
· SetEvent和PluseEvent 设置指定事件对象的可用状态。
· ResetEvent 设置指定事件对象为不可用状态。
对这三种同步对象,系统提供了两个同一的等待操作WaitForSingleobject和WaitForMultipleObject。前者可在指定时间内等待指定对象为可用状态;后者可在指定时间内等待多个对象为可用状态。
除了上述三种同步对象外,Windows 2000/XP还提供了一些与进程同步有关的机制,如临界区对象和互锁变量访问API等。
临界区对象只能用于在同一进程内使用的临界区,同一进程的各线程对它的访问是互斥的。把变量说明为CRITICAL_SECTION类型,就可作为临界区使用。相关的API有:
· InitializeCriticalSection 对临界区对象进行初始化。
· TryEnterCriticalSection 非等待方式申请临界区的使用权,申请失败时返回0。
· EnterCriticalSection 等待占用临界区使用权,得到使用权时返回。
· LeaveCriticalSection 释放临界区的使用权。
· DeleteCriticalSection 释放与临界区对象相关的所有系统资源。
互锁变量访问API相当于硬件指令,用于对整型变量的操作,可避免线程间切换对操作连续性的影响。这组API包括:
· InterLockedExchange 32位数据的先读后写原子操作。
· InterLockedCompareExchange 依据比较结果进行赋值的原子操作。
· InterLockedExchangeAdd 先加后存结果的原子操作。
· InterLockedDecrement 先减1后存结果的原子操作。
· InterLockedIncrement 先加1后存结果的原子操作。
3.3 设计方案介绍
3.3.1 数据结构
1.生产者-消费者问题的类
程序中设计了一个描述生产者-消费者问题的类sy_pc:
class sy_pc {
private:
int in; //生产者使用的循环缓冲区的下标
int out; //消费者使用的循环缓冲区的下标
int bcount; //循环缓冲区的个数
HANDLE empty; //指向生产者的私有信号量
HANDLE full; //指向消费者的私有信号量
HANDLE mutex; //指示互斥信号量
lpBuffer buffer; //指向一个循环缓冲区
public:
sy_pc(int); //构造函数
void getbuffer(lpBuffer); //从缓冲区取出一个“产品”
void putbuffer(Buffer); //向缓冲区放入一个“产品”
~sy_pc(); //析构函数
};
2.缓冲区结构
为简单,一个缓冲区结构中只有2个域(2个成员):
typedef struct Buffer {
DWORD data; //生产者要存放的数据
int Number; //生产者线程的序号,为了较清楚地显示信息而设
} *lpBuffer;
3.3.2 程序的总体设计思想
定义类sy_pc的一个全局对象s_pc,其循环缓冲中缓冲区个数为10。定义全局对象的目的是简化各生产者进程(线程)和消费者进程(线程)共享信号量、缓冲区等资源,简化程序。
在类sy_pc的构造函数中,利用Win32 API函数CreateSemaphore( )创建3个信号量对象,这3个对象的句柄分别是empty、full和mutex。empty所指的信号量对象的初值为10(即循环缓冲的缓冲区个数);full所指的信号量对象的初值为0,即初始状态,满缓冲区个数为0;mutex所指的信号量对象的初值为1,用于互斥访问共享变量in或out。关于Semaphore对象以及相关API函数将在下面介绍。
在成员函数putbuffer( )和getbuffer( )中,利用跟Semaphore Objects有关的wait函数和release函数来实现同步。这些函数将在后面具体介绍。
主函数(主线程)中循环创建5个生产者线程和3个消费者线程,然后主线程查询各个子线程是否结束。待各个子线程结束后主线程也结束。
每个生产者线程执行ProducerThread(LPVOID p)的代码,每个消费者线程执行Consumer- Thread(LPVOID p)的代码。为演示方便,每个生产者存放9个“产品”后便结束运行,并返回数值1,以便主线程查询;每个消费者取完15个“产品”后也结束运行,返回1。程序中,5个生产者总共存放了45个产品,而3个消费者总共取了45个产品,这样规定是为了所有生产者和消费者都能运行结束。
程序中各线程对于屏幕访问的互斥,采用设置CRITICAL_SECTION 对象cs_Screen来实现,这在上机实验三中已经使用过。当然也可以用Mutex对象或用设置初值为1的Semaphore对象来实现对屏幕的互斥访问,就像本程序中的mutex那样。CRITICAL_SECTION 对象只用于互斥,不能用于同步,且只能用于同一进程的各线程之间的互斥。
3.3.3 程序中所用API函数介绍
1.CreateSemaphore( )函数
功能:
创建一个有名字的或无名字的semaphore对象。
格式:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
// pointer to security attributes
LONG lInitialCount, // initial count
LONG lMaximumCount, // maximum count
LPCTSTR lpName // pointer to semaphore-object name
);
参数说明:
lpSemaphoreAttributes:是一个指向SECURITY_ATTRIBUTES结构的指针,该结构确定返回的句柄能否被子进程继承。取值NULL,表示不能被继承。
lInitialCount:semaphore对象的计数器初值。
lMaximumCount:semaphore对象的计数器的最大值。
lpName:指向semaphore对象名字(字符串)的指针,取值NULL表示semaphore对象没有名字。
函数返回值:
若函数执行成功,则返回指向semaphore对象的句柄。若新创建semaphore对象的名字对应的对象已经存在,则返回的句柄指向已存在的semaphore对象,并不重新创建对象。
若函数执行失败,则返回NULL。
关于semaphore对象的补充介绍
semaphore对象是一个同步对象,它维持一个count,使其在0~给定最大值之间变化(这与教科书上的信号量不同,当cout的值降到0时,对semaphore对象执行wait函数,count的值不再减小)。当有线程等待它(例如对该semaphore对象执行wait一类函数)时,该semaphore对象的count减小;当有线程释放它(对该semaphore对象执行ReleaseSemaphore( )函数)时,它的count值增大。当count的值为0时,此时若有线程对该semaphore对象执行wait类操作,则调用wait函数的线程将暂停(挂起、阻塞)。注:和一般信号量不同,VC++中的semaphore对象的count值不会出现负值。wait类函数如下所列:
SignalObjectAndWait( )、WaitForSingleObject( )、WaitForSingleObjectEx( );WaitForMultipleObjects( )、WaitForMultipleObjectsEx( );MsgWaitForMultipleObjects( )、MsgWaitForMultipleObjectsEx( )等。本实验采用的wait函数是WaitForSingleObject( )。
2.WaitForSingleObject( )函数
功能:等待一个事件信号直至信号出现或者超时。若等到信号则返回WAIT_OBJECT_0(即0),若等待超过dwMiliseconds时间还是无信号,则返回WAIT_TIMEOUT(即258)。若函数调用失败,则返回WAIT_FAILED (即-1)。
格式:
DWORD WaitForSingleObject (
HANDLE hHandle, // 事件的句柄
DWORD dwMilliseconds // 最大等待时间,以ms计时。
)
以下对此函数在本程序中的使用做一些补充介绍。程序中调用此函数的形式如下:
WaitForSingleObject(full, INFINITE);
其中full是指向semaphore对象的句柄,INFINITE表示超时时间为无穷。执行该函数时,若full对应的semaphore对象的count值大于0,则将count的值减1;若full对应的semaphore对象的count值等于0,则调用此函数的线程无限等待(相当于阻塞)并插入到一个FIFO等待队列,直到有线程对full对应的semaphore对象执行ReleaseSemaphore( )函数才唤醒队列首部的一个线程。同样,也可对empty和mutex执行此函数,其意义相同。
3.ReleaseSemaphore( )函数
功能:使指定的semaphore对象的count值增加给定的值。
格式:
BOOL ReleaseSemaphore(
HANDLE hSemaphore, // handle to the semaphore object
LONG lReleaseCount, // amount to add to current count
LPLONG lpPreviousCount // address of previous count
);
参数说明:
hSemaphore:semaphore对象的句柄,由CreateSemaphore( )创建对象时返回。
lReleaseCount:count的增量,本程序使用的增量为1。
lpPreviousCount:count原先值的地址。本程序使用NULL,表示不关心count的原先值。
返回值:函数执行成功,返回非0值;执行失败,返回0.
4.CreateThread( )函数
此创建线程函数,参见2.2.2节的介绍
5.有关临界区(CRITICAL_SECTION)的函数
InitializeCriticalSection( )、EnterCriticalSection( )、LeaveCriticalSection( ),参看2.2.2节。
6.Sleep( )函数
参看2.2.2节的介绍。程序中增加此函数调用是为了更清楚地观察线程的执行过程,生产者-消费者问题的同步算法本身是不需要故意延时等待的。
3.4 源程序与运行结果
3.4.1 程序源代码
1.头文件
// ********** P_C.h **********
#include <iostream.h>
#include <windows.h>
#include <time.h> // time( )
//缓冲区结构
typedef struct Buffer {
DWORD data; //数据
int Number; //线程序号
} *lpBuffer;
// 生产者线程的工作代码
DWORD WINAPI ProducerThread(LPVOID p);
// 消费者线程的工作代码
DWORD WINAPI ConsumerThread(LPVOID p);
// 描述生产者-消费者的类
class sy_pc {
private:
int in; //生产者使用的循环缓冲区的下标
int out; //消费者使用的循环缓冲区的下标
int bcount; //循环缓冲区的个数
HANDLE empty; //指向生产者的私有信号量
HANDLE full; //指向消费者的私有信号量
HANDLE mutex; //指示互斥信号量
lpBuffer buffer; //指向一个循环缓冲区
public:
sy_pc(int); //构造函数
int getbuffer(lpBuffer); //从缓冲区取出一个“产品”
int putbuffer(Buffer); //向缓冲区放入一个“产品”
~sy_pc(); //析构函数
};
sy_pc::sy_pc(int c) //构造函数
{
in=0; //生产者对循环缓冲操作的初始位置
out=0; //生产者对循环缓冲操作的初始位置
bcount=c; //缓冲区的个数为c
empty=CreateSemaphore(NULL,bcount,bcount,"sempty");
full=CreateSemaphore(NULL,0,bcount,"sfull");
mutex=CreateSemaphore(NULL,1,1,"smutex");
buffer=new Buffer[bcount]; //循环缓冲是个顺序队列(数组)
}
int sy_pc::getbuffer(lpBuffer Buf) //取到的产品由参数Buf带回
{
WaitForSingleObject(full,INFINITE); //先对私有信号量执行wait
WaitForSingleObject(mutex,INFINITE); //再对公用信号量(互斥信号量)指向wait
*Buf=buffer[out]; //在out所指位置取出一个产品
out=(out+1)%bcount; //out循环增1
ReleaseSemaphore(mutex,1,NULL); //mutex的count增1
ReleaseSemaphore(empty,1,NULL); //empty的count增1
return out;
}
int sy_pc::putbuffer(Buffer b)
{
WaitForSingleObject(empty,INFINITE);
WaitForSingleObject(mutex,INFINITE);
buffer[in]=b;
in=(in+1)%bcount;
ReleaseSemaphore(mutex,1,NULL);
ReleaseSemaphore(full,1,NULL);
return in;
}
sy_pc::~sy_pc() //析构函数
{
delete []buffer;
}
2.P_C.cpp程序
// 进程同步:生产者-消费者问题
#include "P_C.h"
#define M 5 //假设有5个生产者
#define N 3 //假设有3个消费者
CRITICAL_SECTION cs_Screen; //因多线程竞争屏幕,故设此临界区控制变量
sy_pc s_pc(10); //定义全局对象,简化参数传递方式
void main() //主函数(主线程执行的代码)
{
HANDLE hThread[100];// 假设最多创建100个线程
int i,j,sum;
DWORD ExitCode;
InitializeCriticalSection(&cs_Screen); //初始化临界区对象cs_Screen
srand((unsigned int)time(NULL)); // 使用当前时间为随机序列的"种子"
for (i=1,j=0;i<=M;i++,j++) //创建5个生产者线程
{ //每个生产者线程执行ProducerThread( )的代码
hThread[j]=CreateThread(NULL,0,ProducerThread, (LPVOID)&i,0,NULL);
Sleep(10);
}
for (i=1;i<=N;i++,j++) //创建3个消费者线程
{ //每个消费者线程执行ConsumerThread ( )的代码
hThread[j]=CreateThread(NULL,0,ConsumerThread, (LPVOID)&i,0,NULL);
Sleep(10);
}
while (true)//主线程不断循环,直到所有子线程结束
{
EnterCriticalSection(&cs_Screen); //准备进入临界区
cout<<"主线程正在运行 ***********************"<<endl;
LeaveCriticalSection(&cs_Screen); // 退出临界区
Sleep(1000);
for (i=0,sum=0;i<j;i++)//总共有j个子线程
{
ExitCode=0;
GetExitCodeThread(hThread[i],&ExitCode);//获取子线程的退出码
if (ExitCode==1) //如果该线程已结束,则统计入sum
sum=sum+ExitCode;
}
if (sum==j) //若所有子线程已经结束,则主线程也结束
break;
}
cout<<"所有子线程已经结束,主线程也将结束 ************"<<endl;
}
// 生产者线程工作代码
DWORD WINAPI ProducerThread(LPVOID p)
{
int *ip=(int*)p; //转化为整型指针
int ThreadNumber=*ip; //取整型指针所指变量的值
int naptime,in;
Buffer b; //定义一个缓冲区变量b
for (int i=1;i<=3*N;i++)//每个生产者共生产9个产品放入缓冲区
{
b.data=rand(); //b的data成员用随机数演示
b.Number=ThreadNumber; //b的Number成员值是上缠着线程的序号
in=s_pc.putbuffer(b); //将b放入缓冲区,返回当前的下标in的值
EnterCriticalSection(&cs_Screen); //准备进入临界区
cout<<"Producer "<<ThreadNumber<<" 向缓冲区投放了第 "
<<i<<" 个数据 "<<b.data<<"。in="<<in<<endl;
LeaveCriticalSection(&cs_Screen); // 退出临界区
naptime=100+rand()%200; //睡眠100~300ms
Sleep(naptime);
}
EnterCriticalSection(&cs_Screen); //准备进入临界区
cout<<"Producer "<<ThreadNumber<<" 运行完毕"<<endl;
LeaveCriticalSection(&cs_Screen); // 退出临界区
return 1; //子线程的返回值(退出码)为1
}
// 消费者线程工作代码
DWORD WINAPI ConsumerThread(LPVOID p)
{
int ThreadNumber=*((int*)p);
int naptime,out;
Buffer b;
for (int i=1;i<=3*M;i++)//每个消费者共获取15个产品后结束
{
out=s_pc.getbuffer(&b);//从缓冲区取产品放在b中,返回下标out
EnterCriticalSection(&cs_Screen); //准备进入临界区
cout<<"Consumer "<<ThreadNumber<<" 从缓冲区取得了第 "
<<i<<" 个数据 "<<b.data;
cout<<", 它是Producer "<<b.Number<<"存放的。out="<<out<<endl;
LeaveCriticalSection(&cs_Screen); // 退出临界区
naptime=100+rand()%200;
Sleep(naptime); //睡眠等待一段时间只是为了演示
}
EnterCriticalSection(&cs_Screen); //准备进入临界区
cout<<"Consumer "<<ThreadNumber<<" 运行完毕"<<endl;
LeaveCriticalSection(&cs_Screen); // 退出临界区
return 1; //子线程的返回值(退出码)为1
}
3.4.2 程序运行输出结果
程序每一次运行的结果可能都不一样,以下是某次运行的输出结果。可以画出循环缓冲区图(有10个缓冲区),根据下面的运行记录,分析每个生产者和消费者的活动情况以及缓冲区的存储状态,加深对生产者-消费者问题的感性认识和理解。
Producer 1 向缓冲区投放了第 1 个数据 18310。in=1
Producer 2 向缓冲区投放了第 1 个数据 18733。in=2
Producer 3 向缓冲区投放了第 1 个数据 973。in=3
Producer 4 向缓冲区投放了第 1 个数据 27615。in=4
Producer 5 向缓冲区投放了第 1 个数据 29401。in=5
Consumer 1 从缓冲区取得了第 1 个数据 18310, 它是Producer 1存放的。out=1
Consumer 2 从缓冲区取得了第 1 个数据 18733, 它是Producer 2存放的。out=2
Consumer 3 从缓冲区取得了第 1 个数据 973, 它是Producer 3存放的。out=3
主线程正在运行 ***********************
Producer 3 向缓冲区投放了第 2 个数据 14009。in=6
Consumer 2 从缓冲区取得了第 2 个数据 27615, 它是Producer 4存放的。out=4
Producer 1 向缓冲区投放了第 2 个数据 2974。in=7
Consumer 1 从缓冲区取得了第 2 个数据 29401, 它是Producer 5存放的。out=5
Producer 4 向缓冲区投放了第 2 个数据 7746。in=8
Consumer 2 从缓冲区取得了第 3 个数据 14009, 它是Producer 3存放的。out=6
Producer 2 向缓冲区投放了第 2 个数据 16208。in=9
Consumer 3 从缓冲区取得了第 2 个数据 2974, 它是Producer 1存放的。out=7
Producer 5 向缓冲区投放了第 2 个数据 131。in=0
Producer 3 向缓冲区投放了第 3 个数据 35。in=1
Producer 1 向缓冲区投放了第 3 个数据 4973。in=2
Consumer 1 从缓冲区取得了第 3 个数据 7746, 它是Producer 4存放的。out=8
Producer 4 向缓冲区投放了第 3 个数据 7092。in=3
Producer 5 向缓冲区投放了第 3 个数据 9547。in=4
Producer 3 向缓冲区投放了第 4 个数据 7709。in=5
Producer 1 向缓冲区投放了第 4 个数据 16456。in=6
Producer 2 向缓冲区投放了第 3 个数据 22285。in=7
Consumer 3 从缓冲区取得了第 3 个数据 16208, 它是Producer 2存放的。out=9
Consumer 2 从缓冲区取得了第 4 个数据 131, 它是Producer 5存放的。out=0
Producer 4 向缓冲区投放了第 4 个数据 7080。in=8
Producer 1 向缓冲区投放了第 5 个数据 8390。in=9
Consumer 1 从缓冲区取得了第 4 个数据 35, 它是Producer 3存放的。out=1
Producer 5 向缓冲区投放了第 4 个数据 20486。in=0
Consumer 3 从缓冲区取得了第 4 个数据 4973, 它是Producer 1存放的。out=2
Producer 3 向缓冲区投放了第 5 个数据 20011。in=1
Producer 2 向缓冲区投放了第 4 个数据 12362。in=2
Consumer 2 从缓冲区取得了第 5 个数据 7092, 它是Producer 4存放的。out=3
Producer 1 向缓冲区投放了第 6 个数据 30698。in=3
Consumer 3 从缓冲区取得了第 5 个数据 9547, 它是Producer 5存放的。out=4
Producer 4 向缓冲区投放了第 5 个数据 22041。in=4
Consumer 1 从缓冲区取得了第 5 个数据 7709, 它是Producer 3存放的。out=5
Producer 5 向缓冲区投放了第 5 个数据 1998。in=5
Producer 3 向缓冲区投放了第 6 个数据 30948。in=6
Consumer 2 从缓冲区取得了第 6 个数据 16456, 它是Producer 1存放的。out=6
Consumer 3 从缓冲区取得了第 6 个数据 22285, 它是Producer 2存放的。out=7
Producer 2 向缓冲区投放了第 5 个数据 10647。in=7
主线程正在运行 ***********************
Consumer 3 从缓冲区取得了第 7 个数据 7080, 它是Producer 4存放的。out=8
Producer 5 向缓冲区投放了第 6 个数据 26250。in=8
Consumer 2 从缓冲区取得了第 7 个数据 20486, 它是Producer 5存放的。out=0
Producer 4 向缓冲区投放了第 6 个数据 29660。in=9
Consumer 1 从缓冲区取得了第 6 个数据 8390, 它是Producer 1存放的。out=0
Producer 3 向缓冲区投放了第 7 个数据 27954。in=0
Consumer 2 从缓冲区取得了第 8 个数据 20011, 它是Producer 3存放的。out=1
Producer 2 向缓冲区投放了第 6 个数据 666。in=1
Consumer 3 从缓冲区取得了第 8 个数据 12362, 它是Producer 2存放的。out=2
Producer 1 向缓冲区投放了第 7 个数据 4704。in=2
Consumer 1 从缓冲区取得了第 7 个数据 30698, 它是Producer 1存放的。out=3
Producer 5 向缓冲区投放了第 7 个数据 17753。in=3
Consumer 2 从缓冲区取得了第 9 个数据 22041, 它是Producer 4存放的。out=4
Producer 4 向缓冲区投放了第 7 个数据 14823。in=4
Producer 3 向缓冲区投放了第 8 个数据 24774。in=5
Consumer 3 从缓冲区取得了第 9 个数据 1998, 它是Producer 5存放的。out=5
Consumer 1 从缓冲区取得了第 8 个数据 30948, 它是Producer 3存放的。out=6
Producer 2 向缓冲区投放了第 7 个数据 5304。in=6
Producer 1 向缓冲区投放了第 8 个数据 22083。in=7
Consumer 3 从缓冲区取得了第 10 个数据 10647, 它是Producer 2存放的。out=7
Consumer 2 从缓冲区取得了第 10 个数据 26250, 它是Producer 5存放的。out=8
Producer 4 向缓冲区投放了第 8 个数据 23901。in=8
Consumer 1 从缓冲区取得了第 9 个数据 29660, 它是Producer 4存放的。out=9
Producer 5 向缓冲区投放了第 8 个数据 26718。in=9
Consumer 3 从缓冲区取得了第 11 个数据 27954, 它是Producer 3存放的。out=0
Producer 3 向缓冲区投放了第 9 个数据 28204。in=0
Consumer 2 从缓冲区取得了第 11 个数据 666, 它是Producer 2存放的。out=1
主线程正在运行 ***********************
Producer 4 向缓冲区投放了第 9 个数据 2926。in=1
Consumer 1 从缓冲区取得了第 10 个数据 4704, 它是Producer 1存放的。out=2
Producer 2 向缓冲区投放了第 8 个数据 19522。in=2
Consumer 2 从缓冲区取得了第 12 个数据 17753, 它是Producer 5存放的。out=3
Producer 1 向缓冲区投放了第 9 个数据 31161。in=3
Producer 3 运行完毕
Consumer 3 从缓冲区取得了第 12 个数据 14823, 它是Producer 4存放的。out=4
Producer 5 向缓冲区投放了第 9 个数据 1021。in=4
Producer 4 运行完毕
Producer 1 运行完毕
Consumer 1 从缓冲区取得了第 11 个数据 24774, 它是Producer 3存放的。out=5
Producer 2 向缓冲区投放了第 9 个数据 3273。in=5
Producer 5 运行完毕
Consumer 2 从缓冲区取得了第 13 个数据 5304, 它是Producer 2存放的。out=6
Consumer 3 从缓冲区取得了第 13 个数据 22083, 它是Producer 1存放的。out=7
Consumer 2 从缓冲区取得了第 14 个数据 23901, 它是Producer 4存放的。out=8
Consumer 1 从缓冲区取得了第 12 个数据 26718, 它是Producer 5存放的。out=9
Producer 2 运行完毕
Consumer 3 从缓冲区取得了第 14 个数据 28204, 它是Producer 3存放的。out=0
Consumer 2 从缓冲区取得了第 15 个数据 2926, 它是Producer 4存放的。out=1
Consumer 1 从缓冲区取得了第 13 个数据 19522, 它是Producer 2存放的。out=2
Consumer 3 从缓冲区取得了第 15 个数据 31161, 它是Producer 1存放的。out=3
Consumer 1 从缓冲区取得了第 14 个数据 1021, 它是Producer 5存放的。out=4
主线程正在运行 ***********************
Consumer 2 运行完毕
Consumer 3 运行完毕
Consumer 1 从缓冲区取得了第 15 个数据 3273, 它是Producer 2存放的。out=5
Consumer 1 运行完毕
所有子线程已经结束,主线程也将结束 ************
从程序运行时的输出结果,可以清楚地看到各个线程(5个生产者线程、3个消费者线程以及主线程)并发执行的情况,它们执行的顺序和推进的速度都是不可预知的(异步性)。每次重新执行程序,各线程之间的执行顺序可能都不同。
上机实验四 进程通信 (2学时)
4.1 上机实验内容和要求
要求设计实现进程间通信(Interprocess Communication)的程序,而不是同一进程的多个线程之间的通信。Win32 API提供了多种进程间通信的方法,包括剪贴板(Clipboard)、COM(OLE)、动态数据交换(Dynamic Data Exchange,DDE)、文件映射(File Mapping)、邮件槽(Mailslot)、管道(Pipes)、远程过程调用(RPC)、Windows套接字(Windows Sockets)等。本实验采用相对比较容易实现的有名管道(Named Pipe)实现进程间的通信。学生也可采用File Mapping或Mailslot自行设计本实验。File Mapping的使用可参考P_C2文件夹中程序,该程序就是使用File Mapping作为进程通信工具来实现诸进程共享循环缓冲区和缓冲区指针in、out等的共享的,再结合信号量对象(semaphore object)来实现生产者-消费者问题的同步算法的。另外,采用Mailslot实现进程通信的程序在文件夹Mailslot中。邮件槽是一种不定长和不可靠的单向消息通信机制。Named Pipe和File Mapping都是双向通信方式。
【注】本实验用的Named Pipe属于共享文件的通信,而“进程同步”实验中用的File Mapping属于共享存储区的通信。
4.2 相关知识
4.2.1 Windows中的pipe
在Windows 2000/XP中的管道是一条在进程间以字节流方式传送的通信通道,Win32 API提供两种管道:无名管道(anonymous pipes)和赋名管道(named pipes)。无名管道用于有关联的进程之间的信息交换(通信),其典型的使用方式是用于标准I/O的重定向,从而使子进程可以同父进程交换信息。为了实现双向交换信息,需要建立两个无名管道。无名管道不能实现无关联进程之间的通信。
有名管道(named pipes)可用于无关联进程,甚至不同计算机中的进程之间的通信。典型的使用方式是:有名管道的服务器(named-pipe server)进程用一个众所周知的名字,利用CreateNamedPipe创建一个named pipe,而知道管道名字的客户进程(named-pipe client)就可以用OpenFile打开此管道并获得其句柄,并使用此句柄调用读操作(ReadFile)和写操作(WriteFile)来读写管道,从而实现与服务器进程的通信。注:上述ReadFile和WriteFile是阻塞方式工作的,也可使用非阻塞方式的ReadFileEx和WriteFileEx来读写管道。
4.2.2 实验所用的几个Win32 API函数介绍
在Windows 2000/XP系统中,要使用管道实现通信,都需要通过Win32 API调用来完成。本实验涉及的API函数如下:
1.CreateNamedPipe函数
功能:
创建一个有名管道实例(an instance of a named pipe),返回管道句柄(handle)。随后对管道的操作都是对该handle进行的。管道服务器不仅可用此函数创建第一个命名管道实例,而且根据需要,以后还可以用此函数(管道名字不变)创建多个实例。
格式:
HANDLE CreateNamedPipe(
LPCTSTR lpName, // pipe name
DWORD dwOpenMode, // pipe open mode
DWORD dwPipeMode, // pipe-specific modes
DWORD nMaxInstances, // maximum number of instances
DWORD nOutBufferSize, // output buffer size
DWORD nInBufferSize, // input buffer size
DWORD nDefaultTimeOut, // time-out interval
LPSECURITY_ATTRIBUTES lpSecurityAttributes // SD
);
参数说明:
lpName:管道名字符串,该名字必须是如下形式:\\.\pipe\pipename
dwOpenMode:指定pipe的打开模式。例如,取值PIPE_ACCESS_DUPLEX表示打开后对管道既可以读也可以写,即双向通信。
dwPipeMode:指定管道的类型,以及读,等待管道的模式。
nMaxInstances:指定能创建的最大实例数,其值范围在1~ PIPE_UNLIMITED_INSTANCES之间。
nOutBufferSize:输出缓冲区大小(字节数)
nInBufferSize:输入缓冲区大小(字节数)
nDefaultTimeOut:缺省的超时数,单位ms
lpSecurityAttributes:安全属性。NULL表示缺省的安全属性,以及返回的句柄不能被继承。
2.ConnectNamedPipe函数
功能:
named pipe服务器进程调用此函数,用于等待客户进程链接此命名管道的一个最新的实例。客户进程使用CreateFile or CallNamedPipe函数来连接命(有)名管道。
格式:
BOOL ConnectNamedPipe(
HANDLE hNamedPipe, // handle to named pipe
LPOVERLAPPED lpOverlapped // overlapped structure
);
参数说明:
hNamedPipe:命名管道实例的句柄,该句柄在调用CreateNamedPipe时获得。
lpOverlapped:指向一个OVERLAPPED结构。可取值NULL。
返回值:
当函数执行成功,返回非0值;当函数失败,返回0。如果client(客户)在Server(服务器)调用此函数之前已连接Pipe,则此函数返回0且GetLastError函数返回ERROR_PIPE_CONNECTED。如果一个客户在服务器调用CreateNamedPipe至调用ConnectNamedPipe时间之内连接管道,则这种情况就会发生。在这种情况下,虽然此函数返回0,但服务器和客户之间已建立了一个好的连接。即客户和服务器之间连接成功有两种情况:一是此函数返回非0值;二是此函数虽然返回0,但GetLastError函数返回ERROR_PIPE_CONNECTED。具体处理请参看本实验的参考程序。
3.WaitNamedPipe函数
功能:
调用进程(Named-Pipe Client)一直等待,直到给定时间超时,或者指定的named pipe可以被连接(即pipe服务器进程执行了ConnectNamedPipe操作)。
格式:
BOOL WaitNamedPipe(
LPCTSTR lpNamedPipeName, // pipe name
DWORD nTimeOut // time-out interval
);
参数说明:
lpNamedPipeName:命名管道名字。
nTimeOut:指定超时的毫秒数。
【注】若指定名字的管道不存在,则此函数立即返回(并不等待超时)。
4.SetNamedPipeHandleState函数
功能:
设置指定管道的读模式和阻塞模式(设置/改变管道的新模式)。
格式:
BOOL SetNamedPipeHandleState(
HANDLE hNamedPipe, // handle to named pipe
LPDWORD lpMode, // new pipe mode
LPDWORD lpMaxCollectionCount, // maximum collection count
LPDWORD lpCollectDataTimeout // time-out value
);
参数说明
hNamedPipe:管道的句柄。
lpMode:指定命名管道新的模式,包括读模式PIPE_READMODE_BYTE和PIPE_READMODE _MESSAGE(它们可以组合)以及等待模式PIPE_WAIT或PIPE_NOWAIT。
lpMaxCollectionCount:当handle在服务器端或服务器和客户在同一台计算机时,此值必须为NULL
lpCollectDataTimeout:当handle在服务器端或服务器和客户在同一台计算机时,此值必须为NULL
5.WriteFile函数
该函数已在“上机实验八 设备管理——磁盘I/O”实验中作了简单介绍,这里做一些补充:
当用此函数写一个管道时,如果管道缓冲区(其大小由调用CreateNamedPipe函数指定)已写满但仍有要写的数据,则写操作可能不能完成(即WriteFile函数不能返回)。但读端进程调用ReadFile读管道而使管道缓冲区空出更多可用空间时,最终能使写操作完成(即能使WriteFile函数返回)。
6.ReadFile函数
该函数已在“上机实验八 设备管理——磁盘I/O”实验中作了简单介绍,这里做一些补充:
(1) 该函数在出现下列情况之一才会返回:管道(pipe)写端的写操作已经完成;已经读到函数所请求读的字节数的信息;出现一个错误。因此,对于管道而言,当写端进程没有写完全部数据时,读端进程的读不会结束(阻塞方式),这在某种程度上起到了同步作用。
(2) 当一个命名管道的读模式设置成PIPE_READMODE_MESSAGE,而所读的消息长度大于此函数所指定要读的字节数时,则ReadFile函数出错返回FALSE,此时调用GetLastError函数,获得的出错代号为ERROR_MORE_DATA。这种情况实际上数据已经正确读入,只是没有读完而已,可继续读。具体请看本实验的源程序。
7.DisconnectNamedPipe函数
功能:用于在named pipe服务器端断开与管道客户进程的连接。
格式:
BOOL DisconnectNamedPipe(
HANDLE hNamedPipe // handle to named pipe
);
8.FlushFileBuffers函数
功能:将指定文件的缓冲数据写入磁盘并请缓冲区。
格式:
BOOL FlushFileBuffers(
HANDLE hFile // handle to file
);
8.其它函数
CreateFile: 该函数已在“上机实验八 设备管理——磁盘I/O”实验中作了简单介绍,这里用此函数打开一个命名管道;
CreateThread:创建线程,参看“创建线程”实验的介绍。
CloseHandle:关闭句柄。
CreateProcess:创建进程,参看“创建进程”实验的介绍。
4.3 实验总体设计
本实验是在Windows 2000/XP+VC++6.0环境下利用Win32 API设计实现的;进程通信机制采用有名管道;程序分两个部分,即Named pipe server和Named pipe client。
服务器设计成多线程管道服务器,服务器主线程的总体框架是:
1.首先创建一个工作线程(WorkThread),它的任务是创建若干个进程(pipe client);
2.主线程开始无限循环:调用CreateNamedPipe创建一个有名管道实例;调用ConnectNamedPipe等待管道客户进程请求服务;若有客户进程请求管道连接,则创建一个子线程来为该客户提供服务(与该客户实现管道双向通信);主线程返回循环开头。
3.若没有客户请求连接,则关闭刚创建的管道实例(named pipe instance)的句柄后重新循环。
管道客户程序的总体框架是:
1.调用CreateFile函数获得服务器创建的管道实例的句柄;
2.调用WaitNamedPipe函数,等待(请求)与服务器的管道连接;
3.若连接成功,如需要可则调用SetNamedPipeHandleState按需要设置管道的读模式等;
4.执行如下的循环操作:
(1) 调用WriteFile函数向服务器发送请求信息;
(2) 等待并接收服务器的应答信息(用ReadFile函数实现)。这样循环往复,实现与服务器的双向通信,直到完成通信任务后结束进程。
服务器创建的为客户提供服务的子线程也是用ReadFile和WriteFile函数实现与客户进程的管道通信的,此处不再赘述。
4.4 源程序与运行结果
4.4.1 程序源代码
1.Server程序源代码
#include <windows.h>
#include <iostream.h>
#define BUFSIZE 128
#define PIPE_TIMEOUT 2000
HANDLE createProcess(char* name,int num);
VOID MyErrExit(char *);
DWORD InstanceThread(LPVOID);
DWORD WorkThread(LPVOID);
char Reply[][128] = {"Hello, I am pipe server",
"My name is Peter. Tom, welcome.",
"Certianly, pipe can be used in\n\t interprocess communication.",
"Bye-bye, Tom."};
int threadNo=0; // serial number of thread instance
CRITICAL_SECTION cs_Screen; // CRITICAL_SECTION object
// The following example is a multithreaded pipe server. It has
// a main thread with a loop that creates a pipe instance and
// waits for a pipe client to connect. When a pipe client
// connects, the pipe server creates a thread to service that
// client and then continues to execute the loop. It is
// possible for a pipe client to connect successfully to the
// pipe instance in the interval between calls to the
// CreateNamedPipe and ConnectNamedPipe functions. If this
// happens, ConnectNamedPipe returns zero, and GetLastError
// returns ERROR_PIPE_CONNECTED.
// The thread created to service each pipe instance reads
// requests from the pipe and writes replies to the pipe
// until the pipe client closes its handle. When this
// happens, the thread flushes the pipe, disconnects,
// closes its pipe handle, and terminates.
DWORD main() // Pipe Server
{
BOOL fConnected;
DWORD dwThreadId;
HANDLE hPipe,hThrd,hwkThd;
int nProcess=3; // number of pipe client
// name of the pipe
LPTSTR lpszPipename = "\\\\.\\pipe\\mynamedpipe";
// init. CRITICAL_SECTION object cs_Screen
InitializeCriticalSection(&cs_Screen);
HWND hWindow;
// obtain a handle to the foreground window
hWindow=GetForegroundWindow();
// changes the position and dimensions of the specified window
MoveWindow(hWindow,0,10,750,385,TRUE);
// create a thread. it create processes of pipe client
hwkThd=CreateThread(
NULL, // no security attribute
0, // default stack size
(LPTHREAD_START_ROUTINE) WorkThread,
(LPVOID) nProcess, // thread parameter
0, // not suspended
&dwThreadId); // returns thread ID
if (hwkThd == NULL)
MyErrExit("CreateThread1");
// The main loop creates an instance of the named pipe and
// then waits for a client to connect to it. When the client
// connects, a thread is created to handle communications
// with that client, and the loop is repeated.
for (;;) // main loop
{
// create an instance of the named pipe
hPipe = CreateNamedPipe(
lpszPipename, // pipe name
PIPE_ACCESS_DUPLEX, // read/write access
PIPE_TYPE_MESSAGE | // message type pipe
PIPE_READMODE_MESSAGE | // message-read mode
PIPE_WAIT, // blocking mode
PIPE_UNLIMITED_INSTANCES, // max. instances
BUFSIZE, // output buffer size
BUFSIZE, // input buffer size
PIPE_TIMEOUT, // client time-out
NULL); // no security attribute
if (hPipe == INVALID_HANDLE_VALUE)
MyErrExit("CreatePipe");
// Wait for the client to connect; if it succeeds,
// the function returns a nonzero value. If the function returns
// zero, GetLastError returns ERROR_PIPE_CONNECTED.
fConnected = ConnectNamedPipe(hPipe, NULL) ?
TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
if (fConnected)
{
// Create a thread for this client.
threadNo++;
hThrd = CreateThread(
NULL, // no security attribute
0, // default stack size
(LPTHREAD_START_ROUTINE) InstanceThread,
(LPVOID) hPipe, // thread parameter
0, // not suspended
&dwThreadId); // returns thread ID
if (hThrd == NULL)
MyErrExit("CreateThread2");
}
else
// The client could not connect, so close the pipe.
CloseHandle(hPipe);
}
return 1;
}
DWORD InstanceThread(LPVOID lpvParam)
{
int nThread=threadNo;
CHAR chRequest[BUFSIZE];
CHAR chReply[BUFSIZE];
DWORD cbBytesRead, cbReplyBytes, cbWritten;
BOOL fSuccess;
int index=0;
HANDLE hPipe;
// The thread's parameter is a handle to a pipe instance.
hPipe = (HANDLE) lpvParam;
while (1)
{
// Read client requests from the pipe.
fSuccess = ReadFile(
hPipe, // handle to pipe
chRequest, // buffer to receive data
BUFSIZE, // size of buffer
&cbBytesRead, // number of bytes read
NULL); // not overlapped I/O
if (! fSuccess || cbBytesRead == 0)
break;
EnterCriticalSection(&cs_Screen);
cout<<chRequest<<endl;
LeaveCriticalSection(&cs_Screen);
strcpy(chReply,Reply[index]);
cbReplyBytes=strlen(Reply[index])+1;
index=(index+1)%4;
// Write the reply to the pipe.
fSuccess = WriteFile(
hPipe, // handle to pipe
chReply, // buffer to write from
cbReplyBytes, // number of bytes to write
&cbWritten, // number of bytes written
NULL); // not overlapped I/O
if (! fSuccess || cbReplyBytes != cbWritten)
MyErrExit("Write the pipe failrd.");
Sleep(1000); //等待1000ms,为了便于观察并发执行的情况
}
// Flush the pipe to allow the client to read the pipe's contents
// before disconnecting. Then disconnect the pipe, and close the
// handle to this pipe instance.
FlushFileBuffers(hPipe);
DisconnectNamedPipe(hPipe);
CloseHandle(hPipe);
EnterCriticalSection(&cs_Screen);
cout<<"\n ****** InstanceThread "
<<nThread<<" exit."<<endl<<endl;
LeaveCriticalSection(&cs_Screen);
return 1;
}
DWORD WorkThread(LPVOID lpPara)
{
int nClint=(int)lpPara;
char name[20]="PipeClient";
for (int i=1; i<=nClint; i++) // create pipe client
{
createProcess(name,i);
}
return 1;
}
HANDLE createProcess(char *name,int num)
{
static int position=1;
char cmdline[256];
char ich[8];
itoa(num,ich,10); // number of process to string
strcpy(cmdline,name);
strcat(cmdline," ");
strcat(cmdline,ich); // command-line
STARTUPINFO si={sizeof(STARTUPINFO)};
si.dwFlags=STARTF_USEPOSITION| // enable dwX and dwY
STARTF_USESIZE| // enable dwXSize and dwYSize
STARTF_USEFILLATTRIBUTE; // enable dwFillAttribute
si.dwXSize=365; // width, in pixels, of the window
si.dwYSize=380; // height, in pixels, of the window
si.lpTitle=cmdline; // title of window
// produces blue text on a whilte background
si.dwFillAttribute=FOREGROUND_BLUE|BACKGROUND_RED| BACKGROUND_GREEN| BACKGROUND_BLUE;
PROCESS_INFORMATION pi;
DWORD dwx=0,dwy=0; // the upper left corner of a window
switch (position)
{
case 0 : si.dwX=dwx+770; // the upper left corner of a window 0
si.dwY=dwy;
break;
case 1 : si.dwX=dwx; // the upper left corner of a window 1
si.dwY=dwy+400;
break;
case 2 : si.dwX=dwx+385; // the upper left corner of a window 2
si.dwY=dwy+400;
break;
case 3 : si.dwX=dwx+770; // the upper left corner of a window 3
si.dwY=dwy+400;
break;
}
BOOL fsuccess=CreateProcess(
NULL, // no executable module
cmdline,// command line string
NULL, // the returned handle cannot be inherited by child processes
NULL, // the returned handle cannot be inherited by child processes
TRUE, // each inheritable open handle in the calling
// process is inherited by the new process
NORMAL_PRIORITY_CLASS|CREATE_NEW_CONSOLE,
// The new process has a new console
NULL, // new process uses the environment of the calling process
NULL, // new process is created with the same current drive
// and directory as the calling process
&si, // Pointer to a STARTUPINFO structure that specifies how
// the main window for the new process should appear
&pi // Pointer to a PROCESS_INFORMATION structure that receives
// identification information about the new process
);
if (!fsuccess)
MyErrExit("CreateProcess");
position=(position+1)%4;
return pi.hThread;
}
VOID MyErrExit(char *Err)
{
EnterCriticalSection(&cs_Screen);
cout<<"Error : "<<Err<<endl;
LeaveCriticalSection(&cs_Screen);
exit(0);
}
2.Client.cpp文件
#include <iostream.h>
#include <windows.h> //Sleep()
#include <stdio.h>
#include <string.h>
#define BUFFSIZE 128
void MyErrExit(char *Err)
{
cout<<"Error : "<<Err<<endl;
flushall(); //清除缓冲区
getchar(); //等待按键
exit(0);
}
char Request[][128]={"Hello, I am pipe client",
"My name is Tom. What is your name?",
"Can you tell me something about pipe?",
"Thank you, Peter. Goodbye."};
DWORD main(int argc, char *argv[])
{
if (argc<2)
MyErrExit("CommandLine");
HANDLE hPipe;
LPVOID lpvMessage;
CHAR chBuf[BUFFSIZE],chwBuf[BUFFSIZE];
char name[20+BUFFSIZE]="Client ";
BOOL fSuccess;
DWORD cbRead, cbWritten, dwMode;
DWORD Messagesize;
int xx=0;
LPTSTR lpszPipename = "\\\\.\\pipe\\mynamedpipe";
char mutexName[20]="smutex";
strcat(mutexName,argv[1]);
strcat(name,argv[1]);
strcat(name," : ");
// Try to open a named pipe; wait for it, if necessary.
while (1)
{
hPipe = CreateFile(
lpszPipename, // pipe name
GENERIC_READ | // read and write access
GENERIC_WRITE,
0, // no sharing
NULL, // no security attributes
OPEN_EXISTING, // opens existing pipe
0, // default attributes
NULL); // no template file
// Break if the pipe handle is valid.
if (hPipe != INVALID_HANDLE_VALUE)
break;
// Exit if an error other than ERROR_PIPE_BUSY occurs.
if (GetLastError() != ERROR_PIPE_BUSY)
MyErrExit("1 Could not open pipe");
// All pipe instances are busy, so wait for 20 seconds.
if (! WaitNamedPipe(lpszPipename, 20000) )
MyErrExit("2 Could not open pipe");
}
// The pipe connected; change to message-read mode.
dwMode = PIPE_READMODE_MESSAGE;
fSuccess = SetNamedPipeHandleState(
hPipe, // pipe handle
&dwMode, // new pipe mode
NULL, // don't set maximum bytes
NULL); // don't set maximum time
if (!fSuccess)
MyErrExit("SetNamedPipeHandleState");
// Send a message to the pipe server.
strcpy(chwBuf,name);
strcat(chwBuf,Request[xx]);
lpvMessage = chwBuf;
Messagesize=strlen(chwBuf)+1;
fSuccess = WriteFile(
hPipe, // pipe handle
lpvMessage, // message
Messagesize, // message length
&cbWritten, // bytes written
NULL); // not overlapped
if (! fSuccess)
MyErrExit("WriteFile");
do
{
fSuccess = ReadFile(
hPipe, // pipe handle
chBuf, // buffer to receive reply
BUFFSIZE, // size of buffer
&cbRead, // number of bytes read
NULL); // not overlapped
if (! fSuccess && GetLastError() != ERROR_MORE_DATA)
MyErrExit("ReadFile");
cout<<"Server : "<<chBuf<<endl;
xx++;
if (xx>3)
break;
strcpy(chwBuf,name);
strcat(chwBuf,Request[xx]);
lpvMessage = chwBuf;
Messagesize=strlen(chwBuf)+1;
fSuccess = WriteFile(
hPipe, // pipe handle
lpvMessage, // message
Messagesize, // message length
&cbWritten, // bytes written
NULL); // not overlapped
if (! fSuccess)
MyErrExit("WriteFile");
Sleep(1000);//等待1000ms,为了便于观察并发执行的情况
} while (fSuccess);//while (! fSuccess); // repeat loop if ERROR_MORE_DATA
CloseHandle(hPipe);
cout<<"\n ******** pipe client"<<argv[1]<<" will exit ********"<<endl;
cout<<"\n Press Enter key to exit."<<endl;
flushall(); //清除缓冲区
getchar(); //等待按键
return 1;
}
4.4.2 程序运行结果
程序运行示例有4个窗口,其中有一个服务器显示窗口,如图4-1所示。因运行实例创建了3个管道客户进程,因此有3个客户输出显示窗口,如图4-2所示(考虑到图形的篇幅,故只截取了2个窗口)。通信内容演示了最简单的交谈过程。
图4-1 Pipe-Server程序运行结果样例
图4-2 Pipe-Client程序运行输出文件样例
上机实验五 进程调度 (4学时)
5.1 上机实验基本要求 (2学时)
要求实现一个进程调度程序,通过该程序可以完成进程的创建、撤销、查看和调度。具体要求如下:
(1) 实现进程调度程序scheduleProcess,负责整个系统的运行。
这是一个不停循环运行的进(线)程,其任务是及时响应进程的创建、撤销及状态查看请求,要采用适当的进程调度策略调度进程的运行。
(2) 实现创建进程的命令。
格式:create <name> <time>
参数说明
name:进程名
time:该进程计划运行时间
用户通过本命令发送创建进程请求,将进程信息提交给系统。系统创建进程,为其分配一个唯一的进程标识PID,并将状态置为READY,然后放入就绪队列中。
(3) 实现撤销进程命令
格式:remove <name>
参数说明
name:待撤销进程的名字。
输入撤销命令以后,系统就会删除待撤销的进程在缓冲区中的内容,如果输入有误,程序会给出出错提示。
(4) 实现查看进程状态的命令
格式: current
该命令打印(显示)出当前运行进程和就绪队列中各进程的信息,状态信息应包括:
l 进程的PID
l 进程名字
l 进程状态(RUN、READY、WAIT)
(5) 实现时间片轮转调度算法
处理机总是优先调度运行就绪队列中的第一个进程,当时间片结束后就把该进程放在就绪队列的尾部。在系统的实现以及运行中,不必考虑Windows操作系统本身的进程调度。假设所有的作业均由scheduleProcess调度执行,而且进程在分配给它的时间片内总是不间断地运行。
5.2 相关知识
5.2.1 Windows中的进程和线程
在Windows 2000/XP中,每个进程至少包含一个线程,进程均由一个线程启动,该线程称为主线程,例如,我们用VC++语言编写了一个程序,编译运行后就是一个进程,该进程的主线程运行main函数的代码。进程(主线程)可以动态创建新的线程(子线程)。进程和线程均可以通过Win32 API函数CreateProcess和CreateThread动态创建(详见上机实验二和上机实验三)。
线程通常在用户态下运行,当它进行系统调用时,会切换到核心态运行,并继续作为同一线程并具有用户态下相同的属性和限制。当一个线程执行完毕时,它可以退出。当进程的最后一个线程退出时,该进程终止。
线程是一个调度的概念而不是一个占有资源的概念。任何一个线程可以访问它所属进程的所有对象(资源),它所要做的只是获得对象句柄并做适当的Win32调用。
由于Windows 2000/XP中,调度的对象是线程而不是进程,所以每个线程具有一个状态(就绪、运行、阻塞等),但是进程没有这些状态。线程构成了CPU调度的基础,因为操作系统会选择一个线程运行而不是一个进程。因此,Win32 API中有关于线程操作的相关函数,包括:线程的暂停(挂起)、线程的回复(唤醒)、线程的终止等等,利用这些操作,我们可以较方便地模拟进程调度的过程。设计本上机实验的最主要思路,就是用线程模拟进程。然后对进程(实际上是对线程)进行调度演示的。
5.2.2 相关线程的几个Win32 API函数介绍
在Windows 2000/XP系统中,要进行进程/线程的创建、撤销和调度等操作,都需要通过Win32 API调用来完成。涉及的API如下:
1.CreateThread( )函数
该函数的介绍参见2.2.2节。
2.SuspendThread( )函数
功能:
挂起(暂停)线程。被挂起的线程将暂停执行,直到被回复(唤醒)为止。
格式:DWORD SuspendThread ( HANDLE hthread )
参数说明:hthread:线程的句柄。该句柄在调用CreateThread时获得。
3.ResumeThread( )函数
功能:
恢复(唤醒)线程。被挂起的线程恢复后才能为其分配CPU。该函数正确执行则返回一正整数,否则返回0xFFFFFFFF(即-1)。
格式:DWORD ResumeThread ( HANDLE hthread )
参数说明:hthread:线程的句柄。
4.TerminateThread( )函数
功能:终止线程运行。
格式:BOOL TerminateThread ( HANDLE hthread, DWORD dwExitCode )
参数说明
hthread:线程的句柄。
dwExitCode:线程终止时的退出代码(返回值)。
【注】TerminateThread终止线程时,该线程的初始堆栈未被回收,依附于该线程的DDLs也未被告知线程已经结束。TerminateThread是一个危险的函数,该函数仅仅在最极端的情况下使用。只有当你确切地知道目标线程正在工作以及在终止时能控制该线程所有可能正在运行的代码时才能够使用此函数。例如,TerminateThread可能导致如下问题:
· 如果目标线程拥有一个临界区,该临界区将不被释放。
· 如果目标线程被终止时正在执行某些kernel32调用,则将导致该线程进程的kernel32状态的不一致性。
· 如果目标线程正在操纵共享的DDL的全局状态,则DDL的状态将被破坏从而影响使用DDL的其他用户。
5.Sleep函数
该函数的介绍参见2.2.2节。
6.CloseHandle函数
功能:关闭内核对象(线程)的句柄,将对象引用计数减1,或者释放堆栈资源。
格式:BOOL CloseHandle (HANDLE hobj )
参数说明:hobj:对象的句柄。
多个线程操作相同的数据时,一般是需要按顺序访问的(互斥访问),否则会引起数据错乱,使其无法控制数据。为了解决这个问题,需引入互斥变量,让这些线程都按顺序地访问变量(程序中是指互斥访问链队列这类临界资源)。这样就需要使用如下三个函数。
7.InitializeCriticalSection( ) 函数
该函数的介绍参见2.2.2节。
8.EnterCriticalSection( ) 函数
该函数的介绍参见2.2.2节。
9.LeaveCriticalSection ( ) 函数
该函数的介绍参见2.2.2节。
5.3 实验设计
本实验是在Windows 2000/XP+VC++6.0环境下实现的,利用Windows SDK提供的应用程序接口(API)完成程序的功能。实验中所使用的API是操作系统提供的用来进行应用程序设计的系统功能接口。要使用这些API,需要包含对这些函数进行说明的SDK头文件,最常见的是windows.h。一些特殊的API调用还需要包含其他的头文件。
5.3.1 重要的数据结构
1.进程控制块
typedef struct PCB // 进程控制块
{
int id; // 进程标识PID
char name[20]; // 进程名
enum STATUS status; // 进程状态:RUN, READY, WAIT
int flag; //为了不重复显示,额外增加此成员
HANDLE hThis; // 进程句柄(实际上是线程句柄)
DWORD threadID; // 线程ID
int count; // 进程计划运行时间长度,以时间片为单位
struct PCB *next; // 指向就绪队列或缓冲区(空闲PCB)链的指针
} PCB,*pPCB;
该数据结构定义了用于进程控制的若干属性,包括PID(进程标识号)、进程名字、进程状态(有三种:RUN、READY、WAIT)、操作进程的句柄和进程剩余时间长度等。若采用优先级调度,还需要有优先数等。
2.用于管理进程就绪队列和空闲PCB队列的结构
为了操作方便,在程序中还定义了一个数据结构用于操作进程队列。如下:
typedef struct // 就绪队列和空闲PCB队列管理用的结构
{
pPCB head; // 队首指针
pPCB tail; // 队尾指针(基本的时间片轮转算法使用)
int pcbNum; // 队列中的进程数
} readyList, freeList, *pList;
5.3.2 程序实现
1.主函数
在主函数main( )中,首先打开用于记录信息的文件(请思考:为什么要使用记录信息的文件?),并调用init( )函数初始化各数据结构并启动进程调度线程,然后在一个无限循环中接收用户输入的命令:创建进程(create)、撤销进程(remove)或查看信息(current)。并调用相应的函数进行响应。其函数流程如图5-1所示。为了更清楚观察进程调度过程,在进入无限循环前,预先自动创建了6个进程。
2.初始化环境函数
在程序中,设置了一个可调度进程数的上限PCB_LIMIT。在初始化环境函数init( )中为每个可用的PCB结构分配了空间,称为缓存区freeList。每当新创建一个进程,则从缓存区中取一个PCB结构放入就绪队列readyList中。当一个进程结束时,则需把该PCB内容清空并放回缓存区。
3.创建线程用于模拟进程
为了能使程序中的进程调度程序scheduleProcess能够调度进程,在程序中使用线程来模拟用户进程,调度程序也是由线程模拟。这里以用户进程的模拟线程为例说明线程函数的编写格式和线程的创建方法。
模拟用户进程的线程的工作代码(线程函数)如下:
DWORD WINAPI processThread(LPVOID lpParameter)
{
pPCB currentPcb=(pPCB)lpParameter;
while (true)
{
if (currentPcb->flag==1)//若调度后第一次运行,则显示信息
{
currentPcb->flag=0;
EnterCriticalSection(&cs_SaveInfo);
//“进程正在运行”信息保存到文件
log<<"Process "<<currentPcb->id<<':'<<currentPcb->name
<<" is running............"<<endl;
LeaveCriticalSection(&cs_SaveInfo);//离开临界区
}
Sleep(900); // 等待900ms
}
return 1;
}
这个线程只有一个死循环,除了每次被调度(实验中用唤醒模拟调度,真正的调度实际上是由操作系统完成)后输出一个“正在运行”的信息外,什么都不做,直到线程被终止。DWORD WINAPI表明这是一个线程入口函数,LPVOID lpParameter是线程函数的参数表。线程函数必须有一个DWORD(即unsigned long)类型的返回值,作为该线程的退出标识(因此线程的返回值是一个无符号整数)。
创建该线程(用于模拟进程)的代码如下:
void createProcess(char *name,int count)
{
……
newPcb->hThis = CreateThread (NULL, 0, processThread,newPcb,
CREATE_SUSPENDED, &(newPcb->threadID ) );
……
}
这段代码用CreateThread ( )函数创建了一个线程,它对应的线程工作代码为processThread函数,即上面定义的模拟用户进程的线程函数,线程的参数为一个指向PCB结构的指针newPcb,线程的初始状态为暂停(CREATE_SUSPENDED创建的线程不利己运行,而是先挂起),该线程的线程号也记录到newPcb中。关于CreateThread ( )函数各参数的含义可参见前面的API介绍。
4.进程调度函数
进程调度函数scheduleProcess ( )是本实验中的重点。调度要做的主要工作是暂停当前运行的进程,如果运行时间未用完,则将其挂起停止运行(使用SuspendThread函数),并将其PCB插入就绪队列,同时从就绪队列中选择一个进程(就绪队列队首进程)让其运行(使用ResumeThread函数让其从挂起状态恢复),同时更新进程状态。
在每次调度完新进程后,还要利用Sleep()函数使调度程序睡眠一段时间。睡眠时间就是时间片的大小,从而表示消耗了一个时间片(本实验的时间片是1000ms,即1s)。
参考程序中采用的是轮转调度算法。当一个进程运行时间片到期时,要调用SuspendThread()函数暂停进程运行,并判断此进程是否运行完毕。如果进程运行时间结束,就调用TerminateThread()函数终止该进程,并且回收PCB空间;如果该进程没有结束,就把它放到就绪队列尾部,最后取出就绪队列队首的进程,调用resumeThread()函数恢复其运行。程序的流程图如图5-2所示。
5.命令处理函数
命令处理函数分别处理create、remove、current三种命令。该函数比较简单,具体过程如图5-3所示。
5.4 源程序与运行结果
5.4.1 程序源代码
1.schedule.h头文件
#include <string.h>
#include <windows.h>
#include <iostream.h>
#include <fstream.h>
#ifndef SCHEDULE_H
#define SCHEDULE_H
typedef struct PCB // 进程控制块
{
int id; // 进程标识PID
char name[20]; // 进程名
enum STATUS status; // 进程状态:RUN, READY, WAIT
int flag; //为了不重复显示,额外增加此成员
HANDLE hThis; // 进程句柄
DWORD threadID; // 线程ID
int count; // 进程计划运行时间长度,以时间片为单位
struct PCB *next; // 指向就绪队列或缓冲区(空闲PCB)链的指针
} PCB,*pPCB;
// 就绪队列或空白PCB队列的管理用的结构
typedef struct
{
pPCB head; // 队首指针
pPCB tail; // 队尾指针
int pcbNum; // 队列中的进程数
} readyList, freeList, *pList;
enum STATUS {RUN,READY,WAIT}; // 进程的三种状态列表
// 进程控制块数为30(最多允许创建30个进程)
const int PCB_LIMIT=30;
HANDLE init();
void createProcess(char *name,int ax);
void scheduleProcess();
void removeProcess(char *name);
void fprintReadyList();
void printReadyList();
void printCurrent();
void stopAllThreads();
#endif
2.schedule.cpp文件
#include "schedule.h"
readyList readylist; //定义就绪队列的管理结构
pList pReadyList=&readylist; //定义该结构的指针
freeList freelist; //定义空闲PCB队列的管理结构
pList pFreeList=&freelist; //定义该结构的指针
PCB pcb[PCB_LIMIT]; //定义30个PCB
pPCB runPCB; //指向当前运行的进程PCB的指针
int pid=0; //进程id(创建进程用)
//临界区对象
CRITICAL_SECTION cs_ReadyList; //用于互斥访问就绪队列
CRITICAL_SECTION cs_SaveInfo; //用于互斥访问保存信息的文件
//输出文件
extern ofstream log;
extern volatile bool exiting; //当exiting=true时程序结束
//初始化进程控制块
void initialPCB(pPCB p)
{
p->id=0; //进程id
strcpy(p->name,"NoName");//进程名字
p->status=WAIT; //进程状态
p->next=NULL; //PCB的next指针
p->hThis=NULL; //进程句柄
p->threadID=0; //线程id
p->flag=1; //开始时允许线程显示信息
p->count=0; //进程计划运行时间
}
//从空白PCB队列取一空闲进程控制块
pPCB getPcbFromFreeList()
{
pPCB freePCB=NULL;
if (pFreeList->head != NULL && pFreeList->pcbNum>0)
{
freePCB=pFreeList->head;
pFreeList->head=pFreeList->head->next;
pFreeList->pcbNum--;
}
return freePCB;
}
//释放PCB使之插入空闲PCB队列
void returnPcbToFreeList(pPCB p)
{
if (pFreeList->head==NULL) //若当前空闲PCB队列为空
{
pFreeList->head=p;
pFreeList->tail=p;
p->next=NULL;
pFreeList->pcbNum++;
}
else //若空白PCB队列不空,则将释放的PCB插入队首
{
p->next=pFreeList->head;
pFreeList->head=p;
pFreeList->pcbNum++;
}
}
// 模拟用户进程的线程之执行代码
DWORD WINAPI processThread(LPVOID lpParameter)
{
pPCB currentPcb=(pPCB)lpParameter;
while (true)
{
if (currentPcb->flag==1)//若调度后第一次运行,则显示信息
{
currentPcb->flag=0;
EnterCriticalSection(&cs_SaveInfo);
//“进程正在运行”信息保存到文件
log<<"Process "<<currentPcb->id<<':'<<currentPcb->name
<<" is running............"<<endl;
LeaveCriticalSection(&cs_SaveInfo);//离开临界区
}
Sleep(800); // 等待800ms
}
return 1;
}
// 调度线程的执行代码
DWORD WINAPI scheduleThread(LPVOID lpParameter)
{
// pList preadyList=(pList)lpParameter;//实际上此参数无作用
//循环调用进程调度函数,直到exiting=true为止
while (!exiting)//若exiting=false,则循环执行调度程序
{
scheduleProcess();
}
stopAllThreads();//若exiting=true,则结束所有进程(线程)
return 1;
}
// 初始化操作
HANDLE init()//函数正确执行后,返回调度线程的句柄
{
pPCB p=pcb; //指向第一个PCB
//就绪队列初始化为空(初始化其管理结构)
pReadyList->head=NULL;
pReadyList->tail=NULL;
pReadyList->pcbNum=0;
//空闲队列初始化为空(初始化其管理结构)
pFreeList->head=&pcb[0];
pFreeList->tail=&pcb[PCB_LIMIT-1];
pFreeList->pcbNum=PCB_LIMIT;
//构成空闲PCB队列
for (int i=0;i<PCB_LIMIT-1;i++)//PCB_LIMIT已在头文件中定义为30
{
initialPCB(p);
p->next=&pcb[i+1];
p++;
}
initialPCB(p);
pcb[PCB_LIMIT-1].next=NULL;
InitializeCriticalSection(&cs_ReadyList); //初始化临界区对象
InitializeCriticalSection(&cs_SaveInfo);
exiting=false; //使调度程序不断循环
// 创建调度程序的监控线程
HANDLE hSchedule; //程序调度线程的句柄
hSchedule=CreateThread(
NULL, //返回的句柄不能被子线程继承
0, //新线程堆栈大小与主线程相同
scheduleThread, //新线程执行此参数指定的函数的代码
pReadyList, //传递给函数scheduleThread的参数
//(本程序中此参数实际上没有用处)
0, //新线程的初始状态为运行状态
NULL //对新创建线程的id不感兴趣
);
//预先创建6个进程
char pName[6]="p00";
for (i=0;i<6;i++)
{
pName[2]='0'+i;
createProcess(pName,10);//addApplyProcess(pName,10);
}
return hSchedule;
}
// 创建进程(此函数并非API函数CreateProcess)
void createProcess(char *name,int count)
{
EnterCriticalSection(&cs_ReadyList); //准备进入临界区
if (pFreeList->pcbNum>0) // 若有用于创建进程的空白PCB
{
pPCB newPcb=getPcbFromFreeList();// 从空白PCB队列获取一个空白PCB
newPcb->status=READY; // 新进程状态为“READY”
strcpy(newPcb->name,name); // 填写新进程的名字
newPcb->count=count; // 填写新进程的运行时间
newPcb->id=pid++; // 进程id
newPcb->next=NULL; // 填写新PCB的链接指针
//若就绪队列空,则新PCB为ReadyList的第一个结点
if (pReadyList->pcbNum==0)
{
pReadyList->head=newPcb;
pReadyList->tail=newPcb;
pReadyList->pcbNum++;
}
else//否则,就绪队列不空,新PCB插入就绪队列尾部
{
pReadyList->tail->next=newPcb;
pReadyList->tail=newPcb;
pReadyList->pcbNum++;
}
cout<<"New Process Created, Process ID:"
<<newPcb->id<<", Process Name:"
<<newPcb->name<<", Process Length:"<<newPcb->count<<endl;
cout<<"Current ReadyList is:"<<endl;
printReadyList();
//向信息文件输出相关信息以便查看程序执行过程
EnterCriticalSection(&cs_SaveInfo);
log<<"New Process Created, Process ID:"
<<newPcb->id<<", Process Name:"
<<newPcb->name<<", Process Length:"<<newPcb->count<<endl;
log<<"Current ReadyList is:"<<endl;
fprintReadyList();
LeaveCriticalSection(&cs_SaveInfo);
//创建用户线程,初始状态为暂停
newPcb->hThis=CreateThread(NULL,0,processThread,
newPcb,CREATE_SUSPENDED,&(newPcb->threadID));
}
else //空闲PCB用完
{
cout<<"New process intend to append. But PCB has been used out!"<<endl;
EnterCriticalSection(&cs_SaveInfo);
log<<"New process intend to append. But PCB has been used out!"<<endl;
LeaveCriticalSection(&cs_SaveInfo);
}
LeaveCriticalSection(&cs_ReadyList); // 退出临界区
}
// 进程调度
void scheduleProcess()
{
EnterCriticalSection(&cs_ReadyList);
if (pReadyList->pcbNum>0) // 就绪队列中有进程则调度
{
runPCB=pReadyList->head; // 调度程序选择就绪队列中第一个进程
pReadyList->head=pReadyList->head->next; //修改就绪队列的头指针
if (pReadyList->head==NULL) // 若就绪队列已空,则需修改其尾指针
pReadyList->tail=NULL;
pReadyList->pcbNum--; // 就绪队列节点数减1
runPCB->count--; // 新进程时间片数减1
runPCB->flag=1; //进程每次被调度,只显示1次
EnterCriticalSection(&cs_SaveInfo);
log<<"Process "<<runPCB->id<<':'<<runPCB->name<<" is to be scheduleed."<<endl;
LeaveCriticalSection(&cs_SaveInfo);
ResumeThread(runPCB->hThis);// 恢复线程(进程),在本程序中实际上是启动线程运行
runPCB->status=RUN; // 进程状态设置为“RUN”
// 时间片为1s
Sleep(1000); // 等待1秒钟,用此模拟(时间片为1s的)定时中断
EnterCriticalSection(&cs_SaveInfo);
log<<"\nOne time slot used out!\n"<<endl;
LeaveCriticalSection(&cs_SaveInfo);
runPCB->status=READY;
SuspendThread(runPCB->hThis); // 当前运行进程被挂起
// 判断进程是否运行完毕
if (runPCB != NULL && runPCB->count <= 0)
{
cout<<"\n****** Process "<<runPCB->id<<':'<<runPCB->name
<<" has finished. ******"<<endl;
cout<<"Current ReadyList is:"<<endl;
printReadyList();
cout<<"COMMAND>";cout<<flush;
EnterCriticalSection(&cs_SaveInfo);
log<<"****** Process "<<runPCB->id<<':'<<runPCB->name
<<" has finished. ******\n"<<endl;
log<<"Current ReadyList is:"<<endl;
fprintReadyList();
log<<flush;
LeaveCriticalSection(&cs_SaveInfo);
// 终止进程(线程)
if (!TerminateThread(runPCB->hThis,1))
{ //若终止线程失败,则给出出错信息,结束整个程序
EnterCriticalSection(&cs_SaveInfo);
log<<"Terminate thread failed! System will abort!"<<endl;
LeaveCriticalSection(&cs_SaveInfo);
exiting=true; //结束程序
}
else //终止继承操作正确执行
{
CloseHandle(runPCB->hThis);
//终止进程的PCB释放到空白PCB链队列中
returnPcbToFreeList(runPCB);
runPCB=NULL;
}
}
else if (runPCB != NULL) //进程未运行完毕,则将其插入就绪队列
{
if (pReadyList->pcbNum <=0)//就绪队列为空时的处理
{
pReadyList->head=runPCB;
pReadyList->tail=runPCB;
}
else//就绪队列为不空时将原运行进程的PCB接到就绪队列尾部
{
pReadyList->tail->next=runPCB;
pReadyList->tail=runPCB;
}
runPCB->next=NULL;
runPCB=NULL;
pReadyList->pcbNum++; //就绪队列进程数增1
}
}
else if (pReadyList != NULL) // 清空就绪队列
{
pReadyList->head=NULL;
pReadyList->tail=NULL;
pReadyList->pcbNum=0;
}
LeaveCriticalSection(&cs_ReadyList);
}
// 撤销进程
void removeProcess(char *name)
{
pPCB removeTarget = NULL;
pPCB preTemp = NULL;
EnterCriticalSection(&cs_ReadyList); //互斥访问就绪队列
// 若撤销的是当前运行进程
if (runPCB != NULL && strcmp(name,runPCB->name)==0 )
{
removeTarget=runPCB;
if (!(TerminateThread(removeTarget->hThis,1)))
{
cout<<"Terminate thread failed! System will abort!"<<endl;
EnterCriticalSection(&cs_SaveInfo);
log<<"Terminate thread failed! System will abort!"<<endl;
LeaveCriticalSection(&cs_SaveInfo);
exit(0); //结束程序
}
else // 撤销操作成功时
{
CloseHandle(removeTarget->hThis); //关闭进程句柄
returnPcbToFreeList(removeTarget); //该进程的PCB插入空闲PCB队列
runPCB=NULL;
//显示进程已撤销的信息
cout<<"\nProcess "<<removeTarget->id
<<':'<<removeTarget->name<<" has been removed."<<endl;
cout<<"Current ReadyList is:\n";
printReadyList();
//同时将进程已撤销的信息保存到文件
EnterCriticalSection(&cs_SaveInfo);
log<<"\nProcess "<<removeTarget->id
<<':'<<removeTarget->name<<" has been removed."<<endl;
log<<"Current ReadyList is:\n";
fprintReadyList();
log<<flush;
LeaveCriticalSection(&cs_SaveInfo);
LeaveCriticalSection(&cs_ReadyList);
return;
}
}
// 否则,在就绪队列中寻找要撤销的进程
if (pReadyList->head != NULL)
{
removeTarget=pReadyList->head;
while (removeTarget!=NULL)
{
if (strcmp(name,removeTarget->name)==0)//找到要撤销的进程
{
if (removeTarget==pReadyList->head)//是就绪队列中的第一个进程
{
pReadyList->head=pReadyList->head->next;
if (pReadyList->head==NULL)
pReadyList->tail=NULL;
}
else // 找到的不是就绪队列中第一个进程
{
preTemp->next=removeTarget->next;
if (removeTarget==pReadyList->tail)
pReadyList->tail=preTemp;
}
if (!TerminateThread(removeTarget->hThis,0))//执行撤销进程的操作
{ //撤销操作失败时的输出信息
cout<<"Terminate thread failed! System will abort!"<<endl;
EnterCriticalSection(&cs_SaveInfo);
log<<"Terminate thread failed! System will abort!"<<endl;
LeaveCriticalSection(&cs_SaveInfo);
LeaveCriticalSection(&cs_ReadyList);
exit(0); //结束程序
}
//撤销操作成功后的处理
CloseHandle(removeTarget->hThis);
returnPcbToFreeList(removeTarget);
pReadyList->pcbNum--;
cout<<"Process "<<removeTarget->id
<<':'<<removeTarget->name<<" has been removed."<<endl;
cout<<"currentreadyList is:"<<endl;
printReadyList();
EnterCriticalSection(&cs_SaveInfo);
log<<"Process "<<removeTarget->id
<<':'<<removeTarget->name<<" has been removed."<<endl;
log<<"currentreadyList is:"<<endl;
fprintReadyList();
log<<flush;
LeaveCriticalSection(&cs_SaveInfo);
LeaveCriticalSection(&cs_ReadyList);
return;
}
else //未找到,继续找
{
preTemp=removeTarget;
removeTarget=removeTarget->next;
}
}
}
LeaveCriticalSection(&cs_ReadyList);
cout<<"Sorry, there's no process named "<<name<<endl;
return;
}
// 向文件中打印就绪队列信息
void fprintReadyList()
{
pPCB tmp=NULL;
tmp=pReadyList->head;
if (tmp != NULL)
for (int i=0;i<pReadyList->pcbNum;i++)
{
log<<"--"<<tmp->id<<':'<<tmp->name<<"--";
tmp=tmp->next;
}
else
log<<"NULL";
log<<endl<<endl;
}
// 向标准输出打印就绪队列信息
void printReadyList()
{
pPCB tmp=NULL;
tmp=pReadyList->head;
if (tmp != NULL)
for (int i=0;i<pReadyList->pcbNum;i++)
{
cout<<"--"<<tmp->id<<':'<<tmp->name<<"--";
tmp=tmp->next;
}
else
cout<<"NULL";
cout<<endl;
}
// 打印当前运行进程信息
void printCurrent()
{
if (runPCB != NULL)
cout<<"Process "<<runPCB->name<<" is running..."<<endl;
else
cout<<"No process is running."<<endl;
cout<<"Current readyList is:"<<endl;
printReadyList();
}
// 结束所有子线程
void stopAllThreads()
{
if (runPCB != NULL)
{
TerminateThread(runPCB->hThis,0);
CloseHandle(runPCB->hThis);
}
// 结束所有就绪队列中的线程
pPCB q,p=pReadyList->head;
while (p != NULL)
{
if (!TerminateThread(p->hThis,0))
{
cout<<"Terminate thread failed! System will abort!"<<endl;
exit(0); //结束程序
}
CloseHandle(p->hThis);
q=p->next;
returnPcbToFreeList(p);
p=q;
}
}
3.main.cpp文件
#include "schedule.h"
ofstream log; //保存进程调度信息的文件
volatile bool exiting; //是否退出程序
void helpInfo()
{
cout<<"************************************************\n";
cout<<"COMMAND LIST:\n";
cout<<"create process_name process_length (create p0 8)\n";
cout<<"\t append a process to the process list\n";
cout<<"remove process_name (remove p0)\n";
cout<<"\t remove a process from the process list\n";
cout<<"current\t show current runprocess readyList\n";
cout<<"exit\t exit this simulation\n";
cout<<"help\t get command imformation\n";
cout<<"************************************************\n\n";
}
int main()
{
char name[20]={'\0'};
HANDLE hSchedule; //调度线程的句柄
log.open("Process_log.txt");
helpInfo();
hSchedule=init(); //hSchedule是调度程序的句柄
if (hSchedule==NULL)
{
cout<<"\nCreate schedule-process failed. System will abort!"<<endl;
exiting=true;
}
char command[30]={0};
while (!exiting)
{
cout<<"COMMAND>";
cin>>command;
if (strcmp(command,"exit")==0)
break;
else if (strcmp(command,"create")==0)
{
char name[20]={'\0'};
int time=0;
cin>>name>>time;
createProcess(name,time);
}
else if (strcmp(command,"remove")==0)
{
cin>>name;
removeProcess(name);
}
else if (strcmp(command,"current")==0)
printCurrent();
else if (strcmp(command,"help")==0)
helpInfo();
else
cout<<"Enter help to get command information!\n";
}
exiting=true;
if (hSchedule!=NULL)
{ //无限等待,直到Schedule进程(线程)终止为止
WaitForSingleObject(hSchedule,INFINITE);
CloseHandle(hSchedule);
}
log.close();
cout<<"\n******* End *******\n"<<endl;
return 0;
}
5.4.2 程序运行结果
程序运行结果如图5-4所示。程序运行输出文件Process_log.txt的内容如图5-5所示。为了观察分析进程的调度过程,打开文件Process_log.txt,可以看到进程调度的整个过程。当存在多个进程时,其调度的确时按时间片轮转方式进行的。
图5-4 程序运行结果样例
图5-5 程序运行输出文件样例
5.5 进一步要求 (2学时)
以下两个上机实验内容是选做内容,有兴趣的同学可以任选一项或两项来完成。
1.时间片轮转加动态优先级调度算法
本实验程序中实现的是基本时间片轮转调度策略,我们可以对该策略进行改进,实现时间片轮转基础上的动态优先级调度策略(属于抢占式优先级调度)。每个进程有其动态优先级,在用完分配的时间片后,可以被优先级更高的进程抢占运行。就绪队列中的进程等待时间越长,其优先级越高。每个进程都具有以下两种优先级。(实现此算法的程序,请参看Schedule2)
■ 初始优先级:在进程创建时指定,其值将保持不变,直至进程结束。
■ 当前优先级:有scheduleProcess调度更新,用以调度进程运行。scheduleProcess总是选择当前优先级最高的进程来运行。
进程当前优先级的更新主要取决于以下两种情况:
· 一个进程在就绪队列中等待了若干时间片(例如5个),则将它的当前优先级加1。
· 若当前运行进程时间片到,则终止其运行(抢占式多任务),将其插入就绪队列中(注意:就绪队列是按优先级排序的,不再是简单地插入就绪队列尾部),它的当前优先级也恢复为初始优先级。
2.多级反馈队列调度算法
修改程序实现多级反馈队列调度算法。为简单统一起见,不妨假设就绪队列分三级(三个就绪队列)。第一个队列优先级最高,时间片长度分别为1s;第二个队列优先级次之,,时间片长度分别为2s;第三个队列优先级最低,时间片长度分别为3s。新创建的进程按FIFO插入第一个就绪队列尾部;进程运行完一个时间片后“掉入”下一级就绪队列中(插入下一级队列尾部),掉到第三极后不再下掉。
scheduleProcess调度程序总是首先选择第一级就绪队列的队首进程运行,只有当第一级就绪队列为空时,才选择第二级队列中的队首进程,第二级队列空时才调度第三极队列的进程。
【思考问题】本实验的也可采用如下总体设计方案:1.预先编写好一个(或几个)可执行程序,在本实验程序中利用预先准备的可执行程序创建若干个进程,并且把依次创建的进程有关信息存入PCB队列,这些信息中包括进程的主线程句柄(进程创建时,它被保存在PROCESS_INFORMATION结构中)。
2.利用进程主线程句柄,通过挂起、恢复、终止等操作,实现对主线程的“调度”。(实现此设计方案的程序,请参看Schedule3——也采用时间片轮转加动态优先级调度算法,且多窗口显示)
上机实验六 演示银行家算法 (2学时)
6.1 上机实验要求
1.设计并实现银行家算法的程序,以教科书的例题为例子,演示银行家算法。目的是加深对银行家算法的理解。
2.修改程序使之可以解决任意有关银行家算法的问题。例如解决“操作系统习题集2012版”第三章中的应用题第7题。
6.2 实验设计
6.2.1 数据结构
1.可利用资源向量Available
是一个含有m个元素的数组,其中每一个元素代表一类可用资源数目,m是资源种类数。
2.最大需求矩阵Max
是一个n×m矩阵,它定义了系统中n个进程中的每一个进程对m类资源的最大需求数。每一行对应一个进程;每一列对应一类资源。
3.分配矩阵Allocation
是一个n×m矩阵,它定义了系统中每一类资源当前已分配给每一个进程的资源数。
4.需求矩阵Need
是一个n×m矩阵,用于表示每个进程尚需的各类资源数。
6.2.2 程序实现
在Windows XP + VC++ 6.0环境下设计程序,采用面向对应程序设计(OOP)技术。
1.银行家算法函数Banker()
银行家算法函数是本实验设计的重要内容,其程序流程图如图6-1所示。
2.安全性算法函数
安全性算法函数流程图如图6-2所示。
6.3 源程序与运行结果
1.源程序代码
参考源程序如下:
#include <iostream.h>
class Banker {
int *Allocation; // 分配矩阵
int *Need; // 尚需矩阵
int *Available; // 可用资源向量
int *squeue; // 安全序列
int safe; // safe=1,存在安全序列
public:
int m,n; // m为资源种类数,n为进程数
Banker(int,int); // 构造函数
~Banker(); // 析构函数
bool Ifsafe(); // 安全性检查函数
int banker(int k,int *Request); // 银行家算法
void Print(); // 输出资源分配情况
void Getmn(int &,int &); // 获取资源种类数m和进程数n
};
Banker::Banker(int m0,int n0) // 构造函数
{
int i,j;
m=m0; n=n0; safe=0;
Allocation=new int[n*m];
Need=new int[n*m];
Available=new int[m];
squeue=new int[n];
cout<<"输入分配矩阵Allocation:\n";
for (i=0;i<n;i++)
{
cout<<"输入分配矩阵的第 "<<i+1<<" 行:";
for (j=0;j<m;j++)
cin>>Allocation[i*m+j];
}
cout<<"\n输入尚需矩阵Need:\n";
for (i=0;i<n;i++)
{
cout<<"输入尚需矩阵的第 "<<i+1<<" 行:";
for (j=0;j<m;j++)
cin>>Need[i*m+j];
}
cout<<"\n输入可用资源向量Available:";
for (i=0;i<m;i++)
cin>>Available[i];
}
Banker::~Banker()
{
delete []Allocation; // 释放分配矩阵空间
delete []Need; // 释放尚需矩阵空间
delete []Available; // 释放可用资源向量
delete []squeue; // 释放安全序列空间
}
// 判断系统状态是否安全。Finish:标志;Work:工作向量。
bool Banker::Ifsafe( )
{
int *Work=new int[m]; // 定义工作向量
int *Finish=new int[n]; // 定义标志数组
int i,j,flag,k=0;
for (i=0;i<n;i++)
Finish[i]=0; // 置未标记标志
for (j=0;j<m;j++)
Work[j]=Available[j];
do {
flag=0;
for (i=0;i<n;i++)
{
if (Finish[i]==0)
{
for (j=0;j<m;j++) // 判断Need[i]是否不超过Work
if (Need[i*m+j]>Work[j]) break;
if (j==m) { // 若Pi的需求Need[i]≤当前可用资源能Work
Finish[i]=1; // 置Pi能完成的标志
flag=1; // 为了继续循环(查找)
squeue[k++]=i; // 顺便记下安全序列
for (j=0;j<m;j++)//Pi完成后,它所占用的资源归入Work中
Work[j]+=Allocation[i*m+j];
}
}
}
} while (flag);
delete []Work;
for (i=0;i<n;i++)
if (Finish[i]==0) // 若有进程未完成,则系统状态不安全
{
delete []Finish;
return false; // 返回“不安全”
}
delete []Finish;
return true; // 返回“安全”
}
// 银行家算法。k:进程序号; Request:该进程的请求向量
int Banker::banker(int k,int *Request)
{
int i;
for (i=0;i<m;i++)
if (Request[i]>Need[k*m+i])
{
cout<<"\nError : Request of P"<<k<<" > Max["<<k<<"]\n";
return 0;
}
for (i=0;i<m;i++)
if (Request[i]>Available[i])
{
cout<<"\nRequest > Available,P"<<k<<" is blocked\n";
return 0;
}
for (i=0;i<m;i++) // 系统试探分配资源
{
Available[i]-=Request[i];
Allocation[k*m+i]+=Request[i];
Need[k*m+i]-=Request[i];
}
safe=Ifsafe(); // 判断是否安全
if (safe) // 若系统状态仍是安全的
{
cout<<"\n满足进程P"<<k<<"的请求(";
for(i=0;i<m-1;i++)
cout<<Request[i]<<',';
cout<<Request[i]<<")后,系统状态仍然是安全的。安全序列为:\n";
for (i=0;i<n-1;i++)
cout<<'P'<<squeue[i]<<',';
cout<<'P'<<squeue[i]<<endl;
}
else // 若系统状态已是不安全
{
cout<<"\n如果满足进程P"<<k<<"的资源请求(";
for(i=0;i<m-1;i++)
cout<<Request[i]<<',';
cout<<Request[i]<<"),系统将进入不安全状态,"<<endl;
cout<<"故系统将拒绝该进程的请求。"<<endl;
for (i=0;i<m;i++) // 将试探分配作废
{
Available[i]+=Request[i];
Allocation[k*m+i]-=Request[i];
Need[k*m+i]+=Request[i];
}
}
return safe;
}
void Banker::Print() // 输出当前系统的资源分配情况
{
int i,j;
cout<<"\n Allocation Need Available\n\n";
for (i=0;i<n;i++)
{
for (j=0;j<m;j++)
{
cout.width(3);
cout<<Allocation[i*m+j];
}
cout<<'\t';
for (j=0;j<m;j++)
{
cout.width(3);
cout<<Need[i*m+j];
}
if (i==0)
{
cout<<"\t";
for (j=0;j<m;j++)
{
cout.width(3);
cout<<Available[j];
}
}
cout<<endl;
}
cout<<endl;
}
void Banker::Getmn(int &x,int &y)
{
x=m;
y=n;
}
int main()
{
int i,stat;
int k,m,n; // m,n分别是资源种类数和进程数
Banker b(3,5); // 定义对象b,其资源种类数和进程数分别为3和5
b.Getmn(m,n);
int *Request=new int[m]; // 进程申请资源向量
b.Print(); // 显示当前资源分配情况
stat=b.Ifsafe(); // 判断T0时刻状态是否安全
if (stat)
cout<<"当前系统状态是安全的"<<endl;
else {
cout<<"当前系统状态是不安全的"<<endl;
delete []Request;
return 0;
}
while (1) // 当输入的进程号出错时,循环结束
{
cout<<"\n请输入进程号 (0--"<<n-1<<", -1=Exit) : ";
cin>>k;
if (k>=n)
{
cout<<"\nError : The number of Process is out of range !"<<endl;
continue;
}
if (k<0) break;
cout<<"请输入进程P"<<k<<"申请的资源数: ";
for (i=0;i<m;i++)
cin>>Request[i];
stat=b.banker(k,Request);
b.Print();
}
delete []Request;
return 1;
}
2.程序运行结果
按教科书上的例题,运行程序,输入分配矩阵、尚需矩阵、可用资源向量的数值,可得如下的运行结果。请你分析运行结果,加深对银行家算法的理解。
输入分配矩阵Allocation:
输入分配矩阵的第 1 行:0 1 0
输入分配矩阵的第 2 行:2 0 0
输入分配矩阵的第 3 行:3 0 2
输入分配矩阵的第 4 行:2 1 1
输入分配矩阵的第 5 行:0 0 2
输入尚需矩阵Need:
输入尚需矩阵的第 1 行:7 4 3
输入尚需矩阵的第 2 行:1 2 2
输入尚需矩阵的第 3 行:6 0 0
输入尚需矩阵的第 4 行:0 1 1
输入尚需矩阵的第 5 行:4 3 1
输入可用资源向量Available:3 3 2
Allocation Need Available
0 1 0 7 4 3 3 3 2
2 0 0 1 2 2
3 0 2 6 0 0
2 1 1 0 1 1
0 0 2 4 3 1
当前系统状态是安全的
请输入进程号 (0--4, -1=Exit) : 1
请输入进程P1申请的资源数: 1 0 2
满足进程P1的请求(1,0,2)后,系统状态仍然是安全的。安全序列为:
P1,P3,P4,P0,P2
Allocation Need Available
0 1 0 7 4 3 2 3 0
3 0 2 0 2 0
3 0 2 6 0 0
2 1 1 0 1 1
0 0 2 4 3 1
请输入进程号 (0--4, -1=Exit) : 4
请输入进程P4申请的资源数: 3 3 0
Request > Available,P4 is blocked
Allocation Need Available
0 1 0 7 4 3 2 3 0
3 0 2 0 2 0
3 0 2 6 0 0
2 1 1 0 1 1
0 0 2 4 3 1
请输入进程号 (0--4, -1=Exit) : 3
请输入进程P3申请的资源数: 1 0 0
Error : Request of P3 > Max[3]
Allocation Need Available
0 1 0 7 4 3 2 3 0
3 0 2 0 2 0
3 0 2 6 0 0
2 1 1 0 1 1
0 0 2 4 3 1
请输入进程号 (0--4, -1=Exit) : 0
请输入进程P0申请的资源数: 0 2 0
如果满足进程P0的资源请求(0,2,0),系统将进入不安全状态,
故系统将拒绝该进程的请求。
Allocation Need Available
0 1 0 7 4 3 2 3 0
3 0 2 0 2 0
3 0 2 6 0 0
2 1 1 0 1 1
0 0 2 4 3 1
请输入进程号 (0--4, -1=Exit) : 0
请输入进程P0申请的资源数: 0 1 0
满足进程P0的请求(0,1,0)后,系统状态仍然是安全的。安全序列为:
P1,P3,P4,P0,P2
Allocation Need Available
0 2 0 7 3 3 2 2 0
3 0 2 0 2 0
3 0 2 6 0 0
2 1 1 0 1 1
0 0 2 4 3 1
请输入进程号 (0--4, -1=Exit) : -1
上机实验七 模拟页面置换算法 (4学时)
7.1 上机实验基本要求(2学时)
(1) 设计并实现一个虚存管理程序,模拟一个单道程序的虚拟页式存储管理。
(2) 建立一张单级页表。
(3) 程序中使用随机数函数rand()产生的随机数作为要访问的虚地址,为简单起见,该随机数的最低位兼做修改标志。分配的主存块号也使用rand()产生。
(4) 实现函数response()响应访存请求,完成虚地址到实地址的定位,同时判断并处理缺页中断。
(5) 实现FIFO页面置换算法。
7.2 实验设计
本实验参考程序在Windows 2000/XP+VC++6.0环境下开发,程序模拟了单个程序的内存访问控制过程。
7.2.1 数据结构
① 页表结构如下:
struct pagetable //页表结构
{
int sflag; //状态标志,"1"表示该页在内存
int framenum; //内存块号
int aflag; //访问标志,"1"表示该页已被访问过
int mflag; //修改标志,"1"表示该页已被修改过
};
其中状态标志"1"表示对应的页在内存,"0"表示对应的页不在内存;修改标志为"1"表示对应的页已被修改,当该页被淘汰时须存盘。修改标志为"0"表示对应的页未被修改,当被置换时该页可直接被淘汰,不需存盘。
② 用一维整型数组模拟页号队列(循环队列)。
7.2.2 程序实现
1.主程序
主程序(主函数)的总体流程如图7-1所示。
2.随机数函数
通过随机函数rand()生成一个0~RAND_MAX之间的随机数来模拟要访问的虚地址和分配的内存块号,RAND_MAX的值在stdlib.h中定义为0x7fff(即32767)。在使用rand()之前须先调用srand()函数设置用于生成随机数的随机序列“种子”。通常种子可通过srand( (unsigned) time(NULL))的方式进行设置,其中函数time()返回1970年1月1日UTC时间0时0分0秒开始至今的秒数。
3.处理访存请的函数response( )
处理访存请的函数response( )完成的工作包括:分解虚地址为页号和页内偏移、判断是否地址越界、查找页表、处理缺页中断、必要时调用页面置换程序进行页面置换、修改页表、实现地址重定位等。简易流程图如图7-2所示。
4.进程休眠
在主函数中,每次产生和相应访存请求后,要使进程休眠一段时间,从而模拟用户程序访存的不定时性。这里用到Windows的API函数Sleep()。此函数的说明参见“上机实验二”的2.2.2节。
7.3 源程序与运行结果
7.3.1 参考程序源代码
源程序名:VMM_FIFO.cpp
#include <iostream.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
struct pagetable //页表结构
{
int sflag; //状态标志,"1"表示该页在内存
int framenum; //内存块号
int aflag; //访问标志,"1"表示该页已被访问过
int mflag; //修改标志,"1"表示该页已被修改过
};
pagetable *ptab; // 指向页表的指针
int psize=4096; // 页的大小为4K字节
int PN; // 进程的页数
int BN; // 分配给该进程的内存块数
//int MB=65536; // 假设内存共有65536个块
int *ps; // 指向页号的FIFO队列的指针
void Printptab() //输出(显示)页表
{
cout<<"页号\t状态\t块号\t访问位\t修改位"<<endl;
for (int i=0;i<PN;i++)
{
cout<<' '<<i<<"\t ";
cout<<ptab[i].sflag<<"\t ";
cout<<ptab[i].framenum<<"\t ";
cout<<ptab[i].aflag<<'\t';
cout<<ptab[i].mflag<<endl;
}
}
void FIFO(int vaddr,int &k) //FIFO页面置换函数
{
int b; // 内存块号
int pn; // 当前要访问的页号
int pp; // 将被淘汰的页号
int offset; // 页内偏移
unsigned int paddr; // 物理地址
int mf; // 修稿标志
mf=vaddr%2; // 参数vaddr的最低位兼作修改标志
pn=vaddr/psize;
offset=vaddr%psize;
pp=ps[k]; // 取出将要被淘汰的页号
b=ptab[pp].framenum;// 取出被淘汰页所在的内存块号
if (ptab[pp].mflag==1)
cout<<"被置换的"<<pp<<"号页存盘后淘汰,";
else
cout<<"被置换的"<<pp<<"号页直接被淘汰,";
// 修改页表
ptab[pp].sflag=0; // 置pp页已不在内存的标志
ptab[pp].framenum=0; // 操作系统实际上不作此置0工作
ptab[pp].aflag=0; // 此处置0只是为了显示页表清楚
ptab[pp].mflag=0; // 此处置0只是为了显示页表清楚
ptab[pn].sflag=1; // pn页调入内存
ptab[pn].framenum=b;
ptab[pn].aflag=1;
ptab[pn].mflag=mf;
ps[k]=pn; // 页号入队
k=(k+1) % BN; // 调整循环队列ps的指针
paddr=b*psize+offset; /*形成物理地址*/
cout<<"虚地址"<<vaddr<<"所在的"<<pn<<"页调入"<<b
<<"块中,对应实地址是"<<paddr<<endl; //输出绝对地址
}
bool response(int vaddr,int &n)
{
static int k=0; // FIFO页号循环队列中,k所指的页是将被淘汰的页
static int bn=0; // 已分配给该进程的内存块数
int b,bb; // b是申请得到的内存块号
int pn; // 当前要访问的页号
int offset; // 页内偏移
unsigned int paddr; // 物理地址
int mf; // 修改标志
mf=vaddr%2; // 参数vaddr的最低位兼作修改标志
pn=vaddr/psize;
offset=vaddr%psize;
if (pn>=PN) // 地址越界
{
cout<<"虚地址"<<vaddr<<"所在的页号为"<<pn<<"发生地址越界错误!"<<endl;
return false;
}
if (ptab[pn].sflag==1) // 所访问的页已在内存
{
cout<<"虚地址"<<vaddr<<"所在的"<<pn<<"页已经在内存的"
<<ptab[pn].framenum<<"块中,其对应的物理地址为 ";
paddr=ptab[pn].framenum*psize+offset;
cout<<paddr<<endl;
if (ptab[pn].mflag==0 && mf==1)
ptab[pn].mflag=mf; // 修改页表中修改标志位
}
else // 产生缺页中断
{
if (bn<BN) // 尚有内存块可分配
{
bb=rand();
b=bb*2+bb%2; // 假设内存块号不超过0x7fff * 2(即65535)
ps[bn]=pn; // 页号入队
bn++; // 已分配给该进程的内存块数增1
// 修改页表
ptab[pn].sflag=1;
ptab[pn].framenum=b;
ptab[pn].aflag=1;
ptab[pn].mflag=mf;
paddr=b*psize+offset; // 计算对应的物理地址(重定位)
cout<<"虚地址"<<vaddr<<"所在的"<<pn<<"页调入内存的"<<b<<"块中,";
cout<<"其对应的物理地址为 "<<paddr<<endl;
}
else // 缺页中断中发生页面置换
{
n++; // 页面置换次数增1
FIFO(vaddr,k); // 采用FIFO页面置换算法
}
} // 缺页中断完成
cout<<"页号队列:";
for (int i=0;i<BN;i++)
cout<<ps[i]<<' ';
cout<<endl;
return true;
}
void init() // 初始化
{
ps=new int[BN]; // 分配FIFO页号队列空间
for (int i=0;i<BN;i++)
ps[i]=-1; // 初始化页号队列为空
ptab=new pagetable[PN]; // 分配页表空间
for (i=0;i<PN;i++) // 初始化页表
{
ptab[i].sflag=0;
ptab[i].mflag=0;
ptab[i].aflag=0;
ptab[i].framenum=0;
}
}
void main()
{
int *pqueue; // 指向页面引用串的指针
int L; // 该进程的页面引用串的长度
int nn=0; // 用于统计页面置换次数
int i,vaddr,vd,pn;
bool flag;
srand((unsigned int)time(NULL)); // 置随机序列的“种子”(使用当前时间为种子)
cout<<"请输入进程的页数:";
cin>>PN;
cout<<"请输入分配给该进程的内存块数:";
cin>>BN;
cout<<"请输入该进程的页面引用串的长度:";
cin>>L;
cout<<endl;
init(); // 初始化
pqueue=new int[L]; // 分配页面引用串空间
for (i=0;i<L;i++)
pqueue[i]=-1; // 初始化页面引用串
for (i=0;i<L;i++)
{
vd=rand(); //访问串中页号由随机数模拟
vaddr=vd*PN/8; //rand()生成的随机数不超过0x7fff,相当于8页,需折合成PN页。
// vaddr=vd*(PN+1)/8; //rand()生成的随机数不超过0x7fff,相当于8页,需折合成PN页。
//PN+1是为了使之可能产生“地址越界“”
pn=vaddr/psize; // 计算出页号
pqueue[i]=pn; // 该页号存入页面引用串中
flag=response(vaddr,nn);
if (!flag) //若发生地址越界,则结束程序
{
L=i+1; //记下已访问的页数
break;
}
Sleep(3000+(rand()%5)*2000);
}
cout<<"\n页面引用串为:";
for (i=0;i<L-1;i++)
cout<<pqueue[i]<<',';
cout<<pqueue[i]<<endl;
cout<<"页面置换次数为 "<<nn<<endl;
cout<<"\n最终页表变为(不在内存的页对应的内存块号和修改标志无效):"<<endl;
Printptab( ); //输出页表
delete []ps;
delete []ptab;
delete []pqueue;
}
7.3.2 程序运行结果
程序运行样例结果如下(该程序每次运行的访问串可能都不同)。分析程序的运行结果,并用页面置换图来比较此结果,加深对FIFO页面置换算法的理解。
请输入进程的页数:10
请输入分配给该进程的内存块数:4
请输入该进程的页面引用串的长度:16
虚地址10993所在的2页调入内存的38395块中,其对应的物理地址为 157268721
页号队列:2 -1 -1 -1
虚地址3965所在的0页调入内存的45604块中,其对应的物理地址为 186797949
页号队列:2 0 -1 -1
虚地址13145所在的3页调入内存的25819块中,其对应的物理地址为 105755481
页号队列:2 0 3 -1
虚地址31003所在的7页调入内存的41112块中,其对应的物理地址为 168397083
页号队列:2 0 3 7
被置换的2号页存盘后淘汰,虚地址40351所在的9页调入38395块中,对应实地址是157269407
页号队列:9 0 3 7
被置换的0号页存盘后淘汰,虚地址11741所在的2页调入45604块中,对应实地址是186797533
页号队列:9 2 3 7
虚地址10835所在的2页已经在内存的45604块中,其对应的物理地址为 186796627
页号队列:9 2 3 7
虚地址10678所在的2页已经在内存的45604块中,其对应的物理地址为 186796470
页号队列:9 2 3 7
虚地址13586所在的3页已经在内存的25819块中,其对应的物理地址为 105755922
页号队列:9 2 3 7
被置换的3号页存盘后淘汰,虚地址1980所在的0页调入25819块中,对应实地址是105756604
页号队列:9 2 0 7
被置换的7号页存盘后淘汰,虚地址14635所在的3页调入41112块中,对应实地址是168397099
页号队列:9 2 0 3
虚地址12746所在的3页已经在内存的41112块中,其对应的物理地址为 168395210
页号队列:9 2 0 3
被置换的9号页存盘后淘汰,虚地址17596所在的4页调入38395块中,对应实地址是157267132
页号队列:4 2 0 3
虚地址1967所在的0页已经在内存的25819块中,其对应的物理地址为 105756591
页号队列:4 2 0 3
虚地址15341所在的3页已经在内存的41112块中,其对应的物理地址为 168397805
页号队列:4 2 0 3
虚地址9188所在的2页已经在内存的45604块中,其对应的物理地址为 186794980
页号队列:4 2 0 3
页面引用串为:2,0,3,7,9,2,2,2,3,0,3,3,4,0,3,2
页面置换次数为 5
最终页表变为(不在内存的页对应的内存块号和修改标志无效):
页号 状态 块号 访问位 修改位
0 1 25819 1 1
1 0 0 0 0
2 1 45604 1 1
3 1 41112 1 1
4 1 38395 1 0
5 0 0 0 0
6 0 0 0 0
7 0 0 0 0
8 0 0 0 0
9 0 0 0 0
7.4 进一步要求 (2学时)
1.设计LRU页面置换算法的函数,以便演示LRU算法的页面置换过程(提示:LRU算法需频繁调整移动页号队列ps中元素的位置)。参考源程序VMM_LRU.cpp。
2.设计Clock页面置换算法的函数,以便演示Clock算法的页面置换过程(提示:要实现Clock页面置换算法,要用到页表中的“访问标志”位aflag)。参考源程序VMM_Clock.cpp。
3.还可考虑用面向对象的方法(即采用类和对象的方法)设计程序。(选做内容)
【说明】要实现LRU或Clock算法的页面置换演示,只需修改程序中的response( )函数和页面置换函数的相关程序段(当然函数名也应把FIFO改为LRU或Clock)。
上机实验八 设备管理——磁盘I/O (2学时)
8.1 上机实验要求和目的
通过本上机实验了解Windows系统中如何直接使用磁盘的读写功能;所编应用程序能够响应用户指定的读写磁盘扇区的请求,也能提供查看磁盘相关参数的功能。
程序使用了Windows 操作系统提供的API来实现所要求的功能。
通过本实验,用户可以利用Win32 API进行底层的磁盘操作,了解Windows 操作系统设备管理的一些知识。
注意!!由于本实验是对磁盘进行底层I/O,读写操作并不经由操作系统的文件系统控制的,即写盘操作时并不考虑目标扇区是否已经被某个文件使用,所以写操作会破坏磁盘中的数据,甚至可能使该磁盘无法使用。因此强力建议做本实验时使用一张格式化的空白U盘或软盘(若不是空白盘但盘中数据不要了,则也可以)来进行写操作测试。对于C盘等,程序中严禁对它们进行写操作,但可以对它们执行读数据(包括读磁盘基本信息)的操作。读盘操作是安全的。
8.2 设计方案介绍
8.2.1 程序的总体框架
除主函数外,程序主要有如下几个函数组成:
· interwindow( ) 完成功能选择(文本窗口下的菜单)。
· opendisk( ) 打开磁盘并记录磁盘参数。
· physicalDisk( ) 显示磁盘想过数据。
· sectorDump( ) 读取指定的扇区域的内容。
· sectorWrite( ) 将键入的数据写到指定扇区中去。
· sectorRead( ) 进行实际的读盘操作。
· changeDrive( ) 改变操作的驱动器。
· partitionInfo( ) 显示磁盘分区信息
· scandrivetype( ) 扫描驱动器,寻找可移动磁盘
程序总体流程如图8-1所示。
8.2.2 数据结构和程序中用到的API函数介绍
1.数据结构
程序中定义了一个disk结构:
typedef struct disk {
HANDLE hDisk; //磁盘句柄
CHAR driveletter; //驱动器名(盘符)
UINT driveType; //驱动器类型
BOOL writeFlag; //可写标志
DISK_GEOMETRY theSupportedGeometry;
} *Disk ; //Disk是disk结构的指针
结构的主要成员的说明:
hDisk:当前打开的磁盘的句柄。调用CreateFile函数获得句柄,此后使用句柄来操作磁盘。
theSupportedGeometry:是DISK_GEOMETRY结构型成员。DISK_GEOMETRY是系统定义的用于记录磁盘参数的结构,该结构描述磁盘的物理(几何尺寸)参数,在对磁盘操作时需要这些参数,该结构的具体内容可参看MSDN或“OS实验涉及的API函数中使用的几个数据结构介绍.doc”。
2.程序中用到的API函数介绍
(1) CreateFile( )函数
功能:用来创建或打开一个文件、管道、磁盘设备等对象,它返回一个句柄用于以后对这个对象的访问。该函数正确执行返回文件句柄,执行失败,则返回INVALID_HANDLE_VALUE(即-1)。
格式:
HANDLE CreateFile (
LPCTSTR lpszName ,
DWORD fdwAccess ,
DWORD fdwShareMode ,
LPSECURITY_ATTRIBUTES lpsa ,
DWORD fdwCreate ,
DWORD fdwAttrsAndFlags ,
HANDLE hTemplateFile
)
参数说明:
lpszName:指向文件名的指针(可含空格,支持默认路径);当需要打开硬盘分区或U盘、软盘时,文件名格式为“\\.x”,其中x指磁盘名,例如C盘为“\\.C”。本程序中出示设定为I:盘(U盘)。
fdwAccess:创建或打开的文件的读写属性。取值可为:GENERIC_READ(值0x80000000)即可读,GENERIC_WRITE(值0x40000000)指可写,它们可通过“或”运算联接;取值0是查询模式,用于查询设备属性而不访问设备。
fdwShareMode:共享模式,值可取FILE_SHARE_READ(即值1)或FILE_SHARE_WRITE(值为2)或它们的“或”:. FILE_SHARE_READ|FILE_SHARE_WRITE(值为3)。
lpsa:一个LPSECURITY_ATTRIBUTES型指针,指定返回的句柄是否可以被子进程继承以及指定被打开对象的安全问题。NULL表示不可继承。
fdwCreate:指定当目标对象存在或不存在时分别以何种方式动作,例如取OPEN_EXISTING表示存在则打开,不存在则失败返回。对于磁盘对象,应使用OPEN_EXISTING。其它请参阅MSDN。
fdwAttrsAndFlags:设置文件属性和标志。FILE_ATTRIBUTE_NORMAL属性是最普通的文件使用方式,也是默认取值。下面对程序中用的标志作简单介绍:
FILE_FLAG_RANDOM_ACCESS:使所打开的文件(磁盘)可随机访问,操作系统将据此优化文件缓存。
FILE_FLAG_NO_BUFFERING:告诉系统,以无中间缓冲无高速缓存的方式打开文件。下述需求情况下需要以此标志打开文件:
· 访问文件的开始偏移位置必须是扇区长度的整数倍;
· 要读写文件的字节数必须是扇区长度的整数倍;
· 读写文件所用的缓冲区的大小必须是扇区大小的整数倍。
扇区大小可调用GetDiskFreeSpace( )函数得到(参见MSDN)。
hTemplateFile:临时文件的句柄,该临时文件提供本操作要创建文件的属性和扩展属性。这个参数通常不使用,取NULL即可。另,在Windows 95中,此值只能取NULL。
(2) DeviceIoControl( )函数 (注:这是一个功能强大的函数)
功能:直接向相应设备的驱动程序发出指令,以完成在函数参数中所指定的操作。不同的操作由函数的第二个参数dwIoControlCode控制。函数正确执行返回非0值,操作失败返回0.。
格式:
BOOL DeviceIoControl(
HANDLE hDevice, // handle to device of interest
DWORD dwIoControlCode, // control code of operation to perform
LPVOID lpInBuffer, // pointer to buffer to supply input data
DWORD nInBufferSize, // size, in bytes, of input buffer
LPVOID lpOutBuffer, // pointer to buffer to receive output data
DWORD nOutBufferSize, // size, in bytes, of output buffer
LPDWORD lpBytesReturned, // pointer to variable to receive byte count
LPOVERLAPPED lpOverlapped // pointer to structure for asynchronous operation
);
参数说明:(以下的输入/输出是从函数角度讲的)
hDevice:指向目标设备的句柄,由CreateFile函数取得。
dwIoControlCode:指定对目标设备进行何种操作以及目标设备的种类,具体的可取值请参考MSDN;本程序取IOCTL_DISK_GET_DRIVE_GEOMETRY表示获取物理磁盘的信息(执行此操作后,在lpOutBuffer所指的输出缓冲区中存有一个DISK_GEOMETRY结构,用户可从该结构的成员获得磁盘的几何尺寸数据:扇区大小BytesPerSector、每磁道扇区数SectorsPerTrack、每柱面磁道数TracksPerCylinder、柱面数Cylinders、介质类型MediaType等,参看MSDN);
IOCTL_DISK_GET_PARTITION_INFO表示获取磁盘分区信息(在本函数的输出缓冲区中,返回一个PARTITION_INFORMATION结构中,该结构的成员可参看MSDN或从本程序了解)。
lpInBuffer:是指向输入缓冲区的指针,该缓冲区中包含执行本次操作所需要的数据;如果参数dwIoControlCode指定的操作不需要输入数据,则该参数可以取NULL。
nInBufferSize:上述输入缓冲区所占的字节数。
lpOutBuffer:是指向接收该操作的输出数据的缓冲区的指针。
nOutBufferSize:上述区域所占的字节数。
lpBytesReturned:实际返回结果所占字节数。
lpOverlapped:指向OVERLAPPED结构,在本设备带参数FILE_FLAG_OVERLAPPED时来实现异步操作的,其含义是对这种目标对象的操作如果需要一定时间,则可以在操作未完成时便继续调用程序的执行,而该动作异步执行。本程序用不到,设为NULL。
(3) WriteFile( )函数
功能:向文件(或磁盘等对象)中写入数据。
格式:
BOOL WriteFile(
HANDLE hFile, // handle to file to write to
LPCVOID lpBuffer, // pointer to data to write to file
DWORD nNumberOfBytesToWrite, // number of bytes to write
LPDWORD lpNumberOfBytesWritten, // pointer to number of bytes written
LPOVERLAPPED lpOverlapped // pointer to structure for overlapped I/O
);
参数说明:
hFile:文件句柄。
lpBuffer:写数据缓冲区。
nNumberOfBytesToWrite:要写入的字节数。注意:在打开目标文件的函数CreateFile中,倒数第二个参数若取值为FILE-FLAG_NO_BUFFER,则在读写磁盘时读、写字节数必须是磁盘每扇区包含字节数的整数倍,且文件指针初始位置也要对准扇区边缘的。
lpNumberOfBytesWritten:成功写入的字节数,由该参数返回。
lpOverlapped:OVERLAPPED结构体指针,本程序取值为NULL。
(4) ReadFile( )函数
功能:从文件(或磁盘等对象)中读出数据。用法同WriteFile极其相似。
格式:
BOOL ReadFile(
HANDLE hFile, // handle of file to read
LPVOID lpBuffer, // pointer to buffer that receives data
DWORD nNumberOfBytesToRead, // number of bytes to read
LPDWORD lpNumberOfBytesRead, // pointer to number of bytes read
LPOVERLAPPED lpOverlapped // pointer to structure for data
);
参数说明:
hFile:文件句柄。
lpBuffer:读数据缓冲区。
nNumberOfBytesToRead:要读入的字节数。注意:在打开目标文件的函数CreateFile中,倒数第二个参数若取值为FILE-FLAG_NO_BUFFER,则在读/写磁盘时,读/写字节数必须是磁盘每扇区包含字节数的整数倍,且文件指针初始位置也要对准扇区边缘的。
lpNumberOfBytesread:成功读入的字节数,由该参数返回。
lpOverlapped:OVERLAPPED结构体指针,本程序取值为NULL。
(5) SetFilePointer( )函数
功能:移动一个打开的文件(或磁盘等对象)中的读写指针。
格式:
DWORD SetFilePointer(
HANDLE hFile, // handle of file
LONG lDistanceToMove, // number of bytes to move file pointer
PLONG lpDistanceToMoveHigh, // pointer to high-order DWORD of distance to move
DWORD dwMoveMethod // how to move
);
参数说明:
hFile:文件句柄。
lDistanceToMove:要将指针移动的偏移量(字节数)的低32位
lpDistanceToMoveHigh:要将指针移动的偏移量(字节数)的高32位
dwMoveMethod:确定开始移动的初始位置(取值FILE_BEGIN表示从文件头开始,类似地也可取FILE_CURRENT,FILE_END)。对于磁盘而言,初始位置必须是扇区长度的整数倍。
(6) GetDriveType( )函数
功能:过得驱动器类型。
格式:
UINT GetDriveType(
LPCTSTR lpRootPathName // pointer to root path
);
参数说明:
lpRootPathName:根目录路径名。例如"C:\"。若用NULL,则函数使用当前目录的根目录名。
返回值:
函数返回一个无符号整数,其值含义如下表:
值 | 符号常量 | 含 义 |
0 | DRIVE_UNKNOWN | 未知驱动器类型 |
1 | DRIVE_NO_ROOT_DIR | 驱动器无根目录(驱动器不存在) |
2 | DRIVE_REMOVABLE | 可移动驱动器(例如U盘) |
3 | DRIVE_FIXED | 固定盘(硬盘) |
4 | DRIVE_REMOTE | 远程(网络)驱动器 |
5 | DRIVE_CDROM | CD-ROM(光驱) |
6 | DRIVE_RAMDISK | RAM盘(内存中的虚拟盘) |
8.3 源程序和运行结果
8.3.1 源程序代码
参考源代码如下:
1.Disk.h头文件
// *********** Disk.h ***********
#include <windows.h>
#include <winioctl.h> // 包含了DISK_GEOMETRY等定义
#include <stdio.h>
//#include <iostream.h>
//Disk是结构指针
typedef struct disk {
HANDLE hDisk; //磁盘句柄
CHAR driveletter; //驱动器名(盘符)
UINT driveType; //驱动器类型
BOOL writeFlag; //可写标志
DISK_GEOMETRY theSupportedGeometry;
} *Disk; //Disk是指向disk结构的指针
// 以下程序中用到的几个函数
// 打开磁盘,并获得相关物理信息,存入返回的disk结构的theSupportedGeometry项中
int opendisk(Disk &theDisk);
char interwindow(Disk theDisk); // 功能选择接口
bool phyysicalDisk(Disk theDisk); // 获得磁盘的物理参数显示出来
// 读取特定的磁盘区域的内容并将它们显示出来(文件和十六进制两种方式)
bool sectorDump(Disk theDisk);
// 从某磁盘扇区中读出指定字节数的数据到指定缓冲区RdBuf
BOOL sectorRead(Disk theDisk,unsigned logSectorNumber,char *RdBuf);
// 从指定缓冲区WrBuf写指定字节数的数据到某磁盘扇区中
BOOL sectorWrite(Disk theDisk);
bool partitionInfo(Disk theDisk); //显示分区信息
void changeDrive(Disk &theDisk); //改变驱动器
UINT getdrivetype(char drivel); //获取驱动器类型
void scandrivetype(Disk theDisk); //扫描驱动器,寻找可移动磁盘
2.Disk.cpp文件
// ************* Disk.cpp ****************
#include "Disk.h"
// 驱动器类型
char DriveType[][30]={"DRIVE_UNKNOWN","NO_THIS_DRIVE","DRIVE_REMOVABLE",
"DRIVE_FIXED","DRIVE_REMOTE","DRIVE_CDROM","DRIVE_RAMDISK"};
void main()
{
disk objdisk;
Disk theDisk=&objdisk;
objdisk.hDisk=NULL; //句柄初始为空
char choice;
bool runFlag=true;
int openFlag;
//扫描驱动器,寻找可移动磁盘作为初始磁盘
scandrivetype(theDisk);
while (runFlag)
{
if (theDisk->hDisk==NULL) //打开磁盘
openFlag=opendisk(theDisk);
if (openFlag==NULL) //若打开失败,显示出错信息
{
printf("\n\topen disk failed\n");
printf("\tdisk %c: may be not existence\n\n",theDisk->driveletter);
break;
}
if (openFlag==-1)
{
printf("\n\tdisk %c: can't be read\n",theDisk->driveletter);
if (theDisk->driveType==5) //若是光驱
printf("\tthere is no CD-ROM in drive %c:",theDisk->driveletter);
}
choice=interwindow(theDisk);
if (choice=='6')
{
printf("\n\n");
runFlag=false; //退出循环
continue;
}
switch(choice)
{
case '1' : if (phyysicalDisk(theDisk) == false)
printf("\n\tcan't read disk information\n");
break;
case '2' : if (partitionInfo(theDisk)==false)
printf("\n\tGet the information of partition Error.\n");
break;
case '3' : if (!theDisk->writeFlag)
printf("\n\tDrive %c can't be written\n",theDisk->driveletter);
else if (sectorWrite(theDisk) == false)
printf("\n\tWrite sector failed\n");
break;
case '4' : sectorDump(theDisk);
break;
case '5' : changeDrive(theDisk); break;
default : printf("\n\tWrong choice\n");
}
printf("\n\n\tPress \"Enter\" key to continue ");
flushall(); //清除缓冲区
getchar(); //等待按键
}
if (theDisk!=NULL) //关闭磁盘句柄
CloseHandle(theDisk->hDisk);
}
// 功能选择窗口
char interwindow(Disk theDisk)
{
system("cls"); //清屏
char choice;
char chyn[][6]={" not "," "};
printf("\n\n\t*************** disk I/O test ***************\n");
printf("\n\t Current disk is %c:, ",theDisk->driveletter);
printf("It can%sbe written\n\n",chyn[theDisk->writeFlag]);
printf("\n\t Push 1 to get the information of disk\n");
printf("\t Push 2 to get the information of partition\n");
printf("\t Push 3 to write information to a sector\n");
printf("\t Push 4 to read a sector from disk\n");
printf("\t Push 5 to change the drive for I/O\n");
printf("\t Push 6 to exit from the test\n\n");
printf("\t you choice is : ");
scanf("%c",&choice);
flushall(); //清除键盘输入缓冲区
return choice;
}
UINT getdrivetype(char drivel)
{
char rootdir[]=" :\\";
UINT type;
rootdir[0]=drivel; //构成跟目录字符串"x:\"
type=GetDriveType(rootdir);
return type;
}
void scandrivetype(Disk theDisk)
{
char drivel='C';
UINT type;
while ((type=getdrivetype(drivel))!=1)
{
if (type==2) { //找到可移动磁盘
theDisk->driveletter=drivel; //初始盘符是U盘盘符
theDisk->driveType=type;
theDisk->writeFlag=true; //设定对U盘可写
break;
}
drivel++;
}
if (type==1) { //未找到可移动磁盘
theDisk->driveletter='C'; //初始盘符设为'C'
theDisk->driveType=3; //初始值为硬盘
theDisk->writeFlag=false; //不可写
}
}
bool partitionInfo(Disk theDisk) // 获取磁盘分区信息并显示
{ // 光驱中无光盘时能获取部分分区信息
char yesno[][6]={"No","Yes"};
char PartitionType[][30]={"PARTITION_ENTRY_UNUSED","PARTITION_FAT_12",
"PARTITION_XENIX_1","PARTITION_XENIX_2","PARTITION_FAT_16",
"PARTITION_EXTENDED","PARTITION_HUGE","PARTITION_IFS"};
int v[]={14,15,65,99,192};
char type[][30]={"PARTITION_XINT13","PARTITION_XINT13_EXTENDED",
"PARTITION_PREP","PARTITION_UNIX","VALID_NTFT"};
DWORD ReturnSize;
HANDLE hDisk=theDisk->hDisk;
PARTITION_INFORMATION partitionInfo;
PARTITION_INFORMATION *pp=&partitionInfo;
int flag=DeviceIoControl(hDisk,IOCTL_DISK_GET_PARTITION_INFO,NULL,0,
pp,50,&ReturnSize,NULL);
if (!flag) //若调用DeviceIoControl函数失败则返回0
return false;
printf("\n\n\tPARTITION INFORMATION (Drive %c:)\n\n",theDisk->driveletter);
printf("\tStartingOffset : %I64d\n",pp->StartingOffset);
printf("\tPartitionLength : %I64d\n",pp->PartitionLength);
printf("\tHiddenSectors : %d\n",pp->HiddenSectors);
printf("\tPartitionNumber : %d ( %c: )\n",pp->PartitionNumber,theDisk->driveletter);
int n=pp->PartitionType;
if (n<8)
printf("\tPartitionType : %s\n",PartitionType[n]);
else
{
for (int i=0,flag=0;i<5;i++)
{
if (n==v[i])
{
flag=1;
printf("\tPartitionType : %s\n",type[i]);
break;
}
}
if (!flag)
printf("\tPartitionType : %d\n",n);
}
printf("\tBootIndicator : %s\n",yesno[pp->BootIndicator]);
printf("\tRecognizedPartition : %s\n",yesno[pp->RecognizedPartition]);
printf("\tRewritePartition : %s\n",yesno[pp->RewritePartition]);
return true;
}
void changeDrive(Disk &theDisk)
{
char drivel;
char yesno[][6]={"false","true"};
UINT type;
printf("\n\tcurrent driveletter is %c\n",theDisk->driveletter);
printf("\tinput new driveletter : ");
scanf("%c",&drivel);
flushall(); //清除键盘输入缓冲区
if (drivel=='\0'||drivel=='\n') //若直接键入回车键
{
printf("\n\n\tDrive does not change.\n");
return;
}
drivel=toupper(drivel); //转换成大写字母
if (theDisk->driveletter!=drivel)//若输入的驱动器符与当前驱动器符不同
{
type=getdrivetype(drivel);//获取新指定驱动器的类型
if (type==1) //新指定的驱动器不存在
{
printf("\n\tdrive %c: may not be existence\n",drivel);
printf("\tDrive does not change.\n");
return;
}
theDisk->driveType=type; //保存驱动器类型
theDisk->driveletter=drivel; //保存盘符
printf("\n\t%c: is %s\n",drivel,DriveType[type]);//显示驱动器类型
if (type==2) //若新指定的驱动器是U盘
theDisk->writeFlag=true; //允许对新指定驱动器执行写操作
else
theDisk->writeFlag=false; //其它类型的驱动器都不允许写盘
printf("\n\n\tNew drive is : %c\n",drivel); //显示新驱动器符
printf("\twriteFlag is : %s\n",yesno[theDisk->writeFlag]);//显示新驱动器能否“写”
theDisk->driveletter=drivel; //新指定驱动器作为当前驱动器
if (theDisk) //关闭原来的磁盘句柄
{
CloseHandle(theDisk->hDisk);
theDisk->hDisk=NULL; //新驱动器尚未打开,其句柄为0
}
}
else //新指定驱动器与当前驱动器向相同
printf("\n\n\tDrive does not change.\n");
}
// 将获得磁盘的物理参数显示出来
bool phyysicalDisk(Disk theDisk)
{
char mediatype[][40]={"Format is unknown","5.25\", 1.2MB, 512 bytes/sector",
"3.5\", 1.44MB, 512 bytes/sector","3.5\", 2.88MB, 512 bytes/sector",
"3.5\", 20.8MB, 512 bytes/sector"," "," "," "," "," "," ",
"RemovableMedia","FixedMedia"};
if (theDisk->hDisk==NULL)
{
printf("there is no disk available!\n");
return false;
}
DWORD ReturnSize;
int flag=DeviceIoControl(theDisk->hDisk,IOCTL_DISK_GET_DRIVE_GEOMETRY,
NULL,0,&(theDisk->theSupportedGeometry),50,&ReturnSize,NULL);
if (!flag)
return false;
printf("\n\n\tDISK INFORMATION (Drive %c:)\n\n",theDisk->driveletter);
DWORD sectorsize=theDisk->theSupportedGeometry.BytesPerSector;
printf("\tBytesPerSector : %d\n",sectorsize);
printf("\tSectorPerTrack : ");
printf("%d\n",theDisk->theSupportedGeometry.SectorsPerTrack);
printf("\tTrackPerCylinder : ");
printf("%d\n",theDisk->theSupportedGeometry.TracksPerCylinder);
printf("\tCylinders : %d\n",theDisk->theSupportedGeometry.Cylinders);
int mtype=theDisk->theSupportedGeometry.MediaType;
printf("\tMediaType : %s\n\n",mediatype[mtype]);
return true;
}
//打开磁盘,获得句柄存入返回的一个disk结构
int opendisk(Disk &theDisk)
{
char buffer[]="\\\\.\\ :";
buffer[4]=theDisk->driveletter;
DWORD ReturnSize;
// 调用API函数CreateFile( )打开磁盘,返回的磁盘句柄存于hDisk
theDisk->hDisk=CreateFile(
buffer, //根目录路径名,例如 \\.\C
GENERIC_READ|GENERIC_WRITE, //可读、可写。此处可用0xc0000000
FILE_SHARE_READ|FILE_SHARE_WRITE, //读写共享模式。此处可用3
NULL,
OPEN_EXISTING, //若对象存在,则打开它;否则,本操作失败
FILE_FLAG_RANDOM_ACCESS|FILE_FLAG_NO_BUFFERING,
NULL
); //失败时返回INVALID_HANDLE_VALUE(即-1)
if (theDisk->hDisk==INVALID_HANDLE_VALUE)//若驱动器不存在
{
theDisk->hDisk=NULL;
return 0;
}
//获取它的物理参数(磁盘打开后一般需执行此操作后才能正确对磁盘读/写)
int flag=DeviceIoControl(theDisk->hDisk,IOCTL_DISK_GET_DRIVE_GEOMETRY,
NULL,0,&(theDisk->theSupportedGeometry),50,&ReturnSize,NULL);
if (!flag)
return -1;
return 1;
}
// 读取指定的磁盘区域的内容并将它们显示出来(文件和十六进制两种方式)
bool sectorDump(Disk theDisk)
{
if (theDisk->hDisk==NULL)
{
printf("\n\tthere is no disk available!\n");
return false;
}
DWORD sectorsize=theDisk->theSupportedGeometry.BytesPerSector;
char *RdBuf=new char[sectorsize]; //动态分配输入缓冲区
int j,logSectorNumber;
//从磁盘某扇区中读出内容并显示(文件和十六进制两种方式)
printf("\n\n\tPlease Input the Sector NO to read from : ");
scanf("%d",&logSectorNumber);getchar();
if (!sectorRead(theDisk,logSectorNumber,RdBuf))
{
printf("\n\tError occurred while reading the sector!\n");
delete []RdBuf; //释放输入缓冲区
return false;
}
printf("\nText Content : \n");
for (DWORD i=0;i<sectorsize;i++)
printf("%c",RdBuf[i]);
printf("\n\n Hex Content :\n");
for (i=0,j=0;i<sectorsize;i++)
{
if (j%16==0)
printf("\n %03x : ",j);
printf("%02x ",(BYTE)RdBuf[i]); //BYTE是单字节整数
j++;
}
printf("\n");
delete []RdBuf; //释放输入缓冲区
return true;
}
// 从某磁盘扇区中读出指定字节数的数据到指定缓冲区RdBuf
BOOL sectorRead(Disk theDisk,unsigned logSectorNumber,char *RdBuf)
{
DWORD BytesRead;
DWORD sectorsize=theDisk->theSupportedGeometry.BytesPerSector;
long sectortomove=logSectorNumber*(theDisk->theSupportedGeometry.BytesPerSector);
SetFilePointer(theDisk->hDisk,sectortomove,NULL,FILE_BEGIN);
if (!ReadFile(theDisk->hDisk,RdBuf,sectorsize,&BytesRead,NULL))
return false;
return true;
}
// 将用户输入的数据写到指定的磁盘扇区中
BOOL sectorWrite(Disk theDisk)
{
DWORD BytesWrite;
DWORD sectorsize=theDisk->theSupportedGeometry.BytesPerSector;
int logSectorNumber;
if (theDisk->hDisk==NULL)
{
printf("\n\nthere is no disk available!\n");
return false;
}
char *WrBuf=new char[sectorsize]; //动态分配输出缓冲区
ZeroMemory(WrBuf,sectorsize); // 输出缓冲区内存空间清零
// 从指定缓冲区WrBuf写指定字节数的数据到某磁盘扇区中
printf("\n\n\tPlease Input the sector NO to Write to : ");
scanf("%d",&logSectorNumber);
getchar();
printf("\n\tPlease input the content to write to disk\n\t");
gets(WrBuf);
long sectortomove=logSectorNumber*(theDisk->theSupportedGeometry.BytesPerSector);
SetFilePointer(theDisk->hDisk,sectortomove,NULL,FILE_BEGIN);//读写指针移到指定位置
if (!WriteFile(theDisk->hDisk,WrBuf,sectorsize,&BytesWrite,NULL))
{
delete []WrBuf; //释放输出缓冲区
return false;
}
printf("\n\twrite complete successfully\n");
delete []WrBuf; //释放输出缓冲区
return true;
}
8.3.2 程序的运行结果解释(操作说明)
程序运行时,屏幕显示一个选择控制界面,供用户选择5种功能,如图8-2所示。
(1) 在上述“菜单”中键入“1”,输出磁盘的基本信息(DISK_GEOMETRY,即磁盘的“几何形状”)。例如对某个硬盘C:操作后的输出结果如图8-3所示。
【说明】大多数磁盘(或U盘、光盘)的扇区大小为512B,但有极少数是1024或2048字节。
(2) 菜单中选择“2”,显示磁盘的分区信息,一般应用于硬盘,但对U盘也可执行此操作。例如对某U操作的输出结果如图8-4所示。
(3) 菜单中选择“3”,往指定扇区中写数据,所写数据由用户键入。
注意!!由于本实验是对磁盘进行底层I/O,读写操作并不经由操作系统的文件系统控制的,即写盘操作时并不考虑目标扇区是否已经被某个文件使用,因此写操作会破坏磁盘中的数据,甚至可能使该磁盘无法使用。因此强力建议做本实验时使用一张格式化的空白U盘或软盘(若不是空白盘但盘中数据不要了,则也可以)来进行写操作测试。对一个磁盘的0扇区执行写操作,将会破坏该盘的格式化信息,从而导致该盘无法使用。对于C盘等,应严禁对它们进行写操作,但可以对它们执行读数据(包括读磁盘基本信息)的操作。读盘操作是安全的。
以下是对某个U盘的1600号扇区进行写数据的示例:
首先用户输入要写入的扇区号1600后,接着屏幕显示“Please input the content to write to disk”,等待用户输入要写盘的数据。例如要写盘的内容是 “Nanjing University of Technology. 南京工业大学”,用户输入回车后,就将该文字存入到1600号扇区中。整个操作过程如图8-5所示。
(4) 菜单中选择“4”,读磁盘的指定扇区中的数据。首先屏幕提示用户键入要读的扇区号,用户键入某个扇区号(例如键入1600,查看刚才写入1600号扇区的内容)后,结果显示如图8-6所示。
由图8-6可见,从1600号扇区读出的内容正好就是刚才写入该扇区的内容,初步测试了程序读/写磁盘操作的正确性。
(5) 菜单中选择“5”,则可以改变驱动器。提供的参考源程序中,启动时首先扫描系统中的驱动器,寻找是否有可移动磁盘,若找到,则设定该U盘为初始驱动器,属性为“可写”。若找不到U盘,则设定初始驱动器为C:盘,其盘符是C,同时设定C盘是不允许执行写操作的。为安全起见,程序中规定,除了可移动盘(主要指U盘)可执行写操作外,其余各类盘都是不能执行写操作的。用户若要改变驱动器,可选5号功能。屏幕显示和操作过程如图8-7所示。
首先屏幕显示当前驱动器的盘符(本例是C),等待用户输入新的驱动器字母。例如输入i (输入不区分大小写,程序会统一转换成大写字母)。若输入的新盘与当前盘符不同,则程序检查输入的盘符对应的设备的存在性,若存在则获取其类型,然后显示该设备的类型(可移动盘、硬盘、光盘等等),如图8-7所示。
若用户输入的新盘符与当前盘符相同或仅输入回车,则保持原盘符不变,作相应的信息提示后,返回“功能选择”显示状态(即图7-2所示的状态)。
程序能够自动识别用户输入的新盘符对应的磁盘是否U盘,来确定新指定的磁盘是否“可写”。示例中新驱动器I:盘是可移动盘(U盘),能对新驱动器执行写操作。若用户输入的盘符对应的驱动器不存在,则保持驱动器不变,仍使用执行该功能前的驱动器。
值得注意的是,无论是光驱中有无光盘,程序都允许用户选择光驱。当光驱中无光盘时,在对该光驱执行读操作时,程序会发现操作失败而报错。此时用户可以插入光盘或者改变驱动器再操作。另外,即使光驱中无光盘,执行功能“2”时并不报错且能显示部分信息。
上机实验九 命令解释程序 (2学时)
9.1 上机实验目的
l 掌握命令解释程序的设计方法。
l 学习Windows系统调用的使用,了解目录操作、进程控制等相关知识。
l 培养C/C++语言程序设计技能,提高程序设计和文档编写能力。
l 锻炼团队成员的交流与合作能力。
9.2 上机实验要求
本实验要求实现一个简单的命令解释程序,其设计类似于MS-DOS的Command程序。具体要求如下:
(1) 参考Command命令解释程序,采用控制台命令行输入方式,命令行提示符是当前目录名与提示符“>”,在提示符后输入命令。命令执行结束后,在控制台继续显示提示符,等待输入新的命令。
(2) 实现以下内部命令:
l cd <路径名> 切换当前目录。
l dir [<路径名>] 显示指定目录下的文件、子目录及磁盘空间等相关信息。
l tasklist 显示系统当前进程信息,包括进程标识符pid、该进程包含的线程数、进程名等。
l taskkill <pid> 结束系统中正在运行的进程,须指定进程标识符pid。
l history 显示控制台中曾经输入过的命令。
l help 显示本程序使用的帮助信息。
l exit 退出控制台,结束本命令解释程序。
(3) 对前台进程和后台进程的操作
本实验设计的命令解释程序可以将进程放在前台执行或者后台执行。
启动前台进程的方法是在提示符下输入命令行:
fp <可执行文件名(含路径)>
启动后台进程的方法是在提示符下输入命令行:
bg& <可执行文件名(含路径)>
在前台进程运行期间,解释程序一直等待,直到前台进程运行结束,才再显示提示符;而在后台进程运行期间,解释程序不必等待,会立即显示提示符,允许用户输入下一条命令。
(4) 命令解释程序还需要捕获Ctrl+C组合键的信号来结束前台正在运行的控制台进程,并返回用户输入界面(显示提示符),等待新命令输入。本实验程序利用系统自备功能,来实现此功能。(注:若前台进程是图形界面,则按Ctrl+C并不能使其结束,而是使本实验的命令解释程序结束。)
(5) 其他要求
该命令解释程序应具有相应的出错提示功能。
程序每次接收用户输入的一行命令,在用户输入回车键(Enter)后开始执行命令。
若输入命令时仅输入回车键,则不作任何操作,重新显示提示符,等待用户输入新的命令。
定义空格为分隔符,程序应能处理命令中出现的重复空格符。
9.3 相关基础知识
9.3.1 命令解释程序与操作系统内核的关系
命令解释程序是用户和系统内核之间的接口程序。对于Windows系统来说,由于已经提供了具有良好交互性的图形用户界面,传统的控制台命令解释程序已经很少被广大用户所了解和使用。但是,对于某些应用,例如使用一条命令删除所有扩展名为tmp的文件,或者删除某些具有特殊名字的病毒文件,在图形用户界面下很难甚至不能完成。这需要通过Windows提供的Command命令接口来完成。Command程序是一个命令解释器,它拥有自己的内部命令集,用户和其他应用程序都可以通过对Command程序的调用完成与系统内核的交互。Command程序与内核的关系如图9-1所示。
9.3.2 系统调用及Win32 API相关函数介绍
操作系统所能完成的每一个特殊功能都有一个函数与其对应,即操作系统把它所能完成的功能以函数的形式提供给应用程序使用。应用程序对这些函数的调用叫系统调用。这些函数的集合就是Windows操作系统提供给应用程序的编程接口(Application Programming Interface),简称Windows API或Win32 API。所有在Win32平台上运行的应用程序都可以调用这些函数。
使用Windows API,应用程序可以充分挖掘Windows的32位操作系统潜力。Microsoft的所有32位平台都支持统一的API。
Windows的相关API的说明都可以在MSDN(Microsoft Developet Network)中查到,包括定义、使用方法等。下面简单介绍本实验中涉及的Windows API。
1.GetCurrentDirectory函数
功能:查找当前进程的当前目录,调用成功,返回装载到lpBuffer的字节数。失败则返回0。
格式:
DWORD GetCurrentDirectory ( //DWORD就是unsigned long
DWORD nBufferLength, // 缓冲区的长度
LPTSTR lpBuffer // 指定一个预定义字串,用于装载当前目录
) //LPSTR:是指向字符串的指针的类型名,即char *
【注】API中涉及的类型名,请参阅配套文档“Win32 Simple Data Types.doc”。
2.WaitForSingleObject函数
功能:等待一个事件信号直至信号出现或者超时。若等到信号则返回WAIT_OBJECT_0(即0),若等待超过dwMiliseconds时间还是无信号,则返回WAIT_TIMEOUT(即258)。若函数调用失败,则返回WAIT_FAILED (即-1)。
格式:
DWORD WaitForSingleObject (
HANDLE hHandle, // 事件的句柄
DWORD dwMilliseconds // 最大等待时间,以ms计时。
)
3.SetCurrentDirectory函数
功能:设置当前目录。返回非0表示成功,返回0表示失败。
格式:
BOOL SetCurrentDirectory (
LPCTSTR lpPathName // 新设置的当前目录路径
)
4.FindFirstFile函数
功能:用于从一个文件夹(包括子文件夹)中查找指定文件,返回找到的文件句柄。若调用失败,则返回INVALID_HANDLE_VALUE (即-1)。
格式:
HANDLE FindFirstFile (
LPCTSTR lpFileName, // 文件名字符串(可用通配符)
LPWIN32_FIND_DATA lpFindFileData // 指向一个用于保护文件的结构体
)
【注】WIN32_FIND_DATA结构的说明请参看MSDN或本上机实验指导的配套文档。
5.FindNextFile函数
功能:继续查找FindFirstFile函数搜索后的文件。它返回的文件句柄可以作为参数用于FindNextFile函数。这样就可方便地枚举出与lpFileName参数指定的文件名相匹配的所有文件。调用失败,返回0。
格式:
HANDLE FindNextFile (
HANDLE hFindFile, // 前一个搜素到的文件句柄
LPWIN32_FIND_DATA lpFindFileData // 指向一个用于保护文件的结构体
)
6.GetVolumeInformation函数
功能:用于获取磁盘相关信息。执行成功返回非0;失败,返回0。
格式:
BOOL GetVolumeInformation (
LPCTSTR lpRootPathName, // 磁盘驱动器代码字符串(具体构成方法参看程序)
LPCTSTR lpVolumeNameBuffer, // 磁盘驱动器卷标名称
DWORD nVolumeNameSize, // 磁盘驱动器卷标名称长度
LPWORD lpVolumeSerialNumber, // 磁盘驱动器卷标序列号
LPWORD lpMaximunComponentLength, //系统允许的最大文件长度
LPWORD lpFileSystemFlags, // 文件系统标识
LPCTSTR lpFileSystemNameBuffer, // 文件系统名称
DWORD nFileSystemNameSize // 文件系统名称长度
)
7.GetDiskFreeSppaceEx函数
功能:获取与一个磁盘的组织以及剩余容量有关的信息。调用失败返回0。
格式:
HANDLE GetDiskFreeSppaceEx (
LPCTSTR lpRootPathName, // 不包括卷名的磁盘根路径名
PULARGE_INTEGER lpFreeBytesAvailableToCaller, // 调用者可用的字节数
PULARGE_INTEGER lpTotalNumberOfBytes, // 磁盘上的总字节数
PULARGE_INTEGER lpTotalNumberOfFreeBytes // 磁盘上的可用字节数
)
参数说明:lpRootPathName:根路径名。例如形式为"C:\\"。使用NULL表示函数使用当前目录所在的磁盘。
8.FileTimeToLocalFileTime函数
功能:将一个FILETIME结构转换成本地时间。
格式:
BOOL FileTimeToLocalFileTime (
const FILETIME* lpFileTime, // 指向一个包含了UTC时间信息的结构
LPFILETIME lpLocalFileTime // 用于装载转换过的本地时间的结构体
)
9.FileTimeToSystemTime函数
功能:根据一个FILETIME结构的内容,装载一个SYSTENTIME结构。
格式:
BOOL FileTimeToSystemTime (
const FILETIME* lpFileTime, // 指向一个包含了文件时间信息的结构
LPFILETIME lpSystemTime // 用于装载系统时间的结构体
)
10.CreateToolhelp32Snapshot函数
功能:为指定的进程、进程使用的堆(heap)、模块(module)、线程(thread)建立一个快照(snapshot)。快照建立成功则返回快照的句柄,失败则返回INVALID_HANDL_VALUE。
格式:
HANDLE WINAPI CreateToolhelp32Snapshot (
DWORD dwFlags, // 指向快照中包含的系统内容
DWORD th32ProcessID // 指定将要快照的进程ID
)
11.Process32First函数
功能:是一个进程获取函数,当使用CreateToolhelp32Snapshot()函数获得当前运行进程的快照后,可以使用Process32First ( )函数获得第一个进程的句柄。
格式:
BOOL WINAPI Process32First (
HANDLE hSnapshot, // 快照句柄
LPPROCESSENTRY32 lppe // 指向一个保护进程快照信息的LPPROCESSENTRY32结构
)
12.Process32Next函数
功能:获取快照中下一个进程信息。
格式:
BOOL WINAPI Process32Next (
HANDLE hSnapshot, // 由Process32First或Process32Next函数获得的快照句柄
LPPROCESSENTRY32 lppe // 指向一个保护进程快照信息的LPPROCESSENTRY32结构
)
13.OpenProcess函数
功能:该函数打开一个已经存在的进程对象,若成功,返回值是指定进程的打开句柄。若失败,则返回空值。
格式:
HANDLE OpenProcesst (
DWORD dwDesiredAccess, // 权限标识(详见MSDN)
BOOL bInheritHandle, // 指出返回的句柄是否能被当前进程创建的新进程继承,
// TRUE表示可继承,FALSE表示不能继承。
DWORD dwProcessID // 进程ID
)
14.SetConsoleCtrlHandler函数
功能:添加或删除一个事件钩子(Handler)。
格式:
BOOL SetConsoleCtrlHandler (
PHANDLER_ROUTINE HandlerRoutine, // 回调函数的指针
BOOL Add // 表示添加或删除
)
15.CreateProcess函数
此函数已经在前面介绍,请参阅“上机实验一”的1.2.2节,在此不再赘述。
16.GetExitCodeProcess函数
功能:获取一个已中断进程的退出代码。
格式:
BOOL GetExitCodeProcess (
HANDLE hProcess, // 进程句柄
LPDWORD lpExitCode // 指向接受退出码的变量
)
17.TerminateProcess函数
功能:以给定的退出码终止进程。
格式:
BOOL TerminateProcee (
HANDLE hProcess, // 进程句柄
UINT uExitCode // 进程的退出码
)
9.4 实验设计
本实验在WindowsXP+VC++ 6.0环境下实现,利用Windows SDK提供的系统接口(API)完成程序的功能。因为VC++包含了Windows SDK所有工具和定义,所以安装了VC++就不用再特意安装SDK了。实验中所用的API,是操作系统提供的。要使用这些API,需要一些头文件,最常用的就是windows.h。一些特殊的API调研还需要其他的头文件。
9.4.1 重要的数据结构
1.历史命令循环数组(队列)
在history命令中,用数组来存放输入过的历史命令。程序中假设该数组的元素个数为20,数组元素的结构定义如下:
typedef struct ENV_HISTORY {
int start; // 队列的头指针
int end; // 队列的尾指针
char his_cmd[20][128]; // 队列数组(顺序结构的队列)
} ENV_HISTORY;
ENV_HISTORY envhis; // 定义队列变量(为队列分配内存空间)
2.文件信息链表
程序中,需要把dir命令取得的文件信息用链表保存,输出这些信息时对链表遍历。
链表结点的定义如下:
struct files_Content {
FILETIME time; // 文件创建时间
char name[200]; // 文件名
int type; // type=1普通文件, type=0目录
int size; // 文件大小
files_Content *next; // 构成链表的链接指针
} ;
9.4.2 程序实现
主程序的流程如图9-2所示。
1.解析命令
解析命令就是分析输入的命令行(input数组),分离命令行中的命令和参数。命令和参数的分隔是由空格符完成的。将命令存入arg[0]指向的字符串,将参数存入arg[1]指向的字符串中。
2.命令处理
命令出路与执行命令的目的有关,其中系统调用是重要的组成部分。
void cd_cmd(char *route)
{
if (!SetCurrentDirectory(route))
{ // 设置当前目录,若失败则输出出错信息
cout<<" SetCurrentDirectory failed ";
cout<<GetLastError()<<endl;
}
}
以上是cd命令的处理函数,涉及的Windows API为
SetCurrentDirectory( )函数,它的作用是设置当前目录为指定路径,若失败则返回出错信息。其中出错号从另一个API函数GetLastError( )获得。
其余命令处理函数结构类似,具体参见下面的源代码。
9.5 源程序与运行结果
9.5.1 程序源代码
1.WinShell.h
#define BUFSIZE MAX_PATH
#define HISNUM 20 //最多可以保存20个历史命令
char buf[BUFSIZE];
//保存历史命令的结构
typedef struct ENV_HISTORY {
int start; // 队列的头指针
int end; // 队列的尾指针
char his_cmd[20][128]; // 队列数组(顺序结构的队列)
} ENV_HISTORY;
ENV_HISTORY envhis; // 定义队列变量(为队列分配内存空间)
//说明:因envhis是全局变量(属静态变量),故其成员star,end有初值0
//保存文件或目录相关信息的结构
struct files_Content {
FILETIME time; // 文件创建时间
char name[200]; // 文件名
int type; // type=1普通文件, type=0目录
int size; // 文件大小
files_Content *next; // 构成链表的链接指针
} ;
2.WinShell.cpp
#define _Win32_WINNT 0x0501
#include <stdlib.h> //atoi()等
#include <iostream.h>
#include <windows.h> //DWORD;HANDLE...其中还有许多头文件
#include <tlhelp32.h> //CreateToolhelp32Snapshot()
#include <string.h>
#include "WinShell.h"
// 以下两个函数在主函数开头声明后放在主函数后面不行,故将它们移
// 至mian()的前面,原因可能是它们的参数类型分别是FILETIME和DWORD
// ***************** 时间处理函数 ******************
void ftime(FILETIME filetime)
{
SYSTEMTIME systemtime;
if (filetime.dwLowDateTime==-1) // Win32时间的低32位
cout<<"Never Expires\n";
else
{
//将UTC(Universal Time Coordinated)文件时间转换成本地文件时间
if (FileTimeToLocalFileTime(&filetime,&filetime)!=0)
{
//将64位时间转化成系统时间
if (FileTimeToSystemTime(&filetime,&systemtime)!=0)
{
//以一定能格式输出时间
cout.fill('0'); //不足指定宽度是用0填充
cout<<dec<<systemtime.wYear<<'-';
cout.width(2);cout<<systemtime.wMonth<<'-'; //月份用2位显示,下类似
cout.width(2);cout<<systemtime.wDay<<" ";
cout.width(2);cout<<systemtime.wHour<<':';
cout.width(2);cout<<systemtime.wMinute;
}
else
cout<<"FileTimeToSystemTime failed\n";
}
else
cout<<"FileTimeToLocalFileTime failed\n";
}
cout.fill(' '); //恢复空格填充
}
// ************ 回调函数 ************
BOOL WINAPI ConsoleHandler(DWORD CEvent)
{ // 此函数不做什么,由系统处理事件,包括按下Ctrl+C等
switch(CEvent)
{
case CTRL_C_EVENT:
break;
case CTRL_BREAK_EVENT:
break;
case CTRL_CLOSE_EVENT:
break;
case CTRL_LOGOFF_EVENT:
break;
case CTRL_SHUTDOWN_EVENT:
break;
}
return TRUE;
}
// **************** 主函数 ****************
void main()
{
//声明程序中用到的函数
void cd_cmd(char *dir); // cd命令处理函数
void dir_cmd(char *dir); // dir命令处理函数
void GetProcessList(); // 获得系统当前进程列表
void history_cmd(); // 获得最近输入的命令
void add_history(char *); // 将输入命令行添加到命令历史中
HANDLE process(int,char[]); // 创建进程
BOOL killProcess(char *); // kill进程
void help(); // 显示帮助信息
char c,*input,*arg[2],path[BUFSIZE];
int input_len=0,is_bg=0,i,j,k;
HANDLE hprocess; // 进程执行结束,返回进程句柄
DWORD dwRet;
while (true)//显示提示符,等待用户输入命令是个无限循环过程
{
// 将指向输入命令的指针数组初始化
for (i=0;i<2;i++)
arg[i]=NULL;
// 获得当前目录并存入path中,BUFSIZE是最多能够保存的路径名长度
dwRet=GetCurrentDirectory(BUFSIZE,path);//返回目录数据实际长度存于dwRet
if (dwRet==0) // 返回当前目录失败,输出出错信息
cout<<"GetCurrentDirectory failed "<<GetLastError()<<endl;
else if (dwRet>BUFSIZE)// BUFSIZE长度小于返回目录数据的长度,输出出错信息
cout<<"GetCurrentDirectory failed (buffer too small; need "<<dwRet<<"bytes)\n";
else
cout<<path<<'>'; // 显示提示符(当前目录名+'>')
// *********** 键盘输入 ************
input_len=0;
// 将命令开头的无用字符过滤掉
while ((c=cin.get())==' ' || c=='\t' || c==EOF) ;
if (c=='\n') //输入为空命令(仅输入回车符)时
continue; //结束本次循环,回到循环开头,重新显示提示符
while (c != '\n')
{
buf[input_len++]=c;
c=cin.get();
}
buf[input_len++]='\0'; // 加上串结束符
// 分配动态存储空间,将命令从缓存复制到input中
input=new char[input_len];
strcpy(input,buf); //为了便于后边的处理,将命令行复制到input中
// *********** 解析命令 ************
for (i=0,j=0,k=0; i<input_len && k<2; i++)//k<2是限制只处理1个命令参数
{ //即arg[0]为命令,arg[1]为参数
if (input[i]==' ' || input[i]=='\0')
{
if (j==0) // 去掉连在一起的多个空格
continue;
else
{
buf[j++]='\0';
arg[k]=new char[sizeof(char)*(j+1)];
strcpy(arg[k++],buf); // 将命令或参数复制到arg中
j=0; // 准备取下一个参数
}
}
else//不是' '和'\0'字符,则存入buf[]中
buf[j++]=input[i];
}
add_history(input); // 将输入命令添加到历史命令队列中
// ****************** 命令处理 ******************
if (strcmp(arg[0],"cd") == 0) // **** cd命令 ****
{
if (arg[1] != NULL)
{
cd_cmd(arg[1]);
delete []arg[1];
}
else
cout<<"cd命令必须指定路径名!\n";
delete []input;
delete []arg[0];
continue; //返回循环开头,重新显示提示符
}
if (strcmp(arg[0],"dir")==0) // **** dir命令 ****
{
char *route;
if (arg[1]==NULL) // 若dir命令无参数,则对当前目录操作
{
route=path; // 取当前目录
dir_cmd(route);
}
else
{
dir_cmd(arg[1]);
delete []arg[1];
}
delete []input; // 释放堆空间
delete []arg[0];
continue;
}
if (strcmp(arg[0],"tasklist")==0) // **** tasklist命令 ****
{
GetProcessList();// 该函数通过调用若干API函数,获取系统当前进程列表
delete []input;
delete []arg[0];
if (arg[1] != NULL)//防止用户误输入命令参数
delete arg[1];
continue;
}
if (strcmp(arg[0],"fp")==0) // *** fp命令(前台进程) ***
{
if (arg[1]==NULL)
{
cout<<"没有指定可执行文件\n";
delete []input;
delete []arg[0];
continue;
}
is_bg=0; // 后台标志置0(不是后台进程)
hprocess=process(is_bg,arg[1]); //创建进程,返回新进程的句柄
// 等待新进程执行完毕(INFINTE表示等待无限制)
if (WaitForSingleObject(hprocess,INFINITE)==WAIT_OBJECT_0)
{
//如果进程执行完毕,释放控制台
delete []input;
delete []arg[0];
delete []arg[1];
}
continue;
}
if (strcmp(arg[0],"bg&")==0) // *** bg&命令(后台进程) ***
{
if (arg[1]==NULL)
{
cout<<"没有指定可执行文件\n";
delete []input;
delete []arg[0];
continue;
}
is_bg=1; // 后台标志置1(真)
process(is_bg,arg[1]); //为可执行文件arg[1]创建后台进程
delete []input;
delete []arg[0];
delete []arg[1];
continue;
}
if (strcmp(arg[0],"taskkill")==0) // ***** kill进程 *****
{
BOOL success;
if (arg[1]!=NULL)
{
success=killProcess(arg[1]); // arg[1]指向进程ID
if (!success) // 若撤销进程失败,则显示出错信息
cout<<"kill process failed!\n";
delete []arg[1];
}
else
cout<<"taskkill命令必须指定进程ID!"<<endl;
delete []input;
delete []arg[0];
if (arg[1] != NULL)//防止用户误输入命令参数
delete arg[1];
continue;
}
if (strcmp(arg[0],"history")==0) // **** 显示历史命令 ****
{
history_cmd();
delete []input;
delete []arg[0];
if (arg[1] != NULL)//防止用户误输入命令参数
delete arg[1];
continue;
}
if (strcmp(arg[0],"help")==0) // **** help命令 ****
{
help();
delete []input;
delete []arg[0];
if (arg[1] != NULL)//防止用户误输入命令参数
delete arg[1];
continue;
}
if (strcmp(arg[0],"exit")==0) // **** exit命令 ****
{
cout<<"\nBye bye!\n\n";
delete []input;
delete []arg[0];
if (arg[1] != NULL)//防止用户误输入命令参数
delete arg[1];
break; // 退出死循环,结束程序
}
else // 输入命令不正确,给出出错信息
{
cout<<"please input correct commmand!\n";
delete []input;
if (arg[0])
delete []arg[0];
if (arg[1])
delete []arg[1];
continue;
}
}
} // **** 主函数结束 ****
// ************* 相关命令出路函数 **************
void cd_cmd(char *route) // **** cd命令实现函数 ****
{
if (!SetCurrentDirectory(route)) //设置当前目录,若失败则返回出错信息
cout<<"SetCurrentDirectory failed "<<GetLastError()<<endl;
}
// ***************** dir命令实现函数 *****************
void dir_cmd(char *route)
{
WIN32_FIND_DATA FindFileData; //将找到的文件或目录以WIN32_FIND_DATA结构返回
files_Content head,*p,*q; //定义指定文件结构体的头结点和指针
HANDLE hFind=INVALID_HANDLE_VALUE; // 句柄变量初值为“非法句柄值”
DWORD dwError; // 定义32位整数
char volume_name[256],str[22];
int file=0,dir=0; //文件数和目录数初始值为0
_int64 sum_file=0; //总文件大小为0字节,其值较大保存为64位整数
_int64 l_user,l_sum,l_idle; //调用者可用空间,总容量,磁盘总可用空间
unsigned long volume_number; //卷序列号
char *DirSpec[4];
head.next=NULL;
DirSpec[0]=new char[2];
strncpy(DirSpec[0],route,1);
DirSpec[0][1]='\0'; //DirSpec[0]为驱动器名
DirSpec[1]=new char[4];
strcpy(DirSpec[1],DirSpec[0]);
strncat(DirSpec[1],":\\",3); //DirSpec[1]用于获取驱动器信息
DirSpec[2]=new char[strlen(route)+2];
DirSpec[3]=new char[strlen(route)+5];
strcpy(DirSpec[2],route); //DirSpec[2]为dir命令的目录名
strcpy(DirSpec[3],route);
int len=strlen(route);
if (route[len-1]!='\\')
strncat(DirSpec[2],"\\",2);
strncat(DirSpec[3],"\\*.*",5); //DirSpec[3]用于查找目录中的所有文件
//搜素DirSpec[3]指定的文件,文件信息存于FindFileData变量中,返回找到的文件句柄
hFind=FindFirstFile(DirSpec[3],&FindFileData);
if (hFind==INVALID_HANDLE_VALUE) //查找句柄返回为无效值,查找失败
cout<<"Invalid file handle, Error is "<<GetLastError()<<endl;
else
{
//获取卷的卷名(存于volume_name),卷序列号(存于volume_number)
GetVolumeInformation(DirSpec[1],volume_name,50,&volume_number,NULL,NULL, NULL,10);
if (strlen(volume_name)==0)
cout<<"\n\n驱动器"<<DirSpec[0]<<"中的卷没有标签。"<<endl;
else
cout<<"\n\n驱动器"<<DirSpec[0]<<"中的卷是 "<<volume_name<<endl;
cout<<"卷的序列号是 "<<hex<<volume_number<<dec<<endl<<endl;;
cout<<DirSpec[2]<<" 的目录\n\n";
head.time=FindFileData.ftCreationTime;//获得的文件创建时间,存入文件结构体head中
strcpy(head.name,FindFileData.cFileName);//获得的文件名,存入文件结构体head中
// 若数据属性是目录,则置type为0
if (FindFileData.dwFileAttributes==FILE_ATTRIBUTE_DIRECTORY)
{
head.type=0;
dir++;
}
else
{
//如果数据属性是文件,type位为1
head.type=1;
head.size=FindFileData.nFileSizeLow; //将文件大小存入结构体head中
file++; //文件数增1
sum_file += FindFileData.nFileSizeLow; //将文件大小(字节数)累加
}
p=&head; // p指向头结点head
//如果还有下一个数据,继续查找
while (FindNextFile(hFind,&FindFileData) != 0)
{ // 第二个结点开始,分配动态空间
q=new files_Content[sizeof(files_Content)];
q->next=NULL;
q->time=FindFileData.ftCreationTime; // 保存文件创建时间
strcpy(q->name,FindFileData.cFileName); // 保存文件名
if (FindFileData.dwFileAttributes==FILE_ATTRIBUTE_DIRECTORY)
{
q->type=0; // 找到的是目录
dir++; // 目录数增1
}
else // 否则,找到的是文件
{
//如果数据属性是文件,type位为1
q->type=1;
q->size=FindFileData.nFileSizeLow; //将文件大小存入结构体
file++; //文件数增1
sum_file += FindFileData.nFileSizeLow; //将文件大小累加
}
p->next=q; // 构成单链表
p=q; // p指向新结点
}
p->next=NULL; // 链表尾结点的next指针须置为NULL
//将结构体中数据的创建时间、类型、大小、名称等信息依次输出
p=&head; // 从链表头结点开始
while (p != NULL)
{
ftime(p->time); // 按规定格式显示文件创建时间
if (p->type==0) // 若是目录,则显示“<DOR>”
cout<<"\t<DIR>\t\t";
else
{ // 若是文件,则按宽度为9的格式显示文件大小(字节数)
cout<<"\t\t";cout.width(9);
cout<<dec<<(unsigned)p->size;
}
cout<<'\t'<<p->name<<endl; // 显示文件名
p=p->next; // 准备显示下一个目录项(文件或目录)
}
//显示文件和目录总数以及磁盘空间相关信息
cout.width(15);
cout<<file<<" 个文件\t\t\t";
//printf()使用格式符“%I64d”可以输出64位整数,但“cout<<”只支持32位整数
//故此处先将64位整数sum_file转换成以10进制形式的字符串后再输出
_i64toa(sum_file,str,10); //64位整数sum_file转换成10进制字符串存于str中
cout<<str<<" 字节"<<endl;
GetDiskFreeSpaceEx(DirSpec[1],(PULARGE_INTEGER)&l_user,
(PULARGE_INTEGER)&l_sum,(PULARGE_INTEGER)&l_idle);
cout.width(15);
cout<<dir<<" 个目录\t\t\t";
_i64toa(l_idle,str,10); //64位整数l_idle转换成10进制字符串存于str中
cout<<str<<" 可用字节\n";
cout.width(15);
_i64toa(l_sum,str,10); //64位整数l_sum转换成10进制字符串存于str中
cout<<str<<" 磁盘总字节\n\n"<<endl;
dwError=GetLastError();
FindClose(hFind);
// 若出现其他异常情况,则输出出错信息
if (dwError!=ERROR_NO_MORE_FILES)
cout<<"FindNextFile error. Error is "<<dwError<<endl;
//释放files_Content结构体占用的动态空间
p=&head;
p=p->next; // head占用的不是动态空间,跳过head
while (p!=NULL)
{
q=p->next;
delete p; // 依次释放files_Content链表的后续结点
p=q;
}
}
}
// ************** tasklist命令 **************
void GetProcessList() // 函数功能:获取系统当前运行进程列表的命令
{
HANDLE hProcessSnap=NULL;
PROCESSENTRY32 pe32={0};
int pn=0; // 用于累计进程数
// 对系统中的进程进行拍照
hProcessSnap=CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);//pid为0表示任意进出
if (hProcessSnap==INVALID_HANDLE_VALUE)
cout<<"\nCtreateToolhelp32Snapshot() failed:"<<GetLastError();
//使用前要填充结构大小
pe32.dwSize=sizeof(PROCESSENTRY32);
// 列出进程
if (Process32First(hProcessSnap,&pe32))
{
DWORD dwPriorityClass;
cout<<"\n优先级\t\t进程ID\t\t线程\t\t进程名\n";
do {
HANDLE hProcess;
hProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,pe32.th32DefaultHeapID);
dwPriorityClass=GetPriorityClass(hProcess);
CloseHandle(hProcess);
//输出结果
cout<<pe32.pcPriClassBase<<"\t\t"; //输出该进程的优先级
cout<<pe32.th32ProcessID<<"\t\t"; //输出该进程的pid
cout<<pe32.cntThreads<<"\t\t"; //输出该进程的线程数
cout<<pe32.szExeFile<<endl; //输出进程名(或可执行文件名)
pn++; //进程数增1
} while (Process32Next(hProcessSnap,&pe32));//当快照中有下一个进程,继续循环
cout<<pn<<" 个进程\n"<<endl;
}
else
cout<<"\nProcess32First() failed:"<<GetLastError();
CloseHandle(hProcessSnap); // 释放快照句柄
}
void add_history(char *inputcmd)// ***** 将命令行插入历史队列中 *****
{
envhis.end=(envhis.end+1)%HISNUM;// end加1,准备将新的命令行插入队列
// end和start指向同一数组
if (envhis.end==envhis.start)//若队列满,则允许新插入的命令行覆盖旧命令行
envhis.start=(envhis.start+1)%HISNUM; //调整队列头指针start(移一位)
// 将命令存入end指向的数组中
strcpy(envhis.his_cmd[envhis.end],inputcmd);
}
// ************** history 命令 ****************
void history_cmd() // 显示历史命令
{
int i,j=1;
if (envhis.end==envhis.start)
cout<<"无历史命令\n"; //循环数组为空
else if(envhis.start<envhis.end)
{
//显示history命令数组中start+1到end的命令
for (i=envhis.start+1;i<=envhis.end;i++)
{
cout<<j<<'\t'<<envhis.his_cmd[i]<<endl;
j++;
}
}
else//否则,应分两段处理
{
// 显示history命令数组中start+1到HISNUM-1的命令
for (i=envhis.start+1;i<HISNUM;i++)
{
cout<<j<<'\t'<<envhis.his_cmd[i]<<endl;
j++;
}
// 显示history命令数组中0到end+1的命令
for (i=0;i<=envhis.end+1;i++)
{
cout<<j<<'\t'<<envhis.his_cmd[i]<<endl;
j++;
}
}
cout<<endl<<endl;
}
// ************** 创建进程命令 ****************
HANDLE process(int bg,char appName[]) //fp和bg&命令调用此函数
{
// 初始化进程相关信息
STARTUPINFO si; // 关于STARTUPINFO结构的成员的介绍,参看MSDN
PROCESS_INFORMATION pi; //关于PROCESS_INFORMATION结构的成员的介绍,参看MSDN
si.cb=sizeof(si); // si.cb的值应是STARTUPINFO结构体的大小(字节数)
GetStartupInfo(&si);//获得STARTUPINFO结构体,存于si中
ZeroMemory(&pi,sizeof(pi)); // 擦去pi的内容(其内存空间清零)
if (bg==0) //前台进程
{
//设置钩子,捕捉组合键Ctrl+C命令,收到即结束进程
if (SetConsoleCtrlHandler((PHANDLER_ROUTINE)ConsoleHandler,TRUE)==FALSE)
{
cout<<"Unable to install handler!\n";
return NULL;
}
//用可执行文件appName创建前台进程。此函数各个参数的介绍请参看实验指导书
CreateProcess(NULL,appName,NULL,NULL,FALSE,0,NULL,NULL,&si,&pi);
return pi.hProcess;
}
else//创建后台进程的要点是:①新进程另开窗口;②其窗口是隐藏的
{
//设置进程窗口选项
si.dwFlags=STARTF_USESHOWWINDOW;
//若不指定此值,则将忽略下一语句的wShowWindow
si.wShowWindow=SW_HIDE; //隐藏窗口
//创建后台进程,执行可执行文件appName
CreateProcess(NULL,appName,NULL,NULL,FALSE,CREATE_NEW_CONSOLE, NULL,NULL,&si,&pi);
return NULL;
}
}
// ************** taskkill命令(撤销进程) ****************
BOOL killProcess(char *pid)
{
int id,i;
DWORD dwExitStatus;
HANDLE hprocess;
id=atoi(pid); // 将进程ID转换成整数
hprocess=OpenProcess(PROCESS_TERMINATE,FALSE,id);//打开进程ID为id的进程,获得其句柄
GetExitCodeProcess(hprocess,&dwExitStatus);//根据刚获得的句柄,获取其退出码存于dwExitStatus
if (i=TerminateProcess(hprocess,dwExitStatus))//终止该进程
return TRUE; //若终止进程成功,返回TRUE
else
return FALSE;
}
// ************* 显示帮助 ***************
void help()
{
cout<<"\ncd:切换当前目录。\n";
cout<<"输入形式:cd ..\n\t cd [drive:] [path] (cd c:\\temp)\n";
cout<<"注:cd命令以空格为分隔符区分命令和参数。\n\n";
cout<<"dir:显示目录中的文件和子目录列表。\n";
cout<<"输入形式:dir\n\t dir [drive:] [path] (dir c:\\temp)\n";
cout<<"注:dir命令以空格为分隔符区分命令和参数。\n\n";
cout<<"tasklist:显示系统中当前进程的信息。\n";
cout<<"输入形式:tasklist\n\n";
cout<<"fp:创建进程并在前台执行。\n";
cout<<"输入形式:fp\n\n";
cout<<"bg&:创建进程并在后台执行。\n";
cout<<"输入形式:bg&\n\n";
cout<<"taskkill:终止进程。\n";
cout<<"输入形式:taskkill [pid]\n";
cout<<"注:taskkill命令以空格为分隔符,pid是进程id。\n\n";
cout<<"history:显示历史命令。\n";
cout<<"输入形式:history\n\n";
cout<<"exit:退出。\n";
cout<<"输入形式:exit\n\n\n\n";
}
9.5.2 程序运行结果示例
以下运行结果,是本实验的程序实际屏幕显示结果复制而来的,并用文本框加注了必要的说明。
F:\1_操作系统\上机实验\课内上机内容\WinShell>dir
驱动器F中的卷是 BACKUP
卷的序列号是 98a4e3f9
F:\1_操作系统\上机实验\课内上机内容\WinShell\ 的目录
2012-05-19 08:33 <DIR> .
2012-05-19 08:33 <DIR> ..
2012-05-19 08:33 <DIR> Debug
2012-05-20 13:30 22262 WinShell.cpp
2012-05-19 08:33 4371 WinShell.dsp
2012-05-19 08:33 541 WinShell.dsw
2012-05-19 20:10 403 WinShell.h
2012-05-19 08:33 50176 WinShell.ncb
2012-05-19 13:18 53760 WinShell.opt
2012-05-19 19:02 1291 WinShell.plg
7 个文件 132804 字节
3 个目录 29916119040 可用字节
37918507008 磁盘总字节
F:\1_操作系统\上机实验\课内上机内容\WinShell>cd ..
F:\1_操作系统\上机实验\课内上机内容>dir
驱动器F中的卷是 BACKUP
卷的序列号是 98a4e3f9
F:\1_操作系统\上机实验\课内上机内容\ 的目录
2012-04-24 19:23 <DIR> .
2012-04-24 19:23 <DIR> ..
2012-05-01 09:54 7337 Banker_OOP.CPP
2012-05-20 00:10 1952 Example.txt
2012-05-19 13:20 411 resource.h
2012-04-26 21:46 <DIR> Schedule
2012-05-19 13:20 1451 Script1.rc
2012-04-28 11:15 6692 VMM_Clock.CPP
2012-04-28 23:24 6319 VMM_FIFO.CPP
2012-04-28 09:33 6421 VMM_LRU.CPP
2012-05-19 08:33 <DIR> WinShell
2012-04-25 12:03 365568 操作系统课内上机指导书2012.doc
8 个文件 396151 字节
4 个目录 29916119040 可用字节
37918507008 磁盘总字节
F:\1_操作系统\上机实验\课内上机内容>cd d:\
d:\>dir c:\
驱动器c中的卷没有标签。
卷的序列号是 a4269ae7
c:\ 的目录
2009-04-22 18:21 1610612736 pagefile.sys
2009-04-22 18:21 <DIR> WINDOWS
2004-08-17 12:00 322730 bootfont.bin
2004-08-17 12:00 257200 ntldr
…… ……
2009-06-21 16:43 0 Recycled
17 个文件 2684743048 字节
10 个目录 3676577792 可用字节
16837099520 磁盘总字节
d:\>cd f:
F:\1_操作系统\上机实验\课内上机内容\WinShell>tasklist
优先级 进程ID 线程 进程名
0 0 2 [System Process]
8 4 71 System
11 536 3 SMSS.EXE
…… …… …… ……
8 768 1 VCSPAWN.EXE
8 3592 1 WinShell.exe
F:\1_操作系统\上机实验\课内上机内容\WinShell>bg& c:\windows\notepad.exe
F:\1_操作系统\上机实验\课内上机内容\WinShell>tasklist
优先级 进程ID 线程 进程名
0 0 2 [System Process]
8 4 71 System
11 536 3 SMSS.EXE
…… …… …… ……
8 768 1 VCSPAWN.EXE
8 3592 1 WinShell.exe
8 2656 1 notepad.exe
F:\1_操作系统\上机实验\课内上机内容\WinShell>taskkill 2656
F:\1_操作系统\上机实验\课内上机内容\WinShell>tasklist
优先级 进程ID 线程 进程名
优先级 进程ID 线程 进程名
0 0 2 [System Process]
8 4 71 System
11 536 3 SMSS.EXE
…… …… …… ……
8 768 1 VCSPAWN.EXE
8 3592 1 WinShell.exe
F:\1_操作系统\上机实验\课内上机内容\WinShell>fp c:\windows\notepad.exe
F:\1_操作系统\上机实验\课内上机内容\WinShell>help
cd:切换当前目录。
输入形式:cd ..
cd [drive:] [path] (cd c:\temp)
注:cd命令以空格为分隔符区分命令和参数。
dir:显示目录中的文件和子目录列表。
输入形式:dir
dir [drive:] [path] (dir c:\temp)
注:dir命令以空格为分隔符区分命令和参数。
tasklist:显示系统中当前进程的信息。
输入形式:tasklist
fp:创建进程并在前台执行。
输入形式:fp
bg&:创建进程并在后台执行。
输入形式:bg&
taskkill:终止进程。
输入形式:taskkill [pid]
注:taskkill命令以空格为分隔符,pid是进程id。
history:显示历史命令。
输入形式:history
exit:退出。
输入形式:exit
F:\1_操作系统\上机实验\课内上机内容\WinShell>history
1 dir
2 cd ..
3 dir
4 cd d:\
5 dir c:\
6 cd f:
7 tasklist
8 bg& c:\windows\notepad.exe
9 tasklist
10 taskkill 2656
11 tasklist
12 fp c:\windows\notepad.exe
13 help
14 history
F:\1_操作系统\上机实验\课内上机内容\WinShell>exit
Bye bye!
【说明】
如果建立的前台进程是控制台进程而不是图形界面进程(记事本进程是图形界面进程),则该前台进程的输入、输出与本命令解释程序合用一个界面(提示符不再显示),此时若键入Ctrl+C组合键,可使该前台进程结束,屏幕继续显示提示符。Ctrl+C组合键并不能中断图形界面进程。
本实验涉及的API所有的几个数据结构介绍请参看“OS试验涉及的API函数中使用的几个数据结构介绍.doc”文件。
参考文献
1. 汤小丹, 汤子瀛等.计算机操作系统(第三版) .西安:西安电子科技大学出版社,2009
2. 任爱华等编著.操作系统实用教程(第三版)实验指导.北京:清华大学出版社,2009
3. 陈向群等.Windows内核实验教程.北京:机械工业出版社,2002
4. 顾宝根等.操作系统实验教程——核心技术与编程实例.北京:科学出版社,2003
5. 朱友芹.新编Windows API参考大全.北京:电子工业出版社,2000
6. Jeffery Richter著,王建华等译.(Programming Application for Microsoft Windows ,(Fourth Edition)) Widows 核心编程.北京:机械工业出版社,2000
7. Andrew S.Tanenbaum著,陈向群等译.(Modern Operating System(Second Edition))现代操作系统.北京:机械工业出版社,2005
【注】实验中大量使用Win32 API,可从网络下载MSDN。MSDN有6.0版、2005版等不同版本,本实验指导使用的是6.0版本(Win32 API:Platform SDK),可从网络下载MSDN Lib VS 6.0或MSDN VC6.0