写在前面

又到了 一年一度期末课设的时候,距离考试还有两周的时间,此时胡小宁还没有复习(准确的说是学习)之前的课程,所以在这个平平无奇的周一,胡小宁就要开始做课设了!

实验一 Windows 进程管理

  1. 实验目的
    (1)学会使用 VC 编写基本的 Win32 Consol Application(控制台应用程序)。
    (2)通过创建进程、观察正在运行的进程和终止进程的程序设计和调试操作,进一步熟悉操
    作系统的进程概念,理解 Windows 进程的“一生”。
    (3)通过阅读和分析实验程序,学习创建进程、观察进程、终止进程以及父子进程同步的基
    本程序设计方法。

  2. 实验内容和步骤

    (1) 编写基本的 Win32 Consol Application:

    指导书上的步骤有很多缺陷。我在Windows上做课设所使用的编译器是CodeBlocks,所以在操作和代码方面与指导书给的标准有些出入。这里创建的是Consol Application。

    代码如下:

    1
    2
    3
    4
    5
    6
    #include <iostream>
    using namespace std;
    int main()
    {
    cout << "Hello, Win32 Consol Application" << endl ;
    }

    这样就完成了编写基本的Consol Application。

    分析:按照指导书的操作运行会不成功,原因有二。一是编译器的不同,CodeBlocks不允许主函数无返回类型。二是从指导书上直接复制过来的代码有许多非法字符,非法空格,这些都需要更改。

    (2)创建进程

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <windows.h>
#include <iostream>
#include <stdio.h>
// 创建传递过来的进程的克隆过程并赋于其 ID 值
void StartClone(int nCloneID)
{
// 提取用于当前可执行文件的文件名
TCHAR szFilename[MAX_PATH] ;
GetModuleFileName(NULL, szFilename, MAX_PATH) ;
// 格式化用于子进程的命令行并通知其 EXE 文件名和克隆 ID
TCHAR szCmdLine[MAX_PATH];
sprintf(szCmdLine,"\"%s\" %d",szFilename,nCloneID);
// 用于子进程的 STARTUPINFO 结构
STARTUPINFO si;
ZeroMemory(&si , sizeof(si) ) ;
si.cb = sizeof(si) ; // 必须是本结构的大小
// 返回的用于子进程的进程信息
PROCESS_INFORMATION pi;
// 利用同样的可执行文件和命令行创建进程,并赋于其子进程的性质
BOOL bCreateOK=::CreateProcess(
szFilename, // 产生这个 EXE 的应用程序的名称
szCmdLine, // 告诉其行为像一个子进程的标志
NULL, // 缺省的进程安全性
NULL, // 缺省的线程安全性
FALSE, // 不继承句柄
CREATE_NEW_CONSOLE, // 使用新的控制台
NULL, // 新的环境
NULL, // 当前目录
&si, // 启动信息
&pi) ; // 返回的进程信息
// 对子进程释放引用
if (bCreateOK)
{
CloseHandle(pi.hProcess) ;
CloseHandle(pi.hThread) ;
}
}
int main(int argc, char* argv[] )
{
// 确定派生出几个进程,及派生进程在进程列表中的位置
int nClone=0;
//修改语句:int nClone;
//第一次修改:nClone=0;
if (argc > 1)
{
// 从第二个参数中提取克隆 ID
:: sscanf(argv[1] , "%d" , &nClone) ;
}
//第二次修改:nClone=0;
// 显示进程位置
std :: cout << "Process ID:" << :: GetCurrentProcessId()
<< ", Clone ID:" << nClone
<< std :: endl;
// 检查是否有创建子进程的需要
const int c_nCloneMax=5;
if (nClone < c_nCloneMax)
{
// 发送新进程的命令行和克隆号
StartClone(++nClone) ;
}
// 等待响应键盘输入结束进程
getchar();
return 0;
}

运行结果如下:

image-20210621161818916

修改nClone=1;

运行结果如下:

image-20210621161940179

修改nClone=2;

运行结果如下:

image-20210621162033540

如上是nClone取值不同所产生的不同运行结果。可以看出,进程ID起始值同nClone的值相同。如果将nClone的初始化位置换在下方,将会有非常严重的后果产生—进程创建的死循环!不一会儿你的电脑内存就炸了!!!

分析:程序从main函数开始,创建进程,每次引用该进程exe文件位置创建下一个进程(再反过来继续调用main函数,如此循环,使用c_nCloneMax作为限制)。因为调换了nClone的初始化位置,会使nClone永远达不到c_nCloneMax,造成死循环。

