操作系统实验 订阅
《操作系统实验》是2004年中央电大出版社出版的图书,作者是孟庆昌。 展开全文
《操作系统实验》是2004年中央电大出版社出版的图书,作者是孟庆昌。
信息
页    数
93
作    者
孟庆昌
定    价
12.00元
书    名
操作系统实验
出版时间
2004-7
出版社
中央电大
副标题
操作系统实验
ISBN
9787304018801
接单内容简介
内容介绍《计算机应用专业系列教材·操作系统实验》以SCO UNIX为例介绍了操作系统的安装和使用,通过指导读者上机实验,使读者既加深对操作系统概念的理解,又学会对UNIX系统的一般使用。全书共分九章,内容包括:UNIX系统安装,SCO UNIX图形界面,用户帐户建立,vi使用,进程管理,存储器管理,文件系统,设备管理和简单shell编程。《计算机应用专业系列教材·操作系统实验》是中央电大的实验教材,同时也可作为广大UNIX初学者的指导书。 [1] 
收起全文
精华内容
参与话题
问答
  • 操作系统实验

    千次阅读 2018-03-14 12:54:22
    是 “操作系统原理”课内上机实验指导书 适用专业:计算机科学与技术计算机软件技术 电子与信息工程学院计算机系2012年6月前 言操作系统原理是计算机专业本科学生的必修课程,它具有较强的理论性和实践性。...

     

     

    “操作系统原理”课内上机实验指导书

     

    适用专业:计算机科学与技术

    计算机软件技术

     

     

     

     

     

     

     

     

    电子与信息工程学院计算机系

    20126


     

    操作系统原理是计算机专业本科学生的必修课程,它具有较强的理论性和实践性。为了强化实践教学环节,培养学生的实践能力,促进理论与实践的结合,在30学时独立开设的操作系统原理上机实验的基础上,我们又增加了8学时的课内实验。本实验指导通过Windows API提供一些编程实例,使学生熟悉对Windows操作系统编程接口的使用,并了解如何模拟操作系统原理的实现,加深对操作系统设计原理和实现方法的理解,使学生在程序设计和团队合作方面得到基本的训练。

    本上机实验指导从操作系统基本原理出发,提供了不同类型的上机实验题,每个实验题都有参考源程序代码,同时对实验题的设计进行了必要的讲解和指导。在这些上机实验中,重点放在对Windows的应用程序接口API的使用上。利用这些与操作系统直接相关的API,编写一些实践操作系统基本概念和基本原理的实例,便于学生对抽象概念的理解和感性化;通过阅读本实验指导书提供的实例程序代码,能使学生得到编程方面的体验和训练。

    为了能比较全面覆盖操作系统课程的知识点,本实验指导书提供了九个上机实验,具体包括如下五个方面的内容:

    1. 进程管理:包括“上机实验一 进程的创建”、“上机实验二 线程的创建和并发执行”、“上机实验三 进程同步”和“上机实验四 进程通信”。受课内实验时间的限制,对上述四个题目,每个学生一般可选一个实验题目进行上机实验。

    2. 进程调度和死锁:包括“上机实验五 进程调度”和“上机实验六 演示银行家算法”。每个学生可任选一题,建议选做进程调度的题目。

    3. 存储器管理:包括“上机实验七 模拟页面置换算法”。参考程序提供的是FIFO页面置换算法,有兴趣的学生可以编程实现LRUClock算法。

    4. 设备管理:包括“上机实验八 磁盘I/O”。

    5. 用户操作接口:包括“上机实验九 命令解释程序”。

    以上5种类型的九个实验题目共22学时(包括每个实验项目的基本要求各2学时,共18学时;“进程调度”和“页面置换算法”的进阶要求各2学时),每个学生可任选4(每种类型限选1),共完成8学时的课内实验计划。由于操作系统原理课程独立开设的30学时的实验内容是文件系统的设计,故文件系统的实验没有包含在本上机实验指导书中。

    本上机实验指导的“进程调度”、“磁盘I/O”和“命令解释程序”等实验,主要参考了任爱华等编著的《操作系统实用教程(第三版)实验指导》,但都作了重大修改或重新设计。“进程的创建”、“线程的创建和并发执行”利用MSDN(Microsoft Developet Network)提供的函数CreateProcessCreateThread实现的;“进程同步”、“进程通信”、“银行家算法”、“页面置换算法”等实验是编者独立设计的,其中“进程同步”所用的信号量对象和“进程通信”所用的Pipe(管道)技术参阅了MSDN的相关资料。

    本上机实验指导涉及的Win32 API函数、数据结构和数据类型,可参阅配套的Word文档“OS课内上机实验使用的相关API函数介绍.doc”、“OS实验涉及的API函数中使用的几个数据结构介绍.doc”和“Win32 Simple Data Types.doc”。

    限于编者的水平和编写时间仓促,本实验指导一定有许多错误和不妥之处,恳请读者批评指正。

     

                                                           编者

                                                            20126


     

    上机实验一  进程的创建 (2学时) 1

    1.1  上机实验要求 1

    1.2  设计方案介绍 1

    1.2.1  程序的总体框架 1

    1.2.2  程序所用Win32 API函数介绍 2

    1.3  源程序代码与运行结果分析 5

    1.3.1  程序参考源代码 5

    1.3.2  程序运行结果与分析 7

    上机实验二  线程的创建和并发执行 (2学时) 9

    2.1  上机实验要求 9

    2.2  设计方案介绍 9

    2.2.1  程序总体框架 9

    2.2.2  程序所用API函数介绍 9

    2.3  程序源代码和运行结果 11

    2.3.1  程序源代码 11

    2.3.2  运行结果和分析 14

    2.4  进一步要求 17

    上机实验三  进程同步(2学时) 19

    3.1  上机实验要求和目的 19

    3.2  Windows2000/XP的同步和互斥机制 19

    3.3  设计方案介绍 20

    3.3.1  数据结构 20

    3.3.2  程序的总体设计思想 21

    3.3.3  程序中所用API函数介绍 21

    3.4  源程序与运行结果 23

    3.4.1  程序源代码 23

    3.4.2  程序运行输出结果 26

    上机实验四  进程通信 (2学时) 30

    4.1  上机实验内容和要求 30

    4.2  相关知识 30

    4.2.1  Windows中的pipe 30

    4.2.2  实验所用的几个Win32 API函数介绍 30

    4.3  实验总体设计 33

    4.4  源程序与运行结果 33

    4.4.1  程序源代码 33

    4.4.2  程序运行结果 43

    上机实验五  进程调度 (4学时) 44

    5.1  上机实验基本要求 (2学时) 44

    5.2  相关知识 44

    5.2.1  Windows中的进程和线程 44

    5.2.2  相关线程的几个Win32 API函数介绍 45

    5.3  实验设计 46

    5.3.1  重要的数据结构 46

    5.3.2  程序实现 47

    5.4  源程序与运行结果 49

    5.4.1  程序源代码 49

    5.4.2  程序运行结果 61

    5.5  进一步要求 (2学时) 62

    上机实验六  演示银行家算法 (2学时) 63

    6.1  上机实验要求 63

    6.2  实验设计 63

    6.2.1  数据结构 63

    6.2.2  程序实现 63

    6.3  源程序与运行结果 64

    上机实验七  模拟页面置换算法 (4学时) 72

    7.1  上机实验基本要求(2学时) 72

    7.2  实验设计 72

    7.2.1  数据结构 72

    7.2.2  程序实现 72

    7.3  源程序与运行结果 73

    7.3.1  参考程序源代码 73

    7.3.2  程序运行结果 77

    7.4  进一步要求 (2学时) 79

    上机实验八  设备管理——磁盘I/O (2学时) 80

    8.1  上机实验要求和目的 80

    8.2  设计方案介绍 80

    8.2.1  程序的总体框架 80

    8.2.2  数据结构和程序中用到的API函数介绍 80

    8.3  源程序和运行结果 85

    8.3.1  源程序代码 85

    8.3.2  程序的运行结果解释(操作说明) 94

    上机实验九  命令解释程序 (2学时) 98

    9.1  上机实验目的 98

    9.2  上机实验要求 98

    9.3  相关基础知识 99

    9.3.1  命令解释程序与操作系统内核的关系 99

    9.3.2  系统调用及Win32 API相关函数介绍 99

    9.4  实验设计 103

    9.4.1  重要的数据结构 103

    9.4.2  程序实现 103

    9.5  源程序与运行结果 104

    9.5.1  程序源代码 104

    9.5.2  程序运行结果示例 117

    参考文献 121

     


    上机实验一  进程的创建 (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.exeVC++源代码如下:

    #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 APIWin32 API。所有在Win32平台上运行的应用程序都可以调用这些函数。

    使用Windows API,应用程序可以充分挖掘Windows32位操作系统潜力。Microsoft的所有32位平台都支持统一的API

    Windows的相关API的说明都可以在MSDN(Microsoft Developet Network)中查到,包括定义、使用方法等。下面简单介绍本实验中涉及的Windows API

    1GetLastError( )函数

    功能:返回最近一次发生的错误代码。

    格式:DWORD GetLastError(VOID)

    【注】此处的DWORD就是unsugned longVOID就是void。要更多地了解Win32 API中的数据类型,可参阅配套文档“Win32 Simple Data Types.doc”。

    2FormatMessage( )函数

    功能:生成具体的出错信息。在本实验程序中,用它将由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_SYSTEMFORMAT_MESSAGE_FROM_HMODVLEFORMAT _ MESSAGE_FROM_STRING:前者表明利用错误代码dwMessageId到系统消息定义表去找对应的出错信息,它可以和FORMAT_MESSAGE_FROM_HMODVLE合用,表示先到该模块中定义的消息表查找对应消息,找不到再转向系统消息定义表,但二者不能和FORMAT_MESSAGE_FROM_STRING合用,后者表示表示消息定义在由lpSource指针指向的串中。

    n FORMAT_MESSAGE_IGNORE_INSERT:指出在消息定义中的插入值在输出时不被处理,此时后面的ARGUMENTS参数被忽略;除了上面提到的功能,这个参数还可以规定输出消息的行宽格式。

    lpSource:只有上一参数含FORMAT_MESSAGE_FROM_HMODVLEFORMAT_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

    3CreateProcess( )函数

    功能:创建一个新进程和它的主线程,这个新进程运行指定的可执行文件。

    格式

    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-DOSOS/2下的模块,建议带上扩展名和使用全路径名。

    lpCommandLine:指向一个指定要执行的命令行字符串。这个参数可以为NULL,但它不能和第一个参数同时为空。如果lpApplicationNamelpCommandLine都不为空,那么lpApplicationName指定将要运行的模块,lpCommandLine参数指定将要被运行的模块的命令行。

    lpProcessAttributesSECURITY_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。通过获得的进程信息即可对该进程进行进一步操作。本程序未使用这个结果。

    4GetForegroundWindow( )函数

    功能:获取当前活动窗口的句柄。

    格式

    HWND GetForegroundWindow(void)

    5MoveWindow( )函数

    功能:改变指定窗口的位置和大小。

    格式

    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,dwYdwXSize,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”创建的进程窗口,下面两个黑底窗口分别是第23命令行创建的进程窗口。出错信息框(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函数介绍

    1CreateThread( )函数

    功能:创建一个新线程,返回与线程相关的句柄。若调用失败,则返回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标准,例如其返回值类型是普通的voidint等类型,则必须在线程函数名前加强制类型转换运算符(LPTHREAD_START_ROUTINE)。

    pvparam:指定一个32-bit的参数传递给新创建的线程,它对应于线程要执行的函数的参数。该参数必须是LPVOID(void *)类型的。【注】为使主线程能传递多个值给子线程,可以用结构型变量(或对象)

    dwcreationFlags:指定线程的初始状态,0表示运行态,CREATE_SUSPEND表示挂起(暂停)状态。

    pdwthreadId:指向32-bit变量的指针,该变量接收新线程的标识号。在Windows 2000/XP中可以设置为NULL,但在Windows 95/98中必须设置为DWORD的有效地址。

     2GetSystemTime( )函数

    功能:获得系统时间(时、分、秒等),它是格林威治时间,它通过参数返回的是SYSTEMTIME结构型数据。

    格式

    VOID GetSystemTime(

      LPSYSTEMTIME lpst  // LPSYSTEMTIME是指向SYSTEMTIME结构的指针类型

    ) ;

    参数说明:lpst是指针型参数,用于接收函数返回的系统时间。

    LPSYSTEMTIME结构的成员请参看Word文档“OS试验涉及的API函数中使用的几个数据结构介绍.doc”的介绍。

    3InitializeCriticalSection( )函数

    功能:初始化临界区对象(相当于信号量初始化)。临界区对象定义后需调用此函数进行初始化。

    格式

    InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

    参数说明:

    lpCriticalSection:指向临界区对象(CRITICAL_SECTION类型对象)的指针。

    4EnterCriticalSection函数

    功能:等待指定临界区对象的所有权。当对象被赋予所有权时,该函数返回。此函数用作访问临界区的“进入代码”,相当于信号量的wait操作。

    格式

    void EnterCriticalSection (LPCRITICAL_SECTION lpCriticalSection );

    参数说明:

    lpCriticalSection:指向临界区对象(CRITICAL_SECTION类型对象)的指针。

    5LeaveCriticalSection函数

    功能:释放指定临界区对象的所有权。此函数用作离开临界区的“退出代码”,相当于信号量的signal操作。

    格式

    void LeaveCriticalSection (LPCRITICAL_SECTION lpCriticalSection );

    参数说明:lpCriticalSection:指向临界区对象的指针。

    6Sleep( )函数

    功能:当前线程休眠(挂起suspend)一段时间,时间长短由函数参数指定。

    格式

    void Sleep ( DWORD dwMilliseconds );

    参数说明

    dwMilliseconds:休眠的时间,以ms为单位。

    7ExitThread ( )函数 ——本程序没有使用此函数

    功能:强行退出(主动结束)线程。【注】线程函数运行结束执行返回语句时也会退出线程。

    格式

    VOID ExitThread( DWORD dwExitCode );   // exit code for this thread

    参数说明

    dwExitCode:此线程的退出码。可以调用GetExitCodeThread( )获得该线程的退出码。

    8GetExitCodeThread ( )函数

    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; // 线程的生命周期()WORD32位有符号整型,即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)操作(原语)

    【注意 上述3API函数的参数类型都是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]; //线程idDWORD即是unsigned long

    而后根据输入的线程数iThread,通过循环对sthread[i]的各数据成员赋初值,其中各sthread[i]的成员runTime允许赋不同的值;成员sthread[i].runFlag赋初值1sthread[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问题的同步算法,需要解决多个应用程序之间共享变量(即共享存储区)的问题,包括共享循环缓冲区、缓冲区指针inout、缓冲区的个数n、信号量empty,full,mutex。共享信号量问题比较简单(Win32 API中,semaphore对象本身是可以供多个application共享的),共享变量(存储区)比较复杂,它实际上属于进程通信问题。解决共享变量最简单的方法之一是采用File Mapping(参看文件夹P_C2中的程序Monitor.cppProducer.cppConsumer.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分别创建三个信号量对象,其初始值分别是1100;生产者和消费者进程(线程)调用WaitForSingleObject作为临界区的进入区代码(相当于wait操作),调用releaseSemaphore作为临界区的退出区代码(相当于signal操作)。详见源程序。

    3事件对象Event相当于触发器,可用于通知一个或多个线程某事件的出现,它的相关API包括:

    · CreateEvent  创建一个事件对象,返回对象句柄。

    · OpenEvent  打开并返回一个已存在的事件对象句柄,用于后续访问。

    · SetEventPluseEvent  设置指定事件对象的可用状态。

    · ResetEvent  设置指定事件对象为不可用状态。

    对这三种同步对象,系统提供了两个同一的等待操作WaitForSingleobjectWaitForMultipleObject。前者可在指定时间内等待指定对象为可用状态;后者可在指定时间内等待多个对象为可用状态。

    除了上述三种同步对象外,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个对象的句柄分别是emptyfullmutexempty所指的信号量对象的初值为10(即循环缓冲的缓冲区个数)full所指的信号量对象的初值为0,即初始状态,满缓冲区个数为0mutex所指的信号量对象的初值为1,用于互斥访问共享变量inout。关于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对象或用设置初值为1Semaphore对象来实现对屏幕的互斥访问,就像本程序中的mutex那样。CRITICAL_SECTION 对象只用于互斥,不能用于同步,且只能用于同一进程的各线程之间的互斥。

    3.3.3  程序中所用API函数介绍

    1CreateSemaphore( )函数

    功能:

    创建一个有名字的或无名字的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( )

    2WaitForSingleObject( )函数

    功能:等待一个事件信号直至信号出现或者超时。若等到信号则返回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( )函数才唤醒队列首部的一个线程。同样,也可对emptymutex执行此函数,其意义相同。

    3ReleaseSemaphore( )函数

    功能:使指定的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( )创建对象时返回。

    lReleaseCountcount的增量,本程序使用的增量为1

    lpPreviousCountcount原先值的地址。本程序使用NULL,表示不关心count的原先值。

    返回值:函数执行成功,返回非0值;执行失败,返回0.

    4CreateThread( )函数

    此创建线程函数,参见2.2.2节的介绍

    5.有关临界区(CRITICAL_SECTION)的函数

    InitializeCriticalSection( )EnterCriticalSection( )LeaveCriticalSection( ),参看2.2.2节。

    6Sleep( )函数

    参看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); //mutexcount1

    ReleaseSemaphore(empty,1,NULL); //emptycount1

    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;

    }

    2P_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(); //bdata成员用随机数演示

    b.Number=ThreadNumber; //bNumber成员值是上缠着线程的序号

    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 ExchangeDDE)、文件映射(File Mapping)、邮件槽(Mailslot)、管道(Pipes)、远程过程调用(RPC)Windows套接字(Windows Sockets)等。本实验采用相对比较容易实现的有名管道(Named Pipe)实现进程间的通信。学生也可采用File MappingMailslot自行设计本实验。File Mapping的使用可参考P_C2文件夹中程序,该程序就是使用File Mapping作为进程通信工具来实现诸进程共享循环缓冲区和缓冲区指针inout等的共享的,再结合信号量对象(semaphore object)来实现生产者-消费者问题的同步算法的。另外,采用Mailslot实现进程通信的程序在文件夹Mailslot中。邮件槽是一种不定长和不可靠的单向消息通信机制。Named PipeFile 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)来读写管道,从而实现与服务器进程的通信。注:上述ReadFileWriteFile是阻塞方式工作的,也可使用非阻塞方式的ReadFileExWriteFileEx来读写管道。

    4.2.2  实验所用的几个Win32 API函数介绍

    Windows 2000/XP系统中,要使用管道实现通信,都需要通过Win32 API调用来完成。本实验涉及的API函数如下:

    1CreateNamedPipe函数

    功能

    创建一个有名管道实例(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表示缺省的安全属性,以及返回的句柄不能被继承。

    2ConnectNamedPipe函数

    功能

    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,则此函数返回0GetLastError函数返回ERROR_PIPE_CONNECTED。如果一个客户在服务器调用CreateNamedPipe至调用ConnectNamedPipe时间之内连接管道,则这种情况就会发生。在这种情况下,虽然此函数返回0,但服务器和客户之间已建立了一个好的连接。即客户和服务器之间连接成功有两种情况:一是此函数返回非0值;二是此函数虽然返回0,但GetLastError函数返回ERROR_PIPE_CONNECTED。具体处理请参看本实验的参考程序。

    3WaitNamedPipe函数

    功能

    调用进程(Named-Pipe Client)一直等待,直到给定时间超时,或者指定的named pipe可以被连接(pipe服务器进程执行了ConnectNamedPipe操作)

    格式

    BOOL WaitNamedPipe(

      LPCTSTR lpNamedPipeName,  // pipe name

      DWORD nTimeOut            // time-out interval

    );

    参数说明:

    lpNamedPipeName:命名管道名字。

    nTimeOut:指定超时的毫秒数。

    【注】若指定名字的管道不存在,则此函数立即返回(并不等待超时)

    4SetNamedPipeHandleState函数

    功能

    设置指定管道的读模式和阻塞模式(设置/改变管道的新模式)

    格式

    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

    5WriteFile函数

    该函数已在“上机实验八 设备管理——磁盘I/O”实验中作了简单介绍,这里做一些补充:

    当用此函数写一个管道时,如果管道缓冲区(其大小由调用CreateNamedPipe函数指定)已写满但仍有要写的数据,则写操作可能不能完成(WriteFile函数不能返回)。但读端进程调用ReadFile读管道而使管道缓冲区空出更多可用空间时,最终能使写操作完成(即能使WriteFile函数返回)

    6ReadFile函数

    该函数已在“上机实验八 设备管理——磁盘I/O”实验中作了简单介绍,这里做一些补充:

    (1) 该函数在出现下列情况之一才会返回:管道(pipe)写端的写操作已经完成;已经读到函数所请求读的字节数的信息;出现一个错误。因此,对于管道而言,当写端进程没有写完全部数据时,读端进程的读不会结束(阻塞方式),这在某种程度上起到了同步作用。

    (2) 当一个命名管道的读模式设置成PIPE_READMODE_MESSAGE,而所读的消息长度大于此函数所指定要读的字节数时,则ReadFile函数出错返回FALSE,此时调用GetLastError函数,获得的出错代号为ERROR_MORE_DATA。这种情况实际上数据已经正确读入,只是没有读完而已,可继续读。具体请看本实验的源程序。

    7DisconnectNamedPipe函数

    功能:用于在named pipe服务器端断开与管道客户进程的连接。

    格式

    BOOL DisconnectNamedPipe(

      HANDLE hNamedPipe   // handle to named pipe

    );

    8FlushFileBuffers函数

    功能:将指定文件的缓冲数据写入磁盘并请缓冲区。

    格式

    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 serverNamed pipe client

    服务器设计成多线程管道服务器,服务器主线程的总体框架是:

    1.首先创建一个工作线程(WorkThread),它的任务是创建若干个进程(pipe client)

    2.主线程开始无限循环:调用CreateNamedPipe创建一个有名管道实例;调用ConnectNamedPipe等待管道客户进程请求服务;若有客户进程请求管道连接,则创建一个子线程来为该客户提供服务(与该客户实现管道双向通信);主线程返回循环开头。

    3.若没有客户请求连接,则关闭刚创建的管道实例(named pipe instance)的句柄后重新循环。

    管道客户程序的总体框架是:

    1.调用CreateFile函数获得服务器创建的管道实例的句柄;

    2.调用WaitNamedPipe函数,等待(请求)与服务器的管道连接;

    3.若连接成功,如需要可则调用SetNamedPipeHandleState按需要设置管道的读模式等;

    4.执行如下的循环操作:

    (1) 调用WriteFile函数向服务器发送请求信息;

    (2) 等待并接收服务器的应答信息(ReadFile函数实现)。这样循环往复,实现与服务器的双向通信,直到完成通信任务后结束进程。

    服务器创建的为客户提供服务的子线程也是用ReadFileWriteFile函数实现与客户进程的管道通信的,此处不再赘述。

    4.4  源程序与运行结果

    4.4.1  程序源代码

    1Server程序源代码

    #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);

    }

    2Client.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

    该命令打印(显示)出当前运行进程和就绪队列中各进程的信息,状态信息应包括:

    进程的PID

    l 进程名字

    进程状态(RUNREADYWAIT

    (5) 实现时间片轮转调度算法

    处理机总是优先调度运行就绪队列中的第一个进程,当时间片结束后就把该进程放在就绪队列的尾部。在系统的实现以及运行中,不必考虑Windows操作系统本身的进程调度。假设所有的作业均由scheduleProcess调度执行,而且进程在分配给它的时间片内总是不间断地运行。

    5.2  相关知识

    5.2.1  Windows中的进程和线程

    Windows 2000/XP中,每个进程至少包含一个线程,进程均由一个线程启动,该线程称为主线程,例如,我们用VC++语言编写了一个程序,编译运行后就是一个进程,该进程的主线程运行main函数的代码。进程(主线程)可以动态创建新的线程(子线程)。进程和线程均可以通过Win32 API函数CreateProcessCreateThread动态创建(详见上机实验二和上机实验三)

    线程通常在用户态下运行,当它进行系统调用时,会切换到核心态运行,并继续作为同一线程并具有用户态下相同的属性和限制。当一个线程执行完毕时,它可以退出。当进程的最后一个线程退出时,该进程终止。

    线程是一个调度的概念而不是一个占有资源的概念。任何一个线程可以访问它所属进程的所有对象(资源),它所要做的只是获得对象句柄并做适当的Win32调用。

    由于Windows 2000/XP中,调度的对象是线程而不是进程,所以每个线程具有一个状态(就绪、运行、阻塞等),但是进程没有这些状态。线程构成了CPU调度的基础,因为操作系统会选择一个线程运行而不是一个进程。因此,Win32 API中有关于线程操作的相关函数,包括:线程的暂停(挂起)、线程的回复(唤醒)、线程的终止等等,利用这些操作,我们可以较方便地模拟进程调度的过程。设计本上机实验的最主要思路,就是用线程模拟进程。然后对进程(实际上是对线程)进行调度演示的。

    5.2.2  相关线程的几个Win32 API函数介绍

    Windows 2000/XP系统中,要进行进程/线程的创建、撤销和调度等操作,都需要通过Win32 API调用来完成。涉及的API如下:

    1CreateThread( )函数

    该函数的介绍参见2.2.2节。

    2SuspendThread( )函数

    功能

    挂起(暂停)线程。被挂起的线程将暂停执行,直到被回复(唤醒)为止。

    格式DWORD SuspendThread ( HANDLE hthread )

    参数说明:hthread:线程的句柄。该句柄在调用CreateThread时获得。

    3ResumeThread( )函数

    功能

    恢复(唤醒)线程。被挂起的线程恢复后才能为其分配CPU。该函数正确执行则返回一正整数,否则返回0xFFFFFFFF(-1)

    格式DWORD ResumeThread ( HANDLE hthread )

    参数说明:hthread:线程的句柄。

    4TerminateThread( )函数

    功能:终止线程运行。

    格式BOOL TerminateThread ( HANDLE hthread, DWORD dwExitCode )

    参数说明

    hthread:线程的句柄。

    dwExitCode:线程终止时的退出代码(返回值)

    TerminateThread终止线程时,该线程的初始堆栈未被回收,依附于该线程的DDLs也未被告知线程已经结束。TerminateThread是一个危险的函数,该函数仅仅在最极端的情况下使用。只有当你确切地知道目标线程正在工作以及在终止时能控制该线程所有可能正在运行的代码时才能够使用此函数。例如,TerminateThread可能导致如下问题:

    · 如果目标线程拥有一个临界区,该临界区将不被释放。

    · 如果目标线程被终止时正在执行某些kernel32调用,则将导致该线程进程的kernel32状态的不一致性。

    · 如果目标线程正在操纵共享的DDL的全局状态,则DDL的状态将被破坏从而影响使用DDL的其他用户。

    5Sleep函数

    该函数的介绍参见2.2.2节。

    6CloseHandle函数

    功能:关闭内核对象(线程)的句柄,将对象引用计数减1,或者释放堆栈资源。

    格式BOOL CloseHandle (HANDLE hobj )

    参数说明:hobj:对象的句柄。

     

    多个线程操作相同的数据时,一般是需要按顺序访问的(互斥访问),否则会引起数据错乱,使其无法控制数据。为了解决这个问题,需引入互斥变量,让这些线程都按顺序地访问变量(程序中是指互斥访问链队列这类临界资源)。这样就需要使用如下三个函数。

    7InitializeCriticalSection( ) 函数

    该函数的介绍参见2.2.2节。

    8EnterCriticalSection( ) 函数

    该函数的介绍参见2.2.2节。

    9LeaveCriticalSection ( ) 函数

    该函数的介绍参见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(进程标识号)、进程名字、进程状态(有三种:RUNREADYWAIT)、操作进程的句柄和进程剩余时间长度等。若采用优先级调度,还需要有优先数等。

    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.命令处理函数

     

    命令处理函数分别处理createremovecurrent三种命令。该函数比较简单,具体过程如图5-3所示。

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    5.4  源程序与运行结果

    5.4.1  程序源代码

    1schedule.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

    2schedule.cpp文件

    #include "schedule.h"

    readyList readylist; //定义就绪队列的管理结构

    pList pReadyList=&readylist; //定义该结构的指针

    freeList freelist; //定义空闲PCB队列的管理结构

    pList pFreeList=&freelist; //定义该结构的指针

    PCB pcb[PCB_LIMIT]; //定义30PCB

    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; //PCBnext指针

    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的链接指针

    //若就绪队列空,则新PCBReadyList的第一个结点

    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;

    }

    }

    3main.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,其资源种类数和进程数分别为35

    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

     

    输入可用资源向量Available3 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()返回197011UTC时间000秒开始至今的秒数。

    3处理访存请的函数response( )

    处理访存请的函数response( )完成的工作包括:分解虚地址为页号和页内偏移、判断是否地址越界、查找页表、处理缺页中断、必要时调用页面置换程序进行页面置换、修改页表、实现地址重定位等。简易流程图如图7-2所示。

     

     

    4进程休眠

    在主函数中,每次产生和相应访存请求后,要使进程休眠一段时间,从而模拟用户程序访存的不定时性。这里用到WindowsAPI函数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.还可考虑用面向对象的方法(即采用类和对象的方法)设计程序。(选做内容)

    说明】要实现LRUClock算法的页面置换演示,只需修改程序中的response( )函数和页面置换函数的相关程序段(当然函数名也应把FIFO改为LRUClock)


    上机实验八  设备管理——磁盘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 ; //Diskdisk结构的指针

    结构的主要成员的说明:

    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:成功写入的字节数,由该参数返回。

    lpOverlappedOVERLAPPED结构体指针,本程序取值为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:成功读入的字节数,由该参数返回。

    lpOverlappedOVERLAPPED结构体指针,本程序取值为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_CURRENTFILE_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  源程序代码

    参考源代码如下:

    1Disk.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); //扫描驱动器,寻找可移动磁盘

    2Disk.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,但有极少数是10242048字节。

     

     

     

    (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 掌握命令解释程序的设计方法。

    学习Windows系统调用的使用,了解目录操作、进程控制等相关知识。

    培养C/C++语言程序设计技能,提高程序设计和文档编写能力。

    l 锻炼团队成员的交流与合作能力。

    9.2  上机实验要求

    本实验要求实现一个简单的命令解释程序,其设计类似于MS-DOSCommand程序。具体要求如下:

    (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 APIWin32 API。所有在Win32平台上运行的应用程序都可以调用这些函数。

    使用Windows API,应用程序可以充分挖掘Windows32位操作系统潜力。Microsoft的所有32位平台都支持统一的API

    Windows的相关API的说明都可以在MSDN(Microsoft Developet Network)中查到,包括定义、使用方法等。下面简单介绍本实验中涉及的Windows API

    1GetCurrentDirectory函数

    功能:查找当前进程的当前目录,调用成功,返回装载到lpBuffer的字节数。失败则返回0

    格式

    DWORD GetCurrentDirectory ( //DWORD就是unsigned long

    DWORD nBufferLength, // 缓冲区的长度

    LPTSTR lpBuffer // 指定一个预定义字串,用于装载当前目录

    ) //LPSTR:是指向字符串的指针的类型名,即char *

    【注】API中涉及的类型名,请参阅配套文档“Win32 Simple Data Types.doc”。

    2WaitForSingleObject函数

    功能:等待一个事件信号直至信号出现或者超时。若等到信号则返回WAIT_OBJECT_0(0),若等待超过dwMiliseconds时间还是无信号,则返回WAIT_TIMEOUT(258)。若函数调用失败,则返回WAIT_FAILED (-1)

    格式

    DWORD WaitForSingleObject (

    HANDLE hHandle, // 事件的句柄

    DWORD dwMilliseconds // 最大等待时间,以ms计时。

    )

    3SetCurrentDirectory函数

    功能:设置当前目录。返回非0表示成功,返回0表示失败。

    格式

    BOOL SetCurrentDirectory (

    LPCTSTR lpPathName // 新设置的当前目录路径

    )

    4FindFirstFile函数

    功能:用于从一个文件夹(包括子文件夹)中查找指定文件,返回找到的文件句柄。若调用失败,则返回INVALID_HANDLE_VALUE (-1)

    格式

    HANDLE  FindFirstFile (

    LPCTSTR  lpFileName, // 文件名字符串(可用通配符)

    LPWIN32_FIND_DATA lpFindFileData // 指向一个用于保护文件的结构体

    )

    【注】WIN32_FIND_DATA结构的说明请参看MSDN或本上机实验指导的配套文档。

    5FindNextFile函数

    功能:继续查找FindFirstFile函数搜索后的文件。它返回的文件句柄可以作为参数用于FindNextFile函数。这样就可方便地枚举出与lpFileName参数指定的文件名相匹配的所有文件。调用失败,返回0

    格式

    HANDLE  FindNextFile (

    HANDLE  hFindFile, // 前一个搜素到的文件句柄

    LPWIN32_FIND_DATA lpFindFileData // 指向一个用于保护文件的结构体

    )

    6GetVolumeInformation函数

    功能:用于获取磁盘相关信息。执行成功返回非0;失败,返回0

    格式

    BOOL  GetVolumeInformation (

    LPCTSTR  lpRootPathName, // 磁盘驱动器代码字符串(具体构成方法参看程序)

    LPCTSTR  lpVolumeNameBuffer, // 磁盘驱动器卷标名称

    DWORD  nVolumeNameSize, // 磁盘驱动器卷标名称长度

    LPWORD lpVolumeSerialNumber, // 磁盘驱动器卷标序列号

    LPWORD lpMaximunComponentLength, //系统允许的最大文件长度

    LPWORD lpFileSystemFlags, // 文件系统标识

    LPCTSTR lpFileSystemNameBuffer, // 文件系统名称

    DWORD nFileSystemNameSize // 文件系统名称长度

    )

    7GetDiskFreeSppaceEx函数

    功能:获取与一个磁盘的组织以及剩余容量有关的信息。调用失败返回0

    格式

    HANDLE GetDiskFreeSppaceEx (

    LPCTSTR  lpRootPathName, // 不包括卷名的磁盘根路径名

    PULARGE_INTEGER lpFreeBytesAvailableToCaller, // 调用者可用的字节数

    PULARGE_INTEGER lpTotalNumberOfBytes, // 磁盘上的总字节数

    PULARGE_INTEGER lpTotalNumberOfFreeBytes // 磁盘上的可用字节数

    )

    参数说明:lpRootPathName:根路径名。例如形式为"C:\\"。使用NULL表示函数使用当前目录所在的磁盘。

    8FileTimeToLocalFileTime函数

    功能:将一个FILETIME结构转换成本地时间。

    格式

    BOOL  FileTimeToLocalFileTime (

    const FILETIME* lpFileTime, // 指向一个包含了UTC时间信息的结构

    LPFILETIME  lpLocalFileTime // 用于装载转换过的本地时间的结构体

    )

    9FileTimeToSystemTime函数

    功能:根据一个FILETIME结构的内容,装载一个SYSTENTIME结构。

    格式

    BOOL  FileTimeToSystemTime (

    const FILETIME* lpFileTime, // 指向一个包含了文件时间信息的结构

    LPFILETIME  lpSystemTime // 用于装载系统时间的结构体

    )

    10CreateToolhelp32Snapshot函数

    功能:为指定的进程、进程使用的堆(heap)、模块(module)、线程(thread)建立一个快照(snapshot)。快照建立成功则返回快照的句柄,失败则返回INVALID_HANDL_VALUE

    格式

    HANDLE WINAPI  CreateToolhelp32Snapshot (

    DWORD dwFlags, // 指向快照中包含的系统内容

    DWORD th32ProcessID // 指定将要快照的进程ID

    )

    11Process32First函数

    功能:是一个进程获取函数,当使用CreateToolhelp32Snapshot()函数获得当前运行进程的快照后,可以使用Process32First ( )函数获得第一个进程的句柄。

    格式

    BOOL WINAPI  Process32First (

    HANDLE hSnapshot, // 快照句柄

    LPPROCESSENTRY32 lppe // 指向一个保护进程快照信息的LPPROCESSENTRY32结构

    )

    12Process32Next函数

    功能:获取快照中下一个进程信息。

    格式

    BOOL WINAPI  Process32Next (

    HANDLE hSnapshot, // Process32First或Process32Next函数获得的快照句柄

    LPPROCESSENTRY32 lppe // 指向一个保护进程快照信息的LPPROCESSENTRY32结构

    )

    13OpenProcess函数

    功能:该函数打开一个已经存在的进程对象,若成功,返回值是指定进程的打开句柄。若失败,则返回空值。

    格式

    HANDLE  OpenProcesst (

    DWORD  dwDesiredAccess, // 权限标识(详见MSDN

    BOOL bInheritHandle, // 指出返回的句柄是否能被当前进程创建的新进程继承,

    // TRUE表示可继承,FALSE表示不能继承。

    DWORD dwProcessID // 进程ID

    )

    14SetConsoleCtrlHandler函数

    功能:添加或删除一个事件钩子(Handler)

    格式

    BOOL  SetConsoleCtrlHandler (

    PHANDLER_ROUTINE HandlerRoutine, // 回调函数的指针

    BOOL Add // 表示添加或删除

    )

    15CreateProcess函数

    此函数已经在前面介绍,请参阅“上机实验一”的1.2.2节,在此不再赘述。

    16GetExitCodeProcess函数

    功能:获取一个已中断进程的退出代码。

    格式

    BOOL  GetExitCodeProcess (

    HANDLE hProcess, // 进程句柄

    LPDWORD lpExitCode // 指向接受退出码的变量

    )

    17TerminateProcess函数

    功能:以给定的退出码终止进程。

    格式

    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  程序源代码

    1WinShell.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; // 构成链表的链接指针

    } ;

    2WinShell.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()的前面,原因可能是它们的参数类型分别是FILETIMEDWORD

     

    // ***************** 时间处理函数 ******************

    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

    // 若数据属性是目录,则置type0

    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);//pid0表示任意进出

    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;// end1,准备将新的命令行插入队列

    // endstart指向同一数组

    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+1end的命令

    for (i=envhis.start+1;i<=envhis.end;i++)

    {

    cout<<j<<'\t'<<envhis.his_cmd[i]<<endl;

    j++;

    }

     

    }

    else//否则,应分两段处理

    {

    // 显示history命令数组中start+1HISNUM-1的命令

    for (i=envhis.start+1;i<HISNUM;i++)

    {

    cout<<j<<'\t'<<envhis.his_cmd[i]<<endl;

    j++;

    }

    // 显示history命令数组中0end+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[]) //fpbg&命令调用此函数

    {

    // 初始化进程相关信息

    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);//打开进程IDid的进程,获得其句柄

    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,可从网络下载MSDNMSDN6.0版、2005版等不同版本,本实验指导使用的是6.0版本(Win32 APIPlatform SDK),可从网络下载MSDN Lib VS 6.0MSDN VC6.0

     

     

     

    展开全文
  • 操作系统实验c语言

    千次阅读 2019-12-07 20:38:23
    这个学期学了操作系统,实验课是用c语言实现几个操作系统比较核心的算法,其实也只是模拟一下,照真实的操作系统所运行的程序,还差得太远,虽然很想接触硬件,接触底层,用汇编等...操作系统实验二(银行家算法)...

    这个学期学了操作系统,实验课是用c语言实现几个操作系统比较核心的算法,其实也只是模拟一下,照真实的操作系统所运行的程序,还差得太远,虽然很想接触硬件,接触底层,用汇编等实现一下操作系统,但要真正实现一个操作系统,需要付出的太多,也怕耽误了学业,有兴趣的可以去看看《30天自制操作系统》。
    下面是我整理的用c语言实现的操作系统用到的几个算法
    操作系统实验一(进程调度算法)
    操作系统实验二(银行家算法)
    操作系统实验三(动态分区分配算法)
    操作系统实验四(页面置换算法)

    展开全文
  • 基于XV6操作系统实验平台建设和哈工大操作系统实验 的搭建实验环境参加实验平台建设的第一步就是搭建操作系统平台: 说白了就是在linux系统下搭建一个虚拟机运行xv6这个操作系统 配置环境 Ubuntu:16.04 xv6 & qemu ...

    基于XV6操作系统实验平台建设和哈工大操作系统实验 的搭建实验环境

    参加实验平台建设的第一步就是搭建操作系统平台:
    说白了就是在linux系统下搭建一个虚拟机运行xv6这个操作系统

    • 配置环境
    • Ubuntu:16.04
    • xv6 & qemu
    • 注:我同学ubuntu17貌似会出现fail

    安装linux系统,这里默认已经完成

    这里写图片描述

    • 这里使用的是root用户在操作。通过sudo -s切换到root用户。

    准备安装

    如:

    # 安装git
    apt-get install -y git
    # 安装g++
    apt-get install -y g++
    # 安装gdb
    apt-get install -y gdb
    # 安装编译工具链
    apt-get install -y build-essential
    # 下载xv6的代码
    cd /对应路径/
    git clone git://github.com/mit-pdos/xv6-public.git
    /在这里注意不要上github然后download zip,这样会丢失文件,我这就卡了一下午,一定要执行clone,会有非master版本的文件/

    这里写图片描述

    安装qemu

    # 安装qemu
    apt-get install qemu
    

    这里写图片描述

    编译xv6

    cd /到对应位置/xv6-public
    make qemu
    这里写图片描述


    生成qemu环境下运行的xv6
    成功

    有其他问题欢迎交流,联系qq:2282550468

    展开全文
  • 操作系统实验

    千次阅读 2020-05-20 21:13:48
    实验课程名称 操作系统实验 成绩 实验项目名称 进程管理与进程通信 指导老师 张艳玲 实验一 进程管理与进程通信 一、实验目的 1、掌握进程的概念,明确进程的含义。 2、认识并了解进程并发执行的实质,进程的阻塞与...

    广州大学学生实验报告
    开课学院:计算机科学与网络工程学院
    实验室:计算机软件实验室 2020 年5月20 日
    学院 计算机科学与教育软件学院 年级/专业/班 计科183 姓名 张健介 学号 1806100162
    实验课程名称 操作系统实验 成绩
    实验项目名称 进程管理与进程通信 指导老师 张艳玲

    实验一 进程管理与进程通信
    一、实验目的
    1、掌握进程的概念,明确进程的含义。
    2、认识并了解进程并发执行的实质,进程的阻塞与唤醒,终止与退出的过程。
    3、熟悉进程的睡眠、同步、撤消等进程控制方法。
    4、分析进程竞争资源的现象,学习解决进程互斥的方法 。
    5、了解什么是信号,利用信号量机制熟悉进程间软中断通信的基本原理,
    6、熟悉消息传送的机理 ,共享存储机制 。

    二、实验内容
    1、编写一段程序,使用系统调用fork( )创建两个子进程。当此程序运行时,在系统中有一个父进程和两个子进程并发执行,观察实验结果并分析原因。
    2、用fork( )创建一个进程,再调用exec( ),用新的程序替换该子进程的内容,利用wait( )来控制进程执行顺序,掌握进程的睡眠、同步、撤消等进程控制方法,并根据实验结果分析原因。
    3、编写一段多进程并发运行的程序,用lockf( )来给每一个进程加锁,以实现进程之间的互斥,观察并分析出现的现象及原因。
    4、编写程序:用fork( )创建两个子进程,再用系统调用signal( )让父进程捕捉键盘上来的中断信号(即按^c键);捕捉到中断信号后,父进程用系统调用kill( )向两个子进程发出信号,子进程捕捉到信号后分别输出下列信息后终止:
    Child process1 is killed by parent!
    Child process2 is killed by parent!
    父进程等待两个子进程终止后,输出如下的信息后终止:
    Parent process is killed!
    分析利用信号量机制中的软中断通信实现进程同步的机理。
    5、使用系统调用msgget( ),msgsnd( ),msgrev( ),及msgctl( )编制一长度为1k的消息发送和接收的程序,并分析消息的创建、发送和接收机制及控制原理。
    6、编制一长度为1k的共享存储区发送和接收的程序,并设计对该共享存储区进行互斥访问及进程同步的措施,必须保证实现正确的通信。

    三、实验原理
    1、进程创建与进程并发执行
    Linux中,进程既是一个独立拥有资源的基本单位,又是一个独立调度的基本单位。一个进程实体由若干个区(段)组成,包括程序区、数据区、栈区、共享存储区等。每个区又分为若干页,每个进程配置有唯一的进程控制块PCB,用于控制和管理进程。
    系统为每个进程配置了一张进程区表。表中,每一项记录一个区的起始虚地址及指向系统区表中对应的区表项。核心通过查找进程区表和系统区表,便可将区的逻辑地址变换为物理地址。
    进程是进程映像的执行过程,也就是正在执行的进程实体。它由三部分组成:
    (1)用户级上、下文。主要成分是用户程序;
    (2)寄存器上、下文。由CPU中的一些寄存器的内容组成,如PC,PSW,SP及通用寄存器等;
    (3)系统级上、下文。包括OS为管理进程所用的信息,有静态和动态之分。

    进程创建所涉及的系统调用:
    fork( ) 创建一个新进程。
    系统调用格式: pid=fork( )
    参数定义:int fork( )
    fork( )返回值意义如下:
    0:在子进程中,pid变量保存的fork( )返回值为0,表示当前进程是子进程。
    大于0:在父进程中,pid变量保存的fork( )返回值为子进程的id值(进程唯一标识符)。
    -1:创建失败。
    如果fork( )调用成功,它向父进程返回子进程的PID,并向子进程返回0,即fork( )被调用了一次,但返回了两次。此时OS在内存中建立一个新进程,所建的新进程是调用fork( )父进程(parent process)的副本,称为子进程(child process)。子进程继承了父进程的许多特性,并具有与父进程完全相同的用户级上下文。父进程与子进程并发执行。
    核心为fork( )完成以下操作:
    (1)为新进程分配一进程表项和进程标识符
    进入fork( )后,核心检查系统是否有足够的资源来建立一个新进程。若资源不足,则fork( )系统调用失败;否则,核心为新进程分配一进程表项和唯一的进程标识符。
    (2)检查同时运行的进程数目
    超过预先规定的最大数目时,fork( )系统调用失败。
    (3)拷贝进程表项中的数据
    将父进程的当前目录和所有已打开的数据拷贝到子进程表项中,并置进程的状态为“创建”状态。
    (4)子进程继承父进程的所有文件
    对父进程当前目录和所有已打开的文件表项中的引用计数加1。
    (5)为子进程创建进程上、下文
    进程创建结束,设子进程状态为“内存中就绪”并返回子进程的标识符。
    (6)子进程执行
    虽然父进程与子进程程序完全相同,但每个进程都有自己的程序计数器PC(注意子进程的PC开始位置),然后根据pid变量保存的fork( )返回值的不同,执行了不同的分支语句。

    2、进程的睡眠、同步、撤消等进程控制
    用fork( )创建一个进程,再调用exec( )用新的程序替换该子进程的内容,然后利用wait( )来控制进程执行顺序。
    (1)exec( )系列
    系统调用exec( )系列,也可用于新程序的运行。fork( )只是将父进程的用户级上下文拷贝到新进程中,而exec( )系列可以将一个可执行的二进制文件覆盖在新进程的用户级上下文的存储空间上,以更改新进程的用户级上下文。exec( )系列中的系统调用都完成相同的功能,它们把一个新程序装入内存,来改变调用进程的执行代码,从而形成新进程。如果exec( )调用成功,调用进程将被覆盖,然后从新程序的入口开始执行,这样就产生了一个新进程,新进程的进程标识符id 与调用进程相同。
    exec( )没有建立一个与调用进程并发的子进程,而是用新进程取代了原来进程。所以exec( )调用成功后,没有任何数据返回,这与fork( )不同。exec( )系列系统调用在UNIX系统库unistd.h中,共有execl、execlp、execle、execv、execvp五个,其基本功能相同,只是以不同的方式来给出参数。
    一种是直接给出参数的指针,如:
    int execl(path,arg0[,arg1,…argn],0);
    char *path,*arg0,*arg1,…,*argn;
    另一种是给出指向参数表的指针,如:
    int execv(path,argv);
    char *path,*argv[ ];
    具体使用可参考有关书。
    (2)exec( )和fork( )联合使用
    系统调用exec和fork( )联合使用能为程序开发提供有力支持。用fork( )建立子进程,然后在子进程中使用exec( ),这样就实现了父进程与一个与它完全不同子进程的并发执行。
    一般,wait、exec联合使用的模型为:
    int status;

    if (fork( )= =0)
    {
    …;
    execl(…);
    …;
    }
    wait(&status);
    (3)wait( )
    等待子进程运行结束。如果子进程没有完成,父进程一直等待。wait( )将调用进程挂起,直至其子进程因暂停或终止而发来软中断信号为止。如果在wait( )前已有子进程暂停或终止,则调用进程做适当处理后便返回。
    系统调用格式:
    int wait(status) 
    int *status;
    其中,status是用户空间的地址。它的低8位反应子进程状态,为0表示子进程正常结束,非0则表示出现了各种各样的问题;高8位则带回了exit( )的返回值。exit( )返回值由系统给出。
    核心对wait( )作以下处理:
    1)首先查找调用进程是否有子进程,若无,则返回出错码;
    2)若找到一处于“僵死状态”的子进程,则将子进程的执行时间加到父进程的执行时间上,并释放子进程的进程表项;
    3)若未找到处于“僵死状态”的子进程,则调用进程便在可被中断的优先级上睡眠,等待其子进程发来软中断信号时被唤醒。
    (4)exit( )
    终止进程的执行。
    系统调用格式:
        void exit(status)
       int status;
    其中,status是返回给父进程的一个整数,以备查考。
    为了及时回收进程所占用的资源并减少父进程的干预,UNIX/LINUX利用exit( )来实现进程的自我终止,通常父进程在创建子进程时,应在进程的末尾安排一条exit( ),使子进程自我终止。exit(0)表示进程正常终止,exit(1)表示进程运行有错,异常终止。
    如果调用进程在执行exit( )时,其父进程正在等待它的终止,则父进程可立即得到其返回的整数。核心须为exit( )完成以下操作:
    1)关闭软中断
    2)回收资源
    3)写记帐信息
    4)置进程为“僵死状态”

    3、多进程通过加锁互斥并发运行
    用lockf( )来给每一个进程加锁,以实现多进程之间的互斥。
    所涉及的系统调用:lockf(files,function,size),用作锁定文件的某些段或者整个文件。
    本函数的头文件为
    #include “unistd.h”
    参数定义:
    int lockf(files,function,size)
    int files,function;
    long size;
    其中:files是文件描述符;function是锁定和解锁:1表示锁定,0表示解锁。size是锁定或解锁的字节数,为0,表示从文件的当前位置到文件尾。

    4、进程间通过信号机制实现软中断通信
    (1)信号的基本概念
    每个信号都对应一个正整数常量(称为signal number,即信号编号。定义在系统头文件<signal.h>中),代表同一用户的诸进程之间传送事先约定的信息的类型,用于通知某进程发生了某异常事件。每个进程在运行时,都要通过信号机制来检查是否有信号到达。若有,便中断正在执行的程序,转向与该信号相对应的处理程序,以完成对该事件的处理;处理结束后再返回到原来的断点继续执行。实质上,信号机制是对中断机制的一种模拟,故在早期的UNIX版本中又把它称为软中断。
    信号与中断的相似点:
    1)采用了相同的异步通信方式;
    2)当检测出有信号或中断请求时,都暂停正在执行的程序而转去执行相应的处理程序;
    3)都在处理完毕后返回到原来的断点;
    4)对信号或中断都可进行屏蔽。
    信号与中断的区别:
    1)中断有优先级,而信号没有优先级,所有的信号都是平等的;
    2)信号处理程序是在用户态下运行的,而中断处理程序是在核心态下运行;
    (3)中断响应是及时的,而信号响应通常都有较大的时间延迟。
    信号机制具有以下三方面的功能:
    1)发送信号。发送信号的程序用系统调用kill( )实现;
    2)预置对信号的处理方式。接收信号的程序用signal( )来实现对处理方式的预置;
    3)收受信号的进程按事先的规定完成对相应事件的处理。
    (2)信号的发送
    信号的发送,是指由发送进程把信号送到指定进程的信号域的某一位上。如果目标进程正在一个可被中断的优先级上睡眠,核心便将它唤醒,发送进程就此结束。一个进程可能在其信号域中有多个位被置位,代表有多种类型的信号到达,但对于一类信号,进程却只能记住其中的某一个。
    进程用kill( )向一个进程或一组进程发送一个信号。
    (3)对信号的处理
    当一个进程要进入或退出一个低优先级睡眠状态时,或一个进程即将从核心态返回用户态时,核心都要检查该进程是否已收到软中断。当进程处于核心态时,即使收到软中断也不予理睬;只有当它返回到用户态后,才处理软中断信号。对软中断信号的处理分三种情况进行:
    1)如果进程收到的软中断是一个已决定要忽略的信号(function=1),进程不做任何处理便立即返回;
    2)进程收到软中断后便退出(function=0);
    3)执行用户设置的软中断处理程序。
    (4)所涉及的中断调用
    (1)kill( )
    系统调用格式:int kill(pid,sig)
    参数定义:int pid,sig;
    其中,pid是一个或一组进程的标识符,参数sig是要发送的软中断信号。
    1)pid>0时,核心将信号发送给进程pid。
    2)pid=0时,核心将信号发送给与发送进程同组的所有进程。
    3)pid=-1时,核心将信号发送给所有用户标识符真正等于发送进程的有效用户标识号的进程。
    (2)signal( )
    预置对信号的处理方式,允许调用进程控制软中断信号。
    系统调用格式
    signal(sig,function)
    头文件为
      #include <signal.h>
    参数定义
    signal(sig,function)
    int sig;
    void (*func) ( )
    其中sig用于指定信号的类型,sig为0则表示没有收到任何信号,余者如下表:

    值 名 字 说 明
    01 SIGHUP 挂起(hangup)
    02 SIGINT 中断,当用户从键盘按c键或break键时
    03 SIGQUIT 退出,当用户从键盘按quit键时
    04 SIGILL 非法指令
    05 SIGTRAP 跟踪陷阱(trace trap),启动进程,跟踪代码的执行
    06 SIGIOT IOT指令
    07 SIGEMT EMT指令
    08 SIGFPE 浮点运算溢出
    09 SIGKILL 杀死、终止进程
    10 SIGBUS 总线错误
    11 SIGSEGV 段违例(segmentation violation),进程试图去访问其虚地址空间以外的位置
    12 SIGSYS 系统调用中参数错,如系统调用号非法
    13 SIGPIPE 向某个非读管道中写入数据
    14 SIGALRM 闹钟。当某进程希望在某时间后接收信号时发此信号
    15 SIGTERM 软件终止(software termination)
    16 SIGUSR1 用户自定义信号1
    17 SIGUSR2 用户自定义信号2
    18 SIGCLD 某个子进程死
    19 SIGPWR 电源故障

    function:在该进程中的一个函数地址,在核心返回用户态时,它以软中断信号的序号作为参数调用该函数,对除了信号SIGKILL,SIGTRAP和SIGPWR以外的信号,核心自动地重新设置软中断信号处理程序的值为SIG_DFL,一个进程不能捕获SIGKILL信号。
    function 的解释如下:
    1)function=1时,进程对sig类信号不予理睬,亦即屏蔽了该类信号;
    2)function=0时,缺省值,进程在收到sig信号后应终止自己;
    3)function为非0,非1类整数时,function的值即作为信号处理程序的指针。

    5、消息的发送与接收
    使用系统调用msgget( ),msgsnd( ),msgrev( ),及msgctl( )编制一长度为1k的消息发送和接收的程序。
    消息(message)是一个格式化的可变长的信息单元。消息机制允许由一个进程给其它任意的进程发送一个消息。当一个进程收到多个消息时,可将它们排成一个消息队列。消息使用二种重要的数据结构:一是消息首部,其中记录了一些与消息有关的信息,如消息数据的字节数;二个消息队列头表,其每一表项是作为一个消息队列的消息头,记录了消息队列的有关信息。
    (1)消息机制的数据结构
    (1)消息首部
    记录一些与消息有关的信息,如消息的类型、大小、指向消息数据区的指针、消息队列的链接指针等。
    (2)消息队列头表
    其每一项作为一个消息队列的消息头,记录了消息队列的有关信息如指向消息队列中第一个消息和指向最后一个消息的指针、队列中消息的数目、队列中消息数据的总字节数、队列所允许消息数据的最大字节总数,还有最近一次执行发送操作的进程标识符和时间、最近一次执行接收操作的进程标识符和时间等。
    (3) 消息队列的描述符
    UNIX中,每一个消息队列都有一个称为关键字(key)的名字,是由用户指定的;消息队列有一消息队列描述符,其作用与用户文件描述符一样,也是为了方便用户和系统对消息队列的访问。
    涉及的系统调用
    (1) msgget( )
    创建一个消息,获得一个消息的描述符。核心将搜索消息队列头表,确定是否有指定名字的消息队列。若无,核心将分配一新的消息队列头,并对它进行初始化,然后给用户返回一个消息队列描述符,否则它只是检查消息队列的许可权便返回。
    系统调用格式:
    msgqid=msgget(key,flag)
    该函数使用头文件如下:
    #include<sys/types.h>
    #include<sys/ipc.h>
    #include<sys/msg.h>
    参数定义
    int msgget(key,flag)
    key_t key;
    int flag;
    其中:
    key是用户指定的消息队列的名字;flag是用户设置的标志和访问方式。如 IPC_CREAT |0400 是否该队列已被创建。无则创建,是则打开;
    IPC_EXCL |0400 是否该队列的创建应是互斥的。
    msgqid 是该系统调用返回的描述符,失败则返回-1。
    (2) msgsnd()
    发送一消息。向指定的消息队列发送一个消息,并将该消息链接到该消息队列的尾部。
    系统调用格式:
    msgsnd(msgqid,msgp,size,flag)
    该函数使用头文件如下:
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    参数定义:
    int msgsnd(msgqid,msgp,size,flag)
    I int msgqid,size,flag;
    struct msgbuf * msgp;
    其中msgqid是返回消息队列的描述符;msgp是指向用户消息缓冲区的一个结构体指针。缓冲区中包括消息类型和消息正文,即
    {
    long mtype; /消息类型/
    char mtext[ ]; /消息的文本/
    }
    size指示由msgp指向的数据结构中字符数组的长度;即消息的长度。这个数组的最大值由MSG-MAX( )系统可调用参数来确定。flag规定当核心用尽内部缓冲空间时应执行的动作:进程是等待,还是立即返回。若在标志flag中未设置IPC_NOWAIT位,则当该消息队列中的字节数超过最大值时,或系统范围的消息数超过某一最大值时,调用msgsnd进程睡眠。若是设置IPC_NOWAIT,则在此情况下,msgsnd立即返回。
    对于msgsnd( ),核心须完成以下工作:
    1)对消息队列的描述符和许可权及消息长度等进行检查。若合法才继续执行,否则返回;
    2)核心为消息分配消息数据区。将用户消息缓冲区中的消息正文,拷贝到消息数据区;
    3)分配消息首部,并将它链入消息队列的末尾。在消息首部中须填写消息类型、消息大小和指向消息数据区的指针等数据;
    4)修改消息队列头中的数据,如队列中的消息数、字节总数等。最后,唤醒等待消息的进程。
    (3) msgrcv( )
    接受一消息。从指定的消息队列中接收指定类型的消息。
    系统调用格式:
    msgrcv(msgqid,msgp,size,type,flag)
    本函数使用的头文件如下:
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    参数定义:
    int msgrcv(msgqid,msgp,size,type,flag)
    int msgqid,size,flag;
    struct msgbuf *msgp;
    long type;
    其中,msgqid,msgp,size,flag与msgsnd中的对应参数相似,type是规定要读的消息类型,flag规定倘若该队列无消息,核心应做的操作。如此时设置了IPC_NOWAIT标志,则立即返回,若在flag中设置了MS_NOERROR,且所接收的消息大于size,则核心截断所接收的消息。
    对于msgrcv系统调用,核心须完成下述工作:
    1)对消息队列的描述符和许可权等进行检查。若合法,就往下执行;否则返回;
    2)根据type的不同分成三种情况处理:
    type=0,接收该队列的第一个消息,并将它返回给调用者;
    type为正整数,接收类型type的第一个消息;
    type为负整数,接收小于等于type绝对值的最低类型的第一个消息。
    3)当所返回消息大小等于或小于用户的请求时,核心便将消息正文拷贝到用户区,并从消息队列中删除此消息,然后唤醒睡眠的发送进程。但如果消息长度比用户要求的大时,则做出错返回。
    (4) msgctl( )
    消息队列的操纵。读取消息队列的状态信息并进行修改,如查询消息队列描述符、修改它的许可权及删除该队列等。
    系统调用格式:
    msgctl(msgqid,cmd,buf);
    本函数使用的头文件如下:
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    参数定义:
    int msgctl(msgqid,cmd,buf);
    int msgqid,cmd;
    struct msgqid_ds *buf;
    其中,函数调用成功时返回0,不成功则返回-1。buf是用户缓冲区地址,供用户存放控制参数和查询结果;cmd是规定的命令。命令可分三类:
    1)IPC_STAT。查询有关消息队列情况的命令。如查询队列中的消息数目、队列中的最大字节数、最后一个发送消息的进程标识符、发送时间等;
    2)IPC_SET。按buf指向的结构中的值,设置和改变有关消息队列属性的命令。如改变消息队列的用户标识符、消息队列的许可权等;
    3)IPC_RMID。消除消息队列的标识符。
    msgqid_ds 结构定义如下:
    struct msgqid_ds
    { struct ipc_perm msg_perm; /许可权结构/
    short pad1[7]; /由系统使用/
    ushort msg_qnum; /队列上消息数/
    ushort msg_qbytes; /队列上最大字节数/
    ushort msg_lspid; /最后发送消息的PID/
    ushort msg_lrpid; /最后接收消息的PID/
    time_t msg_stime; /最后发送消息的时间/
    time_t msg_rtime; /最后接收消息的时间/
    time_t msg_ctime; /最后更改时间/
    };
    struct ipc_perm
    { ushort uid; /当前用户/
    ushort gid; /当前进程组/
    ushort cuid; /创建用户/
    ushort cgid; /创建进程组/
    ushort mode; /存取许可权/
    { short pid1; long pad2;} /由系统使用/
    }

    6、进程的共享存储区通信
    编制一长度为1k的共享存储区发送和接收的程序。
    (1)共享存储区机制的概念
    共享存储区(Share Memory)是UNIX系统中通信速度最高的一种通信机制。该机制可使若干进程共享主存中的某一个区域,且使该区域出现(映射)在多个进程的虚地址空间中。另一方面,一个进程的虚地址空间中又可连接多个共享存储区,每个共享存储区都有自己的名字。当进程间欲利用共享存储区进行通信时,必须先在主存中建立一共享存储区,然后将它附接到自己的虚地址空间上。此后,进程对该区的访问操作,与对其虚地址空间的其它部分的操作完全相同。进程之间便可通过对共享存储区中数据的读、写来进行直接通信。图示列出二个进程通过共享一个共享存储区来进行通信的例子。其中,进程A将建立的共享存储区附接到自己的AA’区域,进程B将它附接到自己的BB’区域。

    进程A的虚空间 内存空间 进程B的虚空间

                A
    
                A’
    
    
    
    应当指出,共享存储区机制只为进程提供了用于实现通信的共享存储区和对共享存储区进行操作的手段,然而并未提供对该区进行互斥访问及进程同步的措施。因而当用户需要使用该机制时,必须自己设置同步和互斥措施才能保证实现正确的通信。
    

    (2)涉及的系统调用
    1)shmget( )
    创建、获得一个共享存储区。
    系统调用格式:
    shmid=shmget(key,size,flag)
    该函数使用头文件如下:
    #include<sys/types.h>
    #include<sys/ipc.h>
    #include<sys/shm.h>
    参数定义
    int shmget(key,size,flag);
    key_t key;
    int size,flag;
    其中,key是共享存储区的名字;size是其大小(以字节计);flag是用户设置的标志,如IPC_CREAT。IPC_CREAT表示若系统中尚无指名的共享存储区,则由核心建立一个共享存储区;若系统中已有共享存储区,便忽略IPC_CREAT。
    附:
    操作允许权 八进制数
    用户可读 00400
    用户可写 00200
    小组可读 00040
    小组可写 00020
    其它可读 00004
    其它可写 00002

    控制命令 值
    IPC_CREAT 0001000
    IPC_EXCL 0002000
    例:shmid=shmget(key,size,(IPC_CREAT|0400))
    创建一个关键字为key,长度为size的共享存储区
    2)shmat( )
    共享存储区的附接。从逻辑上将一个共享存储区附接到进程的虚拟地址空间上。
    系统调用格式:
    virtaddr=shmat(shmid,addr,flag)
    该函数使用头文件如下:
    #include<sys/types.h>
    #include<sys/ipc.h>
    #include<sys/shm.h>
    参数定义
    char *shmat(shmid,addr,flag);
    int shmid,flag;
    char * addr;
    其中,shmid是共享存储区的标识符;addr是用户给定的,将共享存储区附接到进程的虚地址空间;flag规定共享存储区的读、写权限,以及系统是否应对用户规定的地址做舍入操作。其值为SHM_RDONLY时,表示只能读;其值为0时,表示可读、可写;其值为SHM_RND(取整)时,表示操作系统在必要时舍去这个地址。该系统调用的返回值是共享存储区所附接到的进程虚地址viraddr。
    3)shmdt( )
    把一个共享存储区从指定进程的虚地址空间断开。
    系统调用格式:
    shmdt(addr)
    该函数使用头文件如下:
    #include<sys/types.h>
    #include<sys/ipc.h>
    #include<sys/shm.h>
    参数定义
    int shmdt(addr);
    char addr;
    其中,addr是要断开连接的虚地址,亦即以前由连接的系统调用shmat( )所返回的虚地址。调用成功时,返回0值,调用不成功,返回-1。
    4)shmctl( )
    共享存储区的控制,对其状态信息进行读取和修改。
    系统调用格式:
    shmctl(shmid,cmd,buf)
    该函数使用头文件如下:
    #include<sys/types.h>
    #include<sys/ipc.h>
    #include<sys/shm.h>
    参数定义
    int shmctl(shmid,cmd,buf);
    int shmid,cmd;
    struct shmid_ds *buf;
    其中,buf是用户缓冲区地址,cmd是操作命令。命令可分为多种类型:
    第一种:用于查询有关共享存储区的情况。如其长度、当前连接的进程数、共享区的创建者标识符等;
    第二种:用于设置或改变共享存储区的属性。如共享存储区的许可权、当前连接的进程计数等;
    第三种:对共享存储区的加锁和解锁命令;
    第四种:删除共享存储区标识符等。
    上述的查询是将shmid所指示的数据结构中的有关成员,放入所指示的缓冲区中;而设置是用由buf所指示的缓冲区内容来设置由shmid所指示的数据结构中的相应成员。

    四、实验中用到的系统调用函数(包括实验原理中介绍的和自己采用的),自己采用的系统调用函数要按照指导书中的格式说明进行介绍。
    本实验的用到的主要系统调用函数:
    Fork, exec, wait, exit, getpid, sleep, lockf, kill, signal, read, write, msgget, msgsnd, msgrcv, msgctl,shmget, shmat, shmdt, shmctl。
    五、实验步骤(要求写出实验过程和思路。)
    1.父进程创建两个子进程,然后三个进程并发执行,父进程输出a,子进程a输出b,子进程b输出c,多次运行程序,并通过结果观察其规律。

    2.父进程创建一个子进程,如果子进程创建成功,若此时是父进程正在运行,则父进程进行等待操作,将cpu教给子进程,子进程获取cpu后,开始运行,用系统中bin目录下的ls命令程序装入子进程运行的地址,即用ls命令程序代替子进程。当子进程运行完毕,父进程输出,程序运行完毕。

    3.父进程创建两个子进程,若是父进程运行,则输出parent 1-500,若子进程运行,则输出son1-500或者daughter1-500,分别用加锁和不加锁的程序进行测试。

    4.用fork( )创建两个子进程,再用系统调用signal( )让父进程捕捉键盘上来的中断信号(即按^c键);捕捉到中断信号后,父进程用系统调用kill( )向两个子进程发出信号,子进程捕捉到信号后分别输出下列信息后终止:
    Child process1 is killed by parent!
    Child process2 is killed by parent!
    父进程等待两个子进程终止后,输出如下的信息后终止:
    Parent process is killed!

    5.在两个终端上创建两个进程,分别是client负责发送消息,server负责接受消息。

    6.建立两个进程server和client,开辟一个共享资源区,双方都处于不断同步的状态,client等待资源区被server修改,而server修改后,也在等待client修改资源区,client察觉资源区被修改,也会继续修改,就这样进行循环。

    六、实验数据及源代码(学生必须提交自己设计的程序源代码,并有注释,源代码电子版也一并提交),包括思考题的程序。
    1)

    #include <unistd.h>
    #include <sys/types.h>
    #include <stdio.h>
    int main(int argc,char *argv[])
    {
        int pa;  //进程a
        int pb;  //进程b
        while((pa=fork())==-1); //等待子进程a建立完成
        if(pa==0)
          {
             putchar('b');
          }
        else
          {
             while((pb=fork())==-1);
             if(pb==0)
               {
                  putchar('c');
               }
             else
               {
                 putchar('a');
               }
          }
    }
    

    2)

    #include <stdlib.h>
    #include<stdio.h>
    #include<sys/wait.h>
    #include<unistd.h>
    int main(int argc,char* argv[])
    {
      int pid;
      pid=fork();
      switch(pid)
      {
        case -1:
                printf("fork fail!\n");
                exit(1);
        case 0:
                execl("/bin/ls","-1","-color",NULL);
                printf("exec fail!\n");
                exit(1);
        default:
                wait(NULL);
                printf("ls completed!\n");
                exit(0);
    
      }
    }
    

    3)

    #include<stdio.h>
    #include<stdlib.h>
    #include<sys/wait.h>
    #include<unistd.h>
    
    int main(int argc,char* argv[])
    {
         int p1,p2,i;
         p1=fork();
         p2=fork();
         if(p1==0)
           {
               lockf(1,1,0);     //加锁
               for(i=0;i<500;i++)
                   printf("parent %d\n",i);
               lockf(1,0,0);   //解锁
               wait(0);  //保证子进程终止前,父进程不终止
               exit(0);
           }
         else
          {
               if(p2==0)
                 {
                     lockf(1,1,0);
                     for(i=0;i<500;i++)
                           printf("son %d\n",i);
                     lockf(1,0,0);
                     wait(0);  
                     exit(0);
                 }
               else
                {
                      lockf(1,1,0);
                      for(i=0;i<500;i++)
                           printf("daughter %d\n",i);
                      lockf(1,0,0);
                      exit(0);
                }
          }
    }
    

    4)

    #include<stdio.h>
    #include<signal.h>
    #include<unistd.h>
    #include<sys/wait.h>
    #include<stdlib.h>
    
    int pid1,pid2;
    int EndFlag=0;
    int pf1=0;
    int pf2=0;
    
    void IntDelete()
    {
       kill(pid1,16);
       kill(pid2,17);
    }
    
    void Int1()
    {
    printf("child process 1 is killed by parent!\n");
    exit(0);
    }
    void Int2()
    {
    printf("child process 2 is killed by parent!\n");
    exit(0);
    }
    int main(int argc,char*argv[])
    {
       int exitpid;
       if(pid1=fork())
          {
               if(pid2=fork())
                  {
                      signal(SIGINT,IntDelete);
                      waitpid(-1,&exitpid,0);
                      waitpid(-1,&exitpid,0);
                      printf("parent process is killed\n");
                      exit(0);
                  }
               else
                  {
                       signal(SIGINT,SIG_IGN);
                       signal(17,Int2);
                       pause();
                  }
          }
        else
          {
             signal(SIGINT,SIG_IGN);
             signal(16,Int1);
             pause();
          }
    
    }
    

    5)
    Server:

    #include<stdio.h>
    #include<stdlib.h>
    #include<sys/types.h>
    #include<sys/msg.h>
    #include<sys/ipc.h>
    #define MSGKEY 75
    struct msgform
    {
    long mtype;
    char mtextp[1000];
    }msg;
    int msgqid;
    void server()
    {
      msgqid=msgget(MSGKEY,0777|IPC_CREAT);
      do
       {
         msgrcv(msgqid,&msg,1030,0,0);
         printf("(sever)received\n");
       }while(msg.mtype!=1);
      msgctl(msgqid,IPC_RMID,0);
      exit(0);
    }
    int main(int argc,char* argv[])
    {
      server();
    }
    

    client:

    #include<stdio.h>
    #include<stdlib.h>
    #include<sys/types.h>
    #include<sys/msg.h>
    #include<sys/ipc.h>
    #define MSGKEY 75
    struct magform
    {
    long mtype;
    char mtext[1000];
    }msg;
    int msgqid;
    void client()
    {
       int i;
       msgqid=msgget(MSGKEY,0777);
       for(i=10;i>=-1;i--)
          {
            msg.mtype=i;
            printf("(client)sent\n");
            msgsnd(msgqid,&msg,1024,0);
          }
       exit(0);
    }
    int main(int argc,char* argv[])
    {
    client();
    }
    

    ~
    6)

    #include<stdio.h>
    #include<stdlib.h>
    #include<sys/types.h>
    #include<sys/shm.h>
    #include<sys/ipc.h>
    #include<sys/wait.h>
    
    #include<unistd.h>
    #define SHMKEY 75
    int shmid,i;
    int *addr;
    void client()
    {
      int i;
      shmid=shmget(SHMKEY,1024,0777);
      addr=shmat(shmid,0,0);
      for(i=9;i>=0;i--)
        {
          while(*addr!=-1)
             printf("(client)sent\n");
          *addr=i;
        }
    exit(0);
    }
    void server()
    {
       shmid=shmget(SHMKEY,1024,0777|IPC_CREAT);
       addr=shmat(shmid,0,0);
       do
       {
        *addr=-1;
        while(*addr==-1);
            printf("*(server)receive\n");
       }while(*addr);
       shmctl(shmid,IPC_RMID,0);
       exit(0);
    
    }
    
    int main(int argc,char*argv[])
    {
      while((i=fork())==-1);
      if(!i) server();
      system("ipcs -m");
      while((i=fork())==-1);
      if(!i) client();
      wait(0);
      wait(0);
    }
    

    七、实验结果分析(截屏的实验结果,与实验结果对应的实验分析)
    1、实验结果与实验程序、实验步骤、实验原理、操作系统原理的对应分析;
    2、不同条件下的实验结果反应的问题及原因;
    3、实验结果的算法时间、效率、鲁棒性等性能分析。

    结果:
    刚开始的结果都是acb。
    在这里插入图片描述
    但是这不太符合并发执行的进程结果,所以后来我把虚拟机的处理器内核数量从1提高到3就会出现随机的结果啦!
    在这里插入图片描述
    分析:从结果来看,父进程和两个子进程的并发执行,结果是随机的,即abc的顺序都有可能,也就是说,并发执行的子进程之间,在没有优先级的限制条件下,争夺cpu的情况是随机的,在代码中,可能存在进程建立时间和代码运行输出等时间的一个不对等性,但是进程获取的时间片是随机的,故而会输出随机的不同结果。
    思考题:
    2.
    结果:
    利用wait()函数实现同步:

    在这里插入图片描述
    未用wait()函数实现同步:
    在这里插入图片描述
    分析:第一个结果是利用睡眠等待等操作实现同步,第二幅图的结果是未用等待操作导致不同步的结果,由第一幅图可知,子进程创建成功,且运行了ls命令程序,多次运行程序也是同一个结果,而当取消wait()的时候,顺序输出便错乱了,说明该程序父进程和子进程实现了同步。
    3.
    结果:
    1)不用lockf()进行进程加锁:
    在这里插入图片描述
    利用lockf()给进程加锁:
    在这里插入图片描述
    分析:
    Lockf(1,1,0)加锁输出设备,而Lockf(1,0,0)解锁输出设备,未用lockf进行进程加锁,三个循环之间会交错进行,输出也会出现交错状态,但加锁后则不会,所以在lockf(1,1,0)和lockf(1,0,0)之间的for循环输出中,不会被间断。但加锁前的三个进程间的存在并发执行,故而parent,daughter,son这三个循环的输出顺序随机,但循环不会被间断。
    分析:

    结果:
    在这里插入图片描述
    分析:
    父进程接收到ctrl+c信号才会调用kill()向两个子进程发出信号。子进程接收到信号才会打印。
    5)
    结果:
    在这里插入图片描述
    在这里插入图片描述

    分析:首先运行服务代码test5_s,server会进入等待状态,继而运行客户代码test5_c像服务器发出消息,并输出(client)sent,继而运行服务器的终端收到消息,输出(server)received。

    6)
    结果:
    在这里插入图片描述
    分析:
    Client和server两个进程拥有共享资源区,开始时,client修改了资源区内容,然后等待共享区的内容被修改为-1,因此是等待状态,而server修改了共享区的内容后,client察觉到内容变为-1,故而又继续修改,不断循环,直到client修改为-1。

    思考题:

    1、进程创建与进程并发执行
    (1)系统是怎样创建进程的?
    1.申请空白PCB(进程控制块);
    2.为新进程分配资源;
    3.初始化PCB;
    4.就新进程插入就绪队列;
    (2)当首次调用新创建进程时,其入口在哪里?
    fork()函数被调用一次,但返回两次;两次返回区别在于:子程序返回值是0,而父进程返回值是子进程的ID。子进程和父进程运行相同的代码,但是有自己的数据空间。

    (3)程序的多次运行结果为什么不同?如何控制实验结果的随机性?
    多个进程的并发执行,每个进程都有先获取cpu的可能性,故而哪一个进程先执行是随机的。可以通过等待,睡眠的等操作来实现多个进程的同步。
    (4)利用strace 和ltrace -f -i -S ./executable-file-name查看程序执行过程,并分析原因,画出进程家族树。

    2、进程的睡眠、同步、撤消等进程控制
    (1)可执行文件加载时进行了哪些处理?
    进程用exec( )装入命令ls ,exec( )后,子进程的代码被ls的代码取代,这时子进程的PC指向ls的第1条语句,开始执行ls的命令代码
    (2)什么是进程同步?wait( )是如何实现进程同步的?
    进程同步是指多个相关进程在执行次序上进行协调,以使并发执行的主进程之间有效的共享资源和相互合作,从而使程序的执行具有可再现性。
    首先程序在调用fork()创建了一个子进程后,马上调用wait(),使父进程在子进程调用之前一直处于睡眠状态,这样使子进程先运行,子进程运行exec()装入命令后,然后调用wait(0),使子进程和父进程并发执行,实现了进程同步。
    (3)wait( )和exit()是如何控制实验结果的随机性的?
    可以看出在使用了exec()函数后程序使用了ls的命令,列出/bin/目录下的文件信息,执行完execl()函数后,子进程调用exit()函数,退出当前进程,我们可以发现在使用wait()函数后,父进程永远将在其他的子进程完成之后才执行,所以在输出的结果中我们可以看到最后输出的将是父进程的信息,这样进而可以控制实验结果的随机性。
    3、多进程通过加锁互斥并发运行
    (1)进程加锁和未上锁的输出结果相同吗? 为什么?
    大致与未上锁的输出结果相同,也是随着执行时间不同,输出结果的顺序有所不同。
    不同进程之间不存在共享临界资源问题,所以加锁与不加锁的效果大致相同。
    4、进程间通过信号机制实现软中断通信
    (1)为了得到实验内容要求的结果,需要用到哪些系统调用函数来实现及进程间的通信控制和同步?
    Signal,kill,wait,exit,lockf
    (1)kill( )和signal( )函数在信号通信中的作用是什么?如果分别注释掉它们,结果会如何?
    结果都不会有输出,因为注释kill的情况下,父进程在等待子进程的信号,子进程也在等待父进程的信号,进入的死锁状态。而注释掉signal的情况下,子进程无法接受道父进程的信号,也无法向父进程发送信号,所以子进程结束后,父进程一直处于等待状态。
    5、消息的发送与接收
    (1)为了便于操作和观察结果,需要编制几个程序分别用于消息的发送与接收?
    为了便于操作和观察结果,编制了两个程序client.c和server.c,分别用于信息的发送与接受。
    (2)这些程序如何进行编辑、编译和执行?为什么?
    使用vim工具:vim test5_c.c vim test5_s.c分别编辑两个文件的代码
    使用gcc: gcc -o test5_c test5_c.c和gcc -o test5_s test5_s.c
    最后在两个终端分别先后运行./test5_s和./test5_c

    (3)如何实现消息的发送与接收的同步?
    分别在两个终端建立两个进程server和client,client发送信号,server接受信号,先运行server,后运行client。在server接受到消息后才会输出。

    6、进程的共享存储区通信
    (1)为了便于操作和观察结果,需要如何合理设计程序来实现子进程间的共享存储区通信?
    建立两个进程server和client,开辟一个共享资源区,双方都处于不断同步的状态,client等待资源区被server修改,而server修改后,也在等待client修改资源区,client察觉资源区被修改,也会继续修改,就这样进行循环多次。

    (2)比较消息通信和共享存储区通信这两种进程通信机制的性能和优缺点。
    答:由于两种机制实现的机理和用处都不一样,难以直接进行时间上的比较。如果比较其性能,应更加全面的分析。
    (1)消息队列的建立比共享区的设立消耗的资源少。前者只是一个软件上设定的问题,后者需要对硬件的操作,实现内存的映像,当然控制起来比前者复杂。如果每次都重新进行队列或共享的建立,共享区的设立没有什么优势。
    (2)当消息队列和共享区建立好后,共享区的数据传输,受到了系统硬件的支持,不耗费多余的资源;而消息传递,由软件进行控制和实现,需要消耗一定的cpu的资源。从这个意义上讲,共享区更适合频繁和大量的数据传输。
    (3)消息的传递,自身就带有同步的控制。当等到消息的时候,进程进入睡眠状态,不再消耗cpu资源。而共享队列如果不借助其他机制进行同步,接收数据的一方必须进行不断的查询,白白浪费了大量的cpu资源。可见,消息方式的使用更加灵活。

    展开全文
  • 操作系统实验1

    千次阅读 2019-06-26 18:09:58
    操作系统 实验报告 题目: 实验1 学生姓名: 周思宇 学生学号: 201608030201 ...
  • 操作系统实验报告

    千次阅读 2020-06-24 12:02:17
    操作系统实验报告 1. myecho.c 1.1. 实验内容 myecho.c的功能与系统echo程序相同 接受命令行参数,并将参数打印出来 1.2. 效果展示 myecho$ ./myecho a b c a b c 1.3. 实验思路和关键代码 读取输入的参数,按...
  • 操作系统实验之系统调用

    千次阅读 2016-09-07 21:54:24
    操作系统实验之系统调用
  • 计算机操作系统实验代码,包括先来先服务FCFS和短作业优先SJF进程调度算法、时间片轮转RR进程调度算法、预防进程死锁的银行家算法、动态分区分配算法、虚拟内存页面置换算法、磁盘调度算法
  • 最近在学习清华大学操作系统课程,同时在实验楼做实验 。共9个实验,打算把每次实验过程记录下来。RunNoob!!
  • 哈工大操作系统实验手册 实验资源与参考 不配环境懒人福利:实验楼 在线课程:操作系统,李治军,哈工大(网易云课堂) 参考阅读:《Linux内核完全注释》——赵炯,《操作系统原理、实现与实践》——李治军,...
  • 操作系统实验报告 lab1

    万次阅读 2017-03-18 10:32:27
    操作系统实验报告lab1
  • 操作系统实验(一)

    万次阅读 2019-03-28 16:46:58
    操作系统实验 hello,我是橘子 最近突然发现我以前写的操作系统实验,然后分享给大家吧,就是自己写的代码也不是很好,希望大家见谅哈 实验目的 一、进程控制 ●基本要求:利用简单的结构和控制方法模拟进程结构、...
  • 参考这篇文章在实验楼做了一下系统调用的操作系统实验课,基本把流程走了一遍,对系统调用的理解更加深刻了一点,实验楼真是个良心IT教程网站,对增强动手能力很有帮助,可以加深对书本的理解,强烈推荐,以后应该会...
  • 操作系统实验五:文件系统

    千次阅读 2019-06-17 03:53:39
    操作系统实验五:文件系统 代码地址点这里 1.函数功能说明 1.1模块说明 format模块 功能: 格式化文件系统,即初始化文件系统,相当于硬盘的格式化。将其中原有的用户及用户下的文件系统全部还原初始状态,即没有...
  • 操作系统实验报告 lab3

    千次阅读 2017-04-24 09:26:24
    操作系统实验报告 lab3
  • 操作系统实验报告 lab4

    千次阅读 2017-04-30 22:44:10
    操作系统实验报告 lab4
  • 阅读分别运行用API接口函数getpid()直接调用和汇编中断调用两种方式调用Linux操作系统的同一个系统调用getpid的程序(请问getpid的系统调用号是多少?linux系统调用的中断向量号是多少?)。 2、上机完成习题1.13。 ...
  • 操作系统实验四-页面置换算法实验

    千次阅读 2019-05-27 14:38:41
    操作系统实验四-页面置换算法实验 Y.xj 本实验重点为不同的页面置换算法,所以程序主体部分完全相同 程序总体函数变量如下: 程序实现步骤: 判断是否缺页 判断内存是否存满 若内存不满,则直接插入 若内存满...
  • 杭电操作系统实验一报告

    千次阅读 2019-03-24 00:13:22
    按理说操作系统实验应该自己做,这样能锻炼自己。鉴于我的报告还是比较有参考价值,能让以后的同学参考一下,就做成md的形式。仅供参考! 实验一报告 一、实验内容: (1)实验名:Linux内核编译及添加系统调用...
  • 操作系统实验报告-系统调用

    千次阅读 2017-03-04 15:23:43
    操作系统实验报告-系统调用 实验内容 在Linux 0.11上添加两个系统调用,并编写两个简单的应用程序测试它们。 iam() 第一个系统调用是iam(),其原型为: int iam(const char * name); 完成的功能是将...
  • 杭电操作系统实验三报告

    千次阅读 2019-03-24 00:26:43
    按理说操作系统实验应该自己做,这样能锻炼自己。鉴于我的报告还是比较有参考价值,能让以后的同学参考一下,就做成md的形式。 实验三报告 一、实验内容: (1)实验名:Linux进程管理 (2)实验要求: 1)实现一...
  • 课程名称 操作系统实验 课程编号 201406412 实验项目名称 实验环境的使用 学号 2017201212 班级 20172012 姓名 李博浩 专业 软件工程 学生所在学院 计算机科学与技术学院 指导教师 关键 实验室名称地点 21B476 ...
  • 在本学期(2017-2018学年第二学期)的操作系统实验课的作业时编写操作系统,在调试过程中遇到了极大问题,下面总结一下debug工具和方法。 我使用的是bochs+nasm+Mingw(主要是其中的gcc、objdump、objcopy、ld)。 ...
  • 参考文章:哈工大李治军老师操作系统实验-系统调用 内核层面的修改: 修改 include/unistd.h 文件添加 __NR_whoami 和 __NR_iam 两个宏 /* 添加系统调用号 */ #define __NR_whoami 72 #define __NR_iam 73 修改...
  • 杭电操作系统实验二报告

    千次阅读 2019-03-24 00:19:55
    按理说操作系统实验应该自己做,这样能锻炼自己。鉴于我的报告还是比较有参考价值,能让以后的同学参考一下,就做成md的形式。仅供参考 一、实验内容: (1)实验名:Linux内核模块编程 (2)实验要求: 1)设计一...
  • 操作系统实验-----文件系统 Y.xj 一,实验目的 ​ 本实验要求在假设的I/O系统之上开发一个简单的文件系统,这样做既能让实验者对文件系统有整体了解,又避免了涉及过多细节。用户通过create, open,read等命令与文件...
  • 操作系统实验报告——磁盘调度算法 1、实验名称: 磁盘调度算法的实现 2、实验要求: (1)理解磁盘调度的概念, (2)掌握磁盘调度程序的三种算法; (3)用C或C++语言编程实现算法。 3、实验方式: 通过上机,调试...
  • 实验名称:操作系统初步(系统调用实验)了解系统调用不同的封装形式实验原理API系统调用两者联系与区别系统调用号系统调用和函数调用的区别软中断int与函数调用call的区别Task 1实验过程实验结果Task 2实验过程Task...
  • Linux-0.11操作系统实验1-操作系统的引导 Linux-0.11操作系统实验2-系统调用 Linux-0.11操作系统实验3-进程运行轨迹的跟踪与统计 Linux-0.11操作系统实验4-基于内核栈切换的进程切换 Linux-0.11操作系统实验5-信号量...
  • 操作系统实验四 银行家算法

    万次阅读 多人点赞 2015-11-30 17:00:30
    操作系统实验四 银行家算法一、实验目的 1、 理解银行家算法。 2、 掌握进程安全性检查的方法与资源分配的方法。 二、实验内容与基本要求编制模拟银行家算法的程序,并以下面给出的例子验证所编写的程序的正确性...

空空如也

1 2 3 4 5 ... 20
收藏数 24,938
精华内容 9,975
关键字:

操作系统实验