(3)父子进程的简单通信及终止进程

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// procterm 项目
# include <windows.h>
# include <iostream>
# include <stdio.h>
static LPCTSTR g_szMutexName = "w2kdg.ProcTerm.mutex.Suicide" ;
// 创建当前进程的克隆进程的简单方法
void StartClone()
{
// 提取当前可执行文件的文件名
TCHAR szFilename[MAX_PATH] ;
GetModuleFileName(NULL, szFilename, MAX_PATH) ;
// 格式化用于子进程的命令行,字符串“child”将作为形参传递给子进程的 main 函数
TCHAR szCmdLine[MAX_PATH] ;
//实验 1-3 步骤 3:将下句中的字符串 child 改为别的字符串,重新编译执行,执行前请先保存已经完成的工作
sprintf(szCmdLine, "\"%s\" child", szFilename) ;
// 子进程的启动信息结构
STARTUPINFO si;
ZeroMemory(&si,sizeof(si)) ;
si.cb = sizeof(si) ; // 应当是此结构的大小
// 返回的用于子进程的进程信息
PROCESS_INFORMATION pi;
// 用同样的可执行文件名和命令行创建进程,并指明它是一个子进程
BOOL bCreateOK=CreateProcess(
szFilename, // 产生的应用程序的名称 (本 EXE 文件)
szCmdLine, // 告诉我们这是一个子进程的标志
NULL, // 用于进程的缺省的安全性
NULL, // 用于线程的缺省安全性
FALSE, // 不继承句柄
CREATE_NEW_CONSOLE, //创建新窗口
NULL, // 新环境
NULL, // 当前目录
&si, // 启动信息结构
&pi ) ; // 返回的进程信息
// 释放指向子进程的引用
if (bCreateOK)
{
CloseHandle(pi.hProcess) ;
CloseHandle(pi.hThread) ;
}
}
void Parent()
{
// 创建“自杀”互斥程序体
HANDLE hMutexSuicide=CreateMutex(
NULL, // 缺省的安全性
TRUE, // 最初拥有的
g_szMutexName) ; // 互斥体名称
if (hMutexSuicide != NULL)
{
// 创建子进程
std :: cout << "Creating the child process." << std :: endl;
StartClone() ;
// 指令子进程“杀”掉自身
std :: cout << "Telling the child process to quit. "<< std :: endl;
//等待父进程的键盘响应
getchar() ;
//释放互斥体的所有权,这个信号会发送给子进程的 WaitForSingleObject 过程
ReleaseMutex(hMutexSuicide) ;
// 消除句柄
CloseHandle(hMutexSuicide) ;
}
}
void Child()
{
// 打开“自杀”互斥体
HANDLE hMutexSuicide = OpenMutex(
SYNCHRONIZE, // 打开用于同步
FALSE, // 不需要向下传递
g_szMutexName) ; // 名称
if (hMutexSuicide != NULL)
{
// 报告我们正在等待指令
std :: cout <<"Child waiting for suicide instructions. " << std :: endl;
//子进程进入阻塞状态,等待父进程通过互斥体发来的信号
WaitForSingleObject(hMutexSuicide, INFINITE) ;
//实验 1-3 步骤 4:将上句改为 WaitForSingleObject(hMutexSuicide, 0) ,重新编译执行
// 准备好终止,清除句柄
std :: cout << "Child quiting." << std :: endl;
CloseHandle(hMutexSuicide) ;
}
}
int main(int argc, char* argv[] )
{
// 决定其行为是父进程还是子进程
if (argc>1 && :: strcmp(argv[1] , "child" )== 0)
{
Child() ;
}
else
{
Parent() ;
}
return 0;
}

在利用CreateProcess函数创建进程时,命令行中需要szCmdLine来作为argv的参数创建这个子进程,两个参数是分开的, sprintf()函数中的”\”%s\” child”,%s填入的是可执行文件的路径,child则是创建子进程。两个参数之间必须加上空格才正确。如果只是抄过来指导书上的代码,将会进入死循环。内存炸了哦!

步骤3:第一次修改后,创建进程的第二个参数不是child了,因此子进程进入的是parent函数,所以程序在不断的创建子进程,死循环。

步骤4:第二次修改后,WaitForSingleObject(hMutexSuicide, INFINITE);INFINITE变成了0,所以会进入临界区,继续执行子进程,最后关闭子进程并关闭句柄。

整个程序要体现的是父子进程通信,父子进程的同步是由PV操作完成的。

P操作:WaitForSingleObject(hMutexSuicide, INFINITE);
V操作:ReleaseMutex(hMutexSuicide)

HANDLE hMutexSuicide = CreateMutex(NULL,TRUE,g_szMutexName);
互斥体实现了“互相排斥”(mutual exclusion)同步的简单形式(所以名为互斥体(mutex))。
互斥体禁止多个线程同时进入受保护的代码“临界区”(critical section)。
因此,在任意时刻,只有一个线程被允许进入这样的代码保护区。任何线程在进入临界区之前,
必须获取(acquire)与此区域相关联的互斥体的所有权。如果已有另一线程拥有了临界区的互斥体,其他线程就不能再进入其中。这些线程必须等待,直到当前的属主线程释放(release)该互斥体。

ReleaseMutex(hMutexSuicide);
一个线程释放了互斥对象的控制权后,如果其他进程在等待互斥对象置位,则等待的线程可以得到该互斥对象,等待函数返回,互斥对象被新的线程所拥有。并且会发送信号给waitforsingleobject

WaitForSingleObject(hMutexSuicide, INFINITE);
WaitForSingleObject函数用来检测hMutexSuicide事件的信号状态,在某一线程中调用该函数时,线程暂时挂起,如果在挂起的dwMilliseconds毫秒内,线程所等待的对象变为有信号状态,则该函数立即返回;如果超时时间已经到达dwMilliseconds毫秒,但hMutexSuicide所指向的对象还没有变成有信号状态,函数照样返回。
参数dwMilliseconds有两个具有特殊意义的值:0和INFINITE。若为0,则该函数立即返回;若为INFINITE,则线程一直为阻塞状态,直到hHandle所指向的对象变为有信号状态时为止。

OpenMutex() 打开已有的互斥体