在这里插入图片描述

📃个人主页:island1314

⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞

  • 生活总是不会一帆风顺,前进的道路也不会永远一马平川,如何面对挫折影响人生走向 – 《人民日报》


实验一 Windows 进程管理

一、背景知识

🔥 Windows所创建的每个进程都从调用 CreateProcess()API函数开始,该函数的任务是在对象管理器子系统内初始化进程对象。每一进程都以调用ExitProcess()或 TerminateProcess()API函数终止。通常应用程序的框架负责调用ExitProcess()函数。对于C++运行库来说,这一调用发生在应用程序的 main0) 函数返回之后。

(1)创建进程

CreateProcess() 调用的核心参数是可执行文件运行时的文件名及其命令行。表 1-1详细地列出了每个参数的类型和名称。

在这里插入图片描述

​ 可以指定第一个参数,即应用程序的名称,其中包括相对于当前进程的当前目录的全路径或者利用搜索方法找到的路径;IpCommandLine 参数允许调用者向新应用程序发送数据;接下来的三个参数与进程和它的主线程以及返回的指向该对象的句柄的安全性有关。

  • 然后是标志参数,用以在 dwCreationFlags 参数中指明系统应该给予新进程什么行为。
  • 经常使用的标志是 CREATE SUSPNDED,告诉主线程立刻暂停。
  • 当准备好时,应该使用 ResumeThread()API 来启动进程。
  • 另一个常用的标志是 CREATE NEWCONSOLE,告诉新进程启动自己的控制台窗口,而不是利用父窗口。这一参数还允许设置进程的优先级,用以向系统指明,相对于系统中所有其他的活动进程来说,给此进程多少CPU时间。

接着是 CreateProcess 函数调用所需要的三个通常使用缺省值的参数。

  • 第一个参数是 IpEnvironment 参数,指明为新进程提供的环境;
  • 第二个参数是 IpCurrentDirectory,可用于向主创进程发送与缺省目录不同的新进程使用的特殊的当前目录;
  • 第三个参数是 STARTUPINFO 数据结构所必需的,用于在必要时指明新应用程序的主窗口的外观。
  • CreateProcess() 的最后一个参数是用于新进程对象及其主线程的句柄和DD的返回值缓冲区。
  • PROCESS INFORMATION 结构中返回的句柄调用CloseHandle() API函数是重要的,因为如果不将这些句柄关闭的话,有可能危及主创进程终止之前的任何未释放的资源。

(2)正在运行进程

如果一个进程拥有至少一个执行线程,则为正在系统中运行的进程。通常,这种进程使用主线程来指示它的存在。当主线程结束时,调用ExitProcess()API 函数,通知系统终止它所拥有的所有正在运行、准备运行或正在挂起的其他线程。

当进程正在运行时,可以查看它的许多特性,其中少数特性也允许加以修改。

  • 首先可查看的进程特性是系统进程标识符(PID),可利用 GetCurentProcessId() API数来查看,与 GetCurrentProcess() 相似,对该函数的调用不能失败,但返回的 PID 在整个系统中都可使用。
  • 其他的可显示当前进程信息的 API函数还有 GetStartupInfo()GetProcessShutdownParameters(),可给出进程存活期内的配置详情。

通常,一个进程需要它的运行期环境的信息。

  • 例如API函数 GetModuleFileName()GetCommandLine() ,可以给出用在 CreateProcess() 中的参数以 启动应用程序。在创建应用程序时可使用的另一个API函数是 IsDebuggerPresent()

  • 可利用 API函数 GetGuiResources()查看进程的GUI资源。此函数既可返回指定进程中的打开的GUI对象的数目,也可返回指定进程中打开的 USER对象的数目。

  • 进程的其他性能信息可通过 GetProcessloCounters()GetProcessPriorityBoost()GetProcessTimes()GetProcessWorkingSetSize()API得到。以上这几个API函数都只需要具有 PROCESSOUERYINFORMATION 访问权限的指向所感兴趣进程的句柄

  • 另一个可用于进程信息查询的 API函数是 GetProcessVersion()。此函数只需感兴趣进程的 PID(进程标识号)。这一API函数与GetVersionEx() 的共同作用,可确定运行进程的系统的版本号。

(3)终止进程

所有进程都是以调用 ExitProcess() 或者 TerminateProcess() 函数结束的。

  • 但最好使用前者而不要使用后者,因为进程是在完成了它的所有的关闭“职责”之后以正常的终止方式来调用前者的。而外部进程通常调用后者即突然终止进程的进行,由于关闭时的途径不太正常,有可能引起错误的行为。

  • TerminateProcess() API 函数只要打开带有 PROCESS TERMINATE 访问权的进程对象,就可以终止进程,并向系统返回指定的代码。这是一种“野蛮”的终止进程的方式,但是有时却是需要的。

(4)进程同步

Windows 提供了多种同步对象(synchronization objects),如事件对象(event)、互斥对象(mutex)、 信号量对象(semaphore)等,来实现进程之间的同步。本实验只涉及mutex和semaphore。

相关知识,请参考:

事件(Event):用于不同线程间的信号通知,包括 单次通知事件重复通知事件 两种类型。

  • 事件对象可以通过 CreateEvent函数创建
  • 使用 SetEvent 函数将其设置为有信号状态,使用 ResetEvent 函数将其设置为无信号状态。
  • WaitForSingleObiect 或 WaitForMultipleObiects 函数可以等待事件对象的状态变化。

互斥量(Mutex):用于控制对共享资源的访问,具有独占性,防止线程之间对共享资源的非法访问。互斥量确保同一时间只有一个线程可以访问共享资源

  • 利用 CreateMutex() API可创建互斥体,创建时还可以指定一个初始的拥有权标志,通过使用这个标志,只有当线程完成了资源的所有的初始化工作时,才允许创建线程释放互斥体。
  • 获得互斥体流程如下
    • 线程可使用 OpenMutex() API来获得指向对象的句柄;
    • 然后线程将这个句柄提供给一个等待函数。
    • 当内核将互斥体对象发送给等待线程时,就表明该线程获得了互斥体的拥有权。
    • 当线程获得拥有权时,线程控制了对共享资源的访问。
    • 最后放弃共享资源时需要在该对象上调用 ReleaseMute() API。
    • 然后系统负责将互斥体拥有权传递给下一个等待着的线程(由到达时间决定顺序)。

信号量(Semaphore):基于计数器机制控制并发资源的访问数量。信号量可以允许多个线程同时访问共享资源,但总数有限制。

  • 利用CreateSemaphore()API来创建信号量对象
  • WaitForSingleObiect() 用于等待信号量对象变为可用(即信号量的计数大于0),该函数就是信号量的P操作。
  • ReleaseSemaphore() 用于释放信号量对象,增加信号量的计数,相当于信号量的V操作。

二、实验目的

(1)学会使用 VC 编写基本的 Win32 Consol Application(控制台应用程序)。

(2)通过创建进程、观察正在运行的进程和终止进程的程序设计和调试操作,进一步熟悉操作系统的进程概念,理解 Windows 进程的“一生”。

(3)通过阅读和分析实验程序,学习创建进程、观察进程、终止进程以及父子进程同步的基本程序设计方法。

三、实验内容

(1)编写基本的 Win32 Consol Application

  • 步骤 1:登录进入 Windows 系统,启动 dev C++。
  • 步骤 2:在“FILE”菜单中单击“NEW”子菜单,在“Files”选项卡中选择“C++ Source File”, 然后在“File” 处输入C++源程序的文件名。
  • 步骤 3:将相关代码的程序清单复制到新创建的C++源程序中,编译成可执行文件。
  • 步骤 4:在“开始”菜单中单击“程序”-“附件”-“命令提示符”命令,进入 Windows“命令提示符”窗口,执行编译好的可执行程序,列出运行结果。

代码如下

# include <iostream>
int main()
{
 std::cout << "“Hello, Win32 Consol Application”" << std :: endl ;
 return 0;
}

(2) 创建进程

本实验显示了创建子进程的基本框架。该程序只是再一次地启动自身,显示它的系统进程 ID和它在进程列表中的位置。

  • 步骤 1:创建一个“Win32 Consol Application”工程,然后拷贝清单 2-2 中的程序,编译成可执行4文件。
  • 步骤 2:在“命令提示符”窗口运行步骤 1 中生成的可执行文件,列出运行结果。按下 ctrl+alt+del,调用 windows 的任务管理器,记录进程相关的行为属性。
  • 步骤 3:在“命令提示符”窗口加入参数重新运行生成的可执行文件,列出运行结果。按下ctrl+alt+del,调用 windows 的任务管理器,记录进程相关的行为属性。
  • 步骤 4:修改清单 2-2 中的程序,将 nClone 的定义和初始化方法按程序注释中的修改方法进行修改,编译成可执行文件(执行前请先保存已经完成的工作)。再按步骤 2 中的方式运行,看看结果会有什么不一样。列出行结果。

代码如下

#define _CRT_SECURE_NO_WARNINGS
#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((char*)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;
	//修改语句:int nClone;
	//第一次修改:nClone=0;
	if (argc > 1)
	{
		// 从第二个参数中提取克隆 ID
		::sscanf(argv[1], "%d", &nClone);
	}
	nClone = 0;
	//第二次修改: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;
}

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

  • 步骤 1:创建一个“Win32 Consol Application”工程,然后拷贝清单 2-3 中的程序,编译成可执行文件。
  • 步骤 2:在 VC 的工具栏单击“Execute Program”(执行程序) 按钮,或者按 Ctrl + F5 键,或者在“命令提示符”窗口运行步骤 1 中生成的可执行文件,列出运行结果。
  • 步骤 3:按源程序中注释中的提示,修改源程序 2-3,编译执行(执行前请先保存已经完成的工作),列出运行结果。在程序中加入跟踪语句,或调试运行程序,同时参考 MSDN 中的帮助文件CreateProcess()的使用方法,理解父子进程如何传递参数。给出程序执行过程的大概描述。
  • 步骤 4:按源程序中注释中的提示,修改源程序 2-3,编译执行,列出运行结果。
  • 步骤 5:参考MSDN 中 的 帮 助 文 件 CreateMutex() 、OpenMutex() 、 ReleaseMutex() 和WaitForSingleObject()的使用方法,理解父子进程如何利用互斥体进行同步的。

代码如下

#define _CRT_SECURE_NO_WARNINGS
#include <windows.h>
#include <iostream>
#include <stdio.h>
static LPCTSTR g_szMutexName = (LPCTSTR)"w2kdg.ProcTerm.mutex.Suicide";
// 创建当前进程的克隆进程的简单方法
void StartClone()
{
	// 提取当前可执行文件的文件名
	TCHAR szFilename[MAX_PATH];
	GetModuleFileName(NULL, szFilename, MAX_PATH);
	// 格式化用于子进程的命令行,字符串“child”将作为形参传递给子进程的 main 函数
	TCHAR szCmdLine[MAX_PATH];
	//实验 2-3 步骤 3:将下句中的字符串 child 改为别的字符串,重新编译执行,执行前请先保存已经完成的工作
	sprintf((char*)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, 0);
		//实验 2-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;
}

四、实验结果和分析

实验1-1

image-20250609165603392

实验1-2

image-20250609170250833

第一次修改,将nClone的创建与赋值分开之后,运行结果同上。

第二次修改,在51行增加语句“nclone=0”,运行将无限生成子进程。

  • 分析:可能是生成子进程的过程是父进程生成一个子进程,然后子进程再“克隆”自己生成一个子进程,如此反复,直到到达代码中规定的const int c_nCloneMax=5;完成程序的执行,但是一直不会完成,就会无限生成进程。
  • nClone的值控制着创建的进程数。变量的定义和初始化对程序的执行结果有影响,因为如果初始化在判定语句内,程序会受其影响,可能会无限创建进程。

​ 第一次修改时在if条件判断之前,此时nClone=0会被覆盖。第二次修改则是在if判断之后,其会覆盖掉if中的赋值,将nClone置为0,此时比较nClone < c_nCloneMax会恒成立,然后就会无限生成子进程,同时子进程的nClone参数永远都是1。

实验1-3

image-20250609171308138

第一次修改,将字符串 child 改为别的字符串,重新编译执行,无限生成子进程。

第二次更改,将WaitForSingleObject(hMutexSuicide, INFINITE) 修改为WaitForSingleObject(hMutexSuicide, 0),再重新运行,子进程一闪而过,运行后直接终止,只有主进程存在。

  • 分析:这段代码描述了一个使用进程间同步的程序。程序从main()函数开始执行,首先检查argc的值,即命令行参数的数量。如果argc不大于1,说明没有提供额外的命令行参数,程序将执行parent()函数。在parent()函数中,程序调用StartClone()来创建一个新的进程。接着,使用sprintf()函数来构建一个命令行字符串szCmdLine,这个字符串包含了程序名称和一个参数"child",这个参数是从argv[1]获取的。当满足一定条件后,程序将调用child()函数。
  • 程序使用了一个互斥信号量hMutexSuicide来同步进程。这意味着在同一时间只有一个进程可以执行关键部分。父进程在完成其任务后,会释放这个互斥信号量,子进程检测到互斥信号量被释放后,才会结束进程。

​ 在第一次修改中,如果将sprintf()中的参数"child"更改为其他字符串,那么在child()函数中用于判断终止条件的字符串将不再匹配,导致子进程无法正确终止,从而不断地创建新的子进程。

​ 在第二次修改中,将WaitForSingleObject()函数的第二个参数从INFINITE更改为0。这个函数用于等待互斥信号量变为可用状态。INFINITE表示无限期等待,即直到互斥信号量可用为止。而改为0后,函数将不会等待,如果互斥信号量不可用,它会立即返回。这导致子进程在尝试获取互斥信号量时,如果它不可用,就会立即退出,而不会等待父进程释放信号量,从而出现“一闪而过”的现象。

实验二 Linux 进程控制

一、背景知识

​ 在 Linux 中创建子进程要使用fork()函数,执行新的命令要使用 exec()系列函数,等待子进程结束使用 wait0)函数,结束终止进程使用 exit() 函数。

  • fork() 原型如下:pid tfork(void);
  • fork 建立一个子进程,父进程继续运行,子进程在同样的位置执行同样的程序。对于父进程forkO返回子进程的 pid,对于子进程,fork() 返回0。出错时返回-1。

exec系列有6个函数,原型如下: extern char **environ

  1. int execl( const char *path, const char *arg, …);
  2. int execlp( const char *file, const char *arg, …),
  3. int execle( const char *path, const char *arg , ., char * const enyp[]);
  4. int execv( const char *path, char *const argv[]);
  5. int execve (const char *filename, char *const argv l, char *const envp);
  6. int execvp( const char *file, char *const argv);

exec系列函数用新的进程映象置换当前的进程映象,这些函数的第一个参数是待执行程序的路径名(文件名)。这些函数调用成功后不会返回,其进程的正文(text),数据(data)和栈(stack)段被待执行程序程序覆盖。但是进程的 PID 和所有打开的文件描述符没有改变,同时悬挂信号被清除,信号重置为缺省行为。

在函数 execl、execlp和 execle 中,const char *arg 以及省略号代表的参数可被视为 arg0,argl,…argn。它们合起来描述了指向 NULL结尾的字符串的指针列表,即执行程序的参数列表。作为约定第一个 arg 参数应该指向执行程序名自身,参数列表必须用NULL 指针结束。

execv 和execvp 函数提供指向NULL结尾的字符串的指针数组作为新程序的参数列表。作为约定,指针数组中第一个元素应该指向执行程序名自身。指针数组必须用NULL指针结束。

execle 函数同时说明了执行进程的环境(environment),它在 NULL指针后面要求一个附加参数NULL,指针用于结束参数列表,或者说,argv数组。这个附加参数是指向NULL结尾的字符串的指针数组,它必须用NULL指针结束。其它函数从当前进程的environ外部变量中获取新进程的环境

execlp和execvp可根据path搜索合适的程序运行,其它则需要给出程序全路径。Execve类似execv,但是加上了环境的处理。

Wait() 和 waitpid()可用来等待子进程结束。函数原型:

#include <sys/wait.h>
pid t wait(int *stat loc);
pid t waitpid(pid t pid, int *stat loc,int options);

当进程调用 wait,它将进入睡眠状态直到有一个子进程结束。wait 函数返回子进程的进程 id,stat loc 中返回子进程的退出状态。

waitpid 的第一个参数 pid 的意义:

  • pid > 0:等待进程id为 pid 的子进程。
  • pid == 0:等待与自己同组的任意子进程
  • pid == -1:等待任意一个子进程
  • pid < -1:等待进程组号为-pid 的任意子进程。

因此,wait(&stat)等价于 waitpid(-1,&stat,0),waitpid 第三个参数 option可以是0,WNOHANGWUNTRACED 或这几者的组合。

二、实验目的

通过进程的创建、撤销和运行加深对进程概念和进程并发执行的理解,明确进程和程序之间的 区别。

三、 实验内容

(1)任务一:进程的创建

任务要求:编写一段程序,使用系统调用fork(创建一个子进程。当此程序运行时,在系统中有一个父进程和一个子进程活动。让每一个进程在屏幕上分别显示字符:父进程显示字符“b”;子进程显示字符“a”,另外父子进程都显示字符“c”。

  • 步骤 1:使用 vi 或 gedit 新建一个 fork demo.c程序,使用 cc 或者 gcc 编译成可执行文件fork dem0。例如,可以使用 gcc-ofork demo fork demo.c 完成编译。
  • 步骤 2:在命令行输入./fork demo 运行该程序。
  • 步骤 3:多次运行程序,观察屏幕上的显示结果,并分析多次运行为什么会出现不同的结果。

代码如下

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <algorithm>

int main()
{
    srand(time(nullptr));
    pid_t id = fork();
    if(id < 0) printf("Error\n");
    else if(id == 0){
        sleep(rand() % 2);
        printf("a\n");
    }
    else{
        sleep(rand() % 3);
        printf("b\n");
    }
    printf("c\n");
    return 0;
}

(2)任务二:子进程执行新任务

任务要求:编写一段程序,使用系统调用fork(创建一个子进程。子进程通过系统调用exec更换自己原有的执行代码,转去执行 Linux 命令/bin/ls(显示当前目录的列表),然后调用 exit()函数结束。父进程则调用waitpid0等待子进程结束,并在子进程结束后显示子进程的标识符,然后正常结束。程序执行过程如图 2-1所示。

  • 步骤1:使用vi或 gedit 新建一个 exec demo.c程序,使用cc或者gcc编译成可执行文件exec demo。例如,可以使用 gcc -oexec demoexec demo.c完成编译。
  • 步骤2:在命令行输入./exec demo运行该程序。
  • 步骤3:观察该程序在屏幕上的显示结果,并分析。

image-20250609091354840

代码如下

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
    pid_t id = fork();
    if(id < 0) printf("Error\n");
    else if(id == 0){
        execlp("/bin/ls", "ls", nullptr);
        exit(0);
    }
    else{
        waitpid(-1, nullptr, 0); // 阻塞等待
        printf("Child Complete\n");
    }
    return 0;
}

结果如下

island@VM-8-10-ubuntu:~/code$ ./code
code  code.cc  Makefile
Child Complete

四、实验结果和分析

(1)任务一:进程的创建

image-20250609162648131

根据上图多次运行程序的显示结果,发现每次产生的结果具有随机性。

分析:该程序使用了 while 循环和 fork() 函数来创建进程。在循环中,调用 fork() 函数创建一个新进程,当返回值为 0,说明当前进程是子进程,随机休眠一段时间后打印字符 “a”;如果返回值不为 0,则说明当前进程是父进程,随机休眠一段时间后打印字符 “b”。最后,无论是父进程还是子进程,都会打印字符 “c”。由于父进程和子进程有各自独立的代码路径,它们可能会以不同的顺序打印字符 “a”、“b” 和 “c”,这展示了进程之间的并发执行。

(2)任务二:子进程执行新任务

image-20250609162925054

分析:该程序使用了 fork() 函数创建一个新进程,然后根据返回值判断是是子进程还是父进程。如果返回值为 0,说明当前进程是子进程,它调用execlp()函数执行一个新的程序;如果返回值不为 0,则说明当前进程是父进程,它通过调用 wait(NULL) 函数等待子进程结束,然后打印 “Child Complete” 。execlp()函数是用来执行新任务的。它会取代当前进程的映像,运行指定的可执行文件,并传递给它参数。我们使用 /bin/ls 命令显示当前目录下的所有文件和子目录。

图解如下

子进程id==0

父进程id > 0

main 开始

fork

execlp

执行ls命令输出目录内容

子进程退出

waitpid

阻塞等待子进程完成

Child Complete

main 返回 0

实验三 Linux 进程间通信

一、背景知识

fork 系统调用的相关背景知识参见实验二。 UINX/Linux 系统把信号量、消息队列和共享资源统称为进程间通信资源(IPC resource)。 提供给用户的IPC资源是通过一组系统调用实现的。

这组系统调用为用户态进程提供了以下三种服务:

  • 用信号量对进程要访问的临界资源进行保护。
  • 用消息队列在进程间以异步方式发送消息。
  • 用一块预留出的内存区域供进程之间交换数据。

创建IPC资源的系统调用有:

  • semget() —获得信号量的IPC标识符。
  • msgget() —获得消息队列的IPC标识符。
  • shmget() —获得共享内存的IPC标识符。

控制IPC资源的系统调用有:

  • semctl() —对信号量资源进行控制的函数。
  • msgctl() —对消息队列进行控制的函数。
  • shmctl() —对共享内存进行控制的函数。

上述函数为获得和设置资源的状态信息提供了一些命令。例如:

  • IPC_SET命令:设置属主的用户标识符和组标识符。
  • IPC_STAT和IPC_INFO命令:获得资源状态信息。
  • IPC_RMID命令:释放这个资源。

操作IPC资源的系统调用有:

  • semop() —获得或释放一个IPC信号量。 可以实现P、V操作
  • msgsnd() —发送一个IPC消息。
  • msgrcv() —接收一个IPC消息。
  • shmat() —将一个IPC共享内存段添加到 进程的地址空间
  • shmdt() ——将IPC共享内存段从私有的地址空间剥离。

下面对部分系统调用说明如下:

msgget(key,flag)

  • 功能:获得一个消息的描述符,该描述符指定一个消息队列以便用于其他系统调用。

  • 该函数使用如下:

    # include <sys/types.h> 
    #include <sys/ipc.h> 
    #include <sys/msg.h> 
    
    // 参数定义:
     int msgget(key,flag)  
     key_t key; 
     int flag; 
    
    // 语法格式:msgqid=msgget(key,flag) 
    
  • 其中: msgqid 是该系统调用返回的描述符,失败则返回-1;

  • flag 本身由操作允许权和控制命令值相“或”得到 如:IPC_CREAT | 0400 是否该队列应被创建; IPC_EXCL | 0400

msgsnd(id,msgp,size,flag)

  • 功能:发送一消息。

  • 该函数使用如下:

    #include <sys/types.h> 
    #include <sys/ipc.h> 
    #include <sys/msg.h> 
    
    // 参数定义: 是否该队列的创建映象是互斥的; 
    int msgsnd(id,msgp,size,flag) 
    int id,size,flag; 
    struct msgbuf *msgp; 
    
  • 其中:id 是返回消息队列的描述符;msgp 是指向用户存储区的一个构造体指针,size 由 msgp 指向的数据结构中字符数组的长度,即消息的长度。

  • 这个数组的最大值由 MSG_MAX 系统可调用参数来确定。

  • flag 规定当核心用尽内部缓冲空间时应执行的动作; 若在标志范围 flag 中未设置 IPC_NOWAIT 位,则当该消息队列中的字节数超过一最大值 时,或系统范围的消息数超过某一最大值时,调用 msgsnd 进程睡眠。若是设置 IPC_NOWAIT ,则在此情况下,msgsnd 立即返回。

msgrcv(id,msgp,size,flag)

  • 功能:接受一消息。

  • 该函数使用如下:

    #include <sys/types.h> 
    #include <sys/ipc.h> 
    #include <sys/msg.h> 
    
    // 参数定义: 
    int msgctl(id,cmd,buf) 
    int id,cmd; 
    struct msqid_ds *buf;
    
  • 其中:函数调用成功时返回0,调用不成功时返回-1。

  • id 用来识别该消息的描述符;cmd 规定命令的类型。

  • IPC_STAT 将与id 相关联的消息队列首标读入buf。

  • IPC_SET 为这个消息序列设置有效的用户和小组标识及操作允许权和字节的数量。

  • IPC_RMID 删除id 的消息队列。 buf 是含有控制参数或查询结果的用户数据结构的地址。

msgid_ds 结构定义如下:

struct msgid_ds
{
	struct ipc_perm msg_perm;  /* 许可权结构 */
	short pad1[7];     /* 由系统调用 */
	ushort onsg_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;    /* 当前用户id */
	ushort gid;    /* 当前进程组id */
	ushort cuid;    /* 创建用户id */
	ushort cgid;
	ushort mode;
	/* 创建进程组id */
	/* 存取许可权 */
	{short pad1; long pad2} /* 由系统调用 */
};

二、实验目的

Linux 系统的进程通信机构(IPC)允许在任意进程间大批量地交换数据,通过本实验,理解熟悉 Linux 支持的消息通信机制。

三、实验内容

消息的创建、发送和接收的程序设计

  1. 步骤 1:为了便于操作和观察结果,用一个程序作为“引子”,先后 fork()两个子进程 SERVER 和 CLIENT,进行通信。
  2. 步骤 2:SERVER 端建立一个 key 为75 的消息队列,等待其他进程发来的消息。当遇到类 型为 1 的消息,则作为结束信号,取消该队列,并退出 SERVER。SERVER 每接收到一个消息后显示一句“(server)received”
  3. 步骤 3:CLIENT 端使用 key 为 75 的消息队列,先后发送类型从 10 到 1的消息,然后退出。 最后的一个消息,即是 SERVER 端需要的结束信号。CLIENT每发送一条消息后显示一句“(client)sent”
  4. 步骤 4:父进程在 SERVER 和 CLIENT 均退出后结束

代码如下

#include <stdio.h> 
#include <sys/types.h> 
#include <sys/msg.h> 
#include <sys/ipc.h> 
#include <sys/wait.h> 
#include <sys/ipc.h> 
#include <stdlib.h>   
#include <unistd.h> 
#define MSGKEY 75 
struct msgform
{
    long mtype;
    char mtext[300];
}msg;
int msgqid, i;
void CLIENT()
{
    int i, result;
    msgqid = msgget(MSGKEY, 0777);
    if (msgqid == -1)
    {
        perror("client msgget");
        exit(1);
    }
    for (i = 0; i < 10; i++){
        printf("(client) sent \n");
        result = msgsnd(msgqid, &msg, 300, 0);
        if (result == -1){
            perror("client msgsnd");
            exit(1);
        }
    }
    exit(0);
}
void SERVER()
{
    int i;
    int getsize = 0;

    msgqid = msgget(MSGKEY, 0777);
    if (msgqid == -1){
        perror("server msgget");
        exit(1);
    }
    for (i = 0; i < 10; i++){
        getsize = msgrcv(msgqid, &msg, 300, 0, 0);
        printf("(Server) recieved %d bytes\n", getsize);
    };
    msgctl(msgqid, IPC_RMID, 0);
    exit(0);
}
void main()
{
    msgqid = msgget(MSGKEY, 0777 | IPC_CREAT);//777是权限控制bit,IPC_creat表示创建 
    if (msgqid == -1)
    {
        perror("main msgget");
    }
    msg.mtype = 1;//必须是非负数,否则报错 
    while ((i = fork()) == -1);
    if (!i) SERVER();
    while ((i = fork()) == -1);
    if (!i) CLIENT();
    wait(0);
    wait(0);
}

四、实验结果和分析

(1)编译并运行程序,描述执行结果:“(client)sent”和“(server)received”各出现了几次,以 什么顺序出现,解释出现这种结果的原因

image-20250609164621871

分析:(client)sent在结果中出现了10次,(server)received在结果中出现了10次。出现几个(client)sent连续后再接着出现几个(server)received,是因为message的传送和控制并不保证完全同步,当一个程序不在激活状态的时候,它完全能继续睡眠,造成了上述的现象,在多次发送message的时候才接受信息。

(2)请修改源代码,实现乒乓通信,即 client发送第一条消息,server接收到第一条消息后,client 再发送第二条消息,server 接收第二条消息,依此类推,表现为在端口中交替显示“(client)sent” 和“(server)received”。

修改如下:

void CLIENT()
{
    msgqid = msgget(MSGKEY, 0777);
    if (msgqid == -1){
        perror("client msgget");
        exit(1);
    }
    for (int i = 0; i < 10; i++){
        msg.mtype = 1;
        printf("(client) sent \n");
        int result = msgsnd(msgqid, &msg, 300, 0);
        if (result == -1){
            perror("client msgsnd");
            exit(1);
        }
        // 等待服务器响应
        if(msgrcv(msgqid, &msg, 300, 2, 0) == -1){ // 接收 mtype = 2 的消息
            perror("client msgrcv");
            exit(1);
        }
    }
    exit(0);
}
void SERVER()
{
    msgqid = msgget(MSGKEY, 0777);
    if (msgqid == -1){
        perror("server msgget");
        exit(1);
    }
    for (int i = 0; i < 10; i++){
        int getsize = msgrcv(msgqid, &msg, 300, 1, 0);
        printf("(Server) recieved %d bytes\n", getsize);
        // 发送响应信息
        msg.mtype = 2;
        if(msgsnd(msgqid, &msg, 300, 0) == -1){ // 发送 mtype = 2 的消息
            perror("server msgsnd");
            exit(1);
        }
    };
    msgctl(msgqid, IPC_RMID, 0);
    exit(0);
}

结果如下

(client) sent 
(Server) recieved 300 bytes
...
(client) sent 
(Server) recieved 300 bytes

实验四 Windows 线程同步与互斥

二、实验目的

(1)回顾操作系统进程、线程的有关概念,加深对 Windows 线程的理解。

(2)了解互斥体对象,利用互斥与同步操作编写生产者-消费者问题的并发程序,加深对 P(即semWait)、V(即 semSignal)原语以及利用 P、V 原语进行进程间同步与互斥操作的理解。

三、实验内容

生产者消费者问题

  • 步骤 1:创建一个“Win32 Consol Application”工程,然后拷贝清单 5-1 中的程序,编译成可执行文件。
  • 步骤 2:在“命令提示符”窗口运行步骤 1 中生成的可执行文件,列出运行结果。
  • 步骤 3:仔细阅读源程序,找出创建线程的 WINDOWS API 函数,回答下列问题:线程的第一个执行函数是什么(从哪里开始执行)?它位于创建线程的 API 函数的第几个参数中?
  • 步骤 4:修改清单 5-1 中的程序,调整生产者线程和消费者线程的个数,使得消费者数目大与生产者,看看结果有何不同。察看运行结果,从中你可以得出什么结论?
  • 步骤 5:修改清单 5-1程序,按程序注释中的说明修改信号EmptySemaphore 的初始化方法,看看结果有何不同。
  • 步骤 6:根据步骤 4 的结果,并查看 MSDN,回答下列问题:

1)CreateMutex 中有几个参数,各代表什么含义。

2)CreateSemaphore 中有几个参数,各代表什么含义,信号量的初值在第几个参数中。

3)程序中 P、V 原语所对应的实际 Windows API 函数是什么,写出这几条语句。

4)CreateMutex 能用 CreateSemaphore 替代吗?尝试修改程序 5-1,将信号量 Mutex 完全用CreateSemaphore 及相关函数实现。写出要修改的语句。

代码如下

#include <windows.h>
#include <iostream>
const unsigned short SIZE_OF_BUFFER = 2; //缓冲区长度
unsigned short ProductID = 0; //产品号
unsigned short ConsumeID = 0; //将被消耗的产品号
unsigned short in = 0; //产品进缓冲区时的缓冲区下标
unsigned short out = 0; //产品出缓冲区时的缓冲区下标
int buffer[SIZE_OF_BUFFER]; //缓冲区是个循环队列
bool p_ccontinue = true; //控制程序结束
HANDLE Mutex; //用于线程间的互斥
HANDLE FullSemaphore; //当缓冲区满时迫使生产者等待
HANDLE EmptySemaphore; //当缓冲区空时迫使消费者等待
DWORD WINAPI Producer(LPVOID); //生产者线程
DWORD WINAPI Consumer(LPVOID); //消费者线程
int main()
{
	//创建各个互斥信号
   //注意,互斥信号量和同步信号量的定义方法不同,互斥信号量调用的是 CreateMutex 函数,同步信号量调用的是 CreateSemaphore 函数,函数的返回值都是句柄。
	Mutex = CreateMutex(NULL, FALSE, NULL);
	EmptySemaphore = CreateSemaphore(NULL, SIZE_OF_BUFFER, SIZE_OF_BUFFER, NULL);
	//将上句做如下修改,看看结果会怎样
   //EmptySemaphore = CreateSemaphore(NULL,0,SIZE_OF_BUFFER-1,NULL);
	FullSemaphore = CreateSemaphore(NULL, 0, SIZE_OF_BUFFER, NULL);
	//调整下面的数值,可以发现,当生产者个数多于消费者个数时,
	//生产速度快,生产者经常等待消费者;反之,消费者经常等待
	const unsigned short PRODUCERS_COUNT = 3; //生产者的个数
	const unsigned short CONSUMERS_COUNT = 1; //消费者的个数
	//总的线程数
	const unsigned short THREADS_COUNT = PRODUCERS_COUNT + CONSUMERS_COUNT;
	HANDLE hThreads[THREADS_COUNT]; //各线程的 handle
	DWORD producerID[PRODUCERS_COUNT]; //生产者线程的标识符
	DWORD consumerID[CONSUMERS_COUNT]; //消费者线程的标识符
	//创建生产者线程
	for (int i = 0; i < PRODUCERS_COUNT; ++i) {
		hThreads[i] = CreateThread(NULL, 0, Producer, NULL, 0, &producerID[i]);
		if (hThreads[i] == NULL) return -1;
	}
	//创建消费者线程
	for (int ii = 0; ii < CONSUMERS_COUNT; ++ii) {

		hThreads[PRODUCERS_COUNT + ii] = CreateThread(NULL, 0, Consumer, NULL, 0, &consumerID[ii]);
		if (hThreads[ii] == NULL) return -1;
	}
	while (p_ccontinue) {
		if (getchar()) { //按回车后终止程序运行
			p_ccontinue = false;
		}
	}
	return 0;
}
//生产一个产品。简单模拟了一下,仅输出新产品的 ID 号
void Produce()
{
	std::cout << std::endl << "Producing " << ++ProductID << " ... ";
	std::cout << "Succeed" << std::endl;
}
//把新生产的产品放入缓冲区
void Append()
{
	std::cerr << "Appending a product ... ";
	buffer[in] = ProductID;
	in = (in + 1) % SIZE_OF_BUFFER;
	std::cerr << "Succeed" << std::endl;
	//输出缓冲区当前的状态
	for (int i = 0; i < SIZE_OF_BUFFER; ++i) {
		std::cout << i << ": " << buffer[i];
		if (i == in) std::cout << " <-- 生产";
		if (i == out) std::cout << " <-- 消费";
		std::cout << std::endl;
	}
}
//从缓冲区中取出一个产品
void Take()
{
	std::cerr << "Taking a product ... ";
	ConsumeID = buffer[out];
	buffer[out] = 0;
	out = (out + 1) % SIZE_OF_BUFFER;
	std::cerr << "Succeed" << std::endl;
	//输出缓冲区当前的状态
	for (int i = 0; i < SIZE_OF_BUFFER; ++i) {
		std::cout << i << ": " << buffer[i];
		if (i == in) std::cout << " <-- 生产";
		if (i == out) std::cout << " <-- 消费";
		std::cout << std::endl;
	}
}
//消耗一个产品
void Consume()
{
	std::cout << "Consuming " << ConsumeID << " ... ";
	std::cout << "Succeed" << std::endl;
}
//生产者
DWORD WINAPI Producer(LPVOID lpPara)
{
	while (p_ccontinue) {
		WaitForSingleObject(EmptySemaphore, INFINITE); //p(empty);
		WaitForSingleObject(Mutex, INFINITE); //p(mutex);
		Produce();
		Append();
		Sleep(1500);
		ReleaseMutex(Mutex); //V(mutex);
		ReleaseSemaphore(FullSemaphore, 1, NULL); //V(full);
	}
	return 0;
}
//消费者
DWORD WINAPI Consumer(LPVOID lpPara)
{
	while (p_ccontinue) {
		WaitForSingleObject(FullSemaphore, INFINITE);//P(full);
		WaitForSingleObject(Mutex, INFINITE); //P(mutex);
		Take();
		Consume();
		Sleep(1500);
		ReleaseMutex(Mutex); //V(mutex);
		ReleaseSemaphore(EmptySemaphore, 1, NULL); //V(empty);
	}
	return 0;
}

四、实验结果和分析

结果如下:

image-20250609192423488

结果分析:生产者生产一个产品,生产产品放在当入缓冲区,此时指针指向下一个缓冲区,然后当生产完第一个产品,未满之后,FullSemaphore 加 1,EmptySemaphore 减1,继续生产第二个产品,直到缓冲区满了,才开始通知消费者进行生产。消费者进行消费时,消费者先从当前缓冲区取出一个产品,然后指向下一个缓冲区,直至缓冲区为空,再通知生产者进行生产。

  • 生产阶段 :
    • Producing 1 ... Succeed 表示生产者生成产品1。
    • Appending a product ... Succeed 显示产品1被放入缓冲区,此时 buffer[0] = 1in 指针移动到位置1(in = (0 + 1) % 2 = 1)。
    • 缓冲区状态显示 0: 1 <-- 消费(out 指向位置0),1: 0 <-- 生产(in 指向位置1)
  • 消费阶段 :
    • Taking a product ... Succeed 表示消费者取出产品1(ConsumeID = 1),buffer[0] 被清零,out 移动到位置1((0 + 1) % 2 = 1)。
    • Consuming 1 ... Succeed 显示消费成功,此时 FullSemaphore 减1,EmptySemaphore 加1

流程图如下

消费者线程 (Consumer)

生产者线程 (Producer)

主线程控制

开始 main()

初始化同步对象
(Mutex, EmptySemaphore, FullSemaphore)

创建多个生产者和消费者线程

循环 p_ccontinue == true

循环 p_ccontinue == true

等待用户输入 (getchar)

设置 p_ccontinue = false
程序准备退出

等待空位
WaitForSingleObject(EmptySemaphore)

请求独占访问
WaitForSingleObject(Mutex)

Produce() & Append()
生产产品并放入缓冲区

释放独占访问
ReleaseMutex(Mutex)

通知有新产品
ReleaseSemaphore(FullSemaphore, 1, NULL)

线程结束

等待产品
WaitForSingleObject(FullSemaphore)

请求独占访问
WaitForSingleObject(Mutex)

Take() & Consume()
取出产品并消费

释放独占访问
ReleaseMutex(Mutex)

通知有新空位
ReleaseSemaphore(EmptySemaphore, 1, NULL)

线程结束

步骤 3 回答:线程的第一个执行函数是 Producer 或 Consumer,且它们是创建线程 API 函数的第三个参数

步骤 4 回答:调整生产者线程和消费者线程的个数,使得消费者数目大于生产者,看看结果有何不同,如下:

image-20250609194033572

结果如下:

image-20250609194046043

  • 结论:刚开始时运行结果类似,但是逐渐发现消费者会经常因为资源不足而等待生产者,可能会出现资源争夺情况

步骤 5 回答:修改清单 5-1程序,按程序注释中的说明修改信号EmptySemaphore 的初始化方法,看看结果有何不同。

  • 结果:没有任何生成,因为此时给信号量的初始计数值设定为0,导致没有资源可用

步骤6 回答

1)CreateMutex 中有几个参数,各代表什么含义 (有三个参数,含义如下:)

  • 第一个参数 lpMutexAttributes :指向安全属性的指针(通常为 NULL,表示默认安全设置)
  • 第二个参数 bInitialOwner :指定互斥体的初始所有权。若为 TRUE,则创建它的线程立即拥有该互斥体;若为 FALSE,则不拥有。
  • 第三个参数 lpName :互斥体的名称(用于跨进程共享)。若为 NULL,则创建无名互斥体。

2)CreateSemaphore 中有几个参数,各代表什么含义,信号量的初值在第几个参数中(有四个参数,含义如下:)

  • 第一个参数 lpSemaphoreAttributes :指向安全属性的指针(通常为 NULL
  • 第二个参数 lInitialCount :信号量的初始计数值(即可用资源数)。这是信号量的初值所在参数
  • 第三个参数 lMaximumCount :信号量的最大计数值(即最大可用资源数)
  • 第四个参数 lpName :信号量的名称(用于跨进程共享)。若为 NULL,则创建无名信号量

3)程序中 P、V 原语所对应的实际 Windows API 函数是什么,写出这几条语句。

  • P 原语对应 WaitForSingleObject函数,用于等待信号量或互斥体变为有信号状态
  • V 原语 对应 ReleaseMutexReleaseSemaphore 函数,用于释放互斥体或信号量

4)CreateMutex 能用 CreateSemaphore 替代吗?尝试修改程序 5-1,将信号量 Mutex 完全用 CreateSemaphore 及相关函数实现。写出要修改的语句。

  • 可以用替代,修改代码: Mutex = CreateMutex(NULL,FALSE,NULL) 改为 Mutex = CreateSemaphore(NULL,1,1,NULL),将ReleaseMutex(Mutex)改为ReleaseSemaphore(Mutex,1,NULL)。

实验五 内存管理

一、背景知识

Windows Xp 是 32 位的操作系统,它使计算机 CPU 可以用 32 位地址对 32 位内存块进行操作。 内存中的每一个字节都可以用一个 32 位的指针来寻址。这样,最大的存储空间就是 2 32 2^{32} 232 字节或 4G字节。这样,在 Windows 下运行的每一个应用程序最大可能占有 4GB 大小的空间。

​ 然而,实际上每个进程一般不会占有 4GB 内存。Windows 在幕后将虚拟内存 (virtual memory,VM) 地址映射到了各进程的物理内存地址上。而所谓物理内存是指计算机的 RAM 和由 Windows分配到用户驱动器根目录上的换页文件。物理内存完全由系统管理。

​ 在 Windows 环境下,4GB 的虚拟地址空间被划分成两个部分:低端 2GB 提供给进程使用,高端 2GB 提供给系统使用。这意味着用户的应用程序代码,包括 DLL 以及进程使用的各种数据等,都装在用户进程地址空间内 (低端 2GB) 。

​ 如何管理虚拟地址空间?Windows内存管理器维持了一种数据结构,以记住哪些虚拟地址已被保留在进程的地址空间中,而哪些还没有。这些数据结构称为虚拟地址描述符(virtual addressdescriptors,VAD)。对于每个进程,Windows 内存管理器维护了一组 VAD,以描述进程地址空间的状态。VAD 被构造成一棵自平衡二叉树(self-balancing binary tree),以使查找高效。

image-20250609201718957

​ 虚拟地址空间被分为很多region,一个 region 是一个虚拟地址连续的区域,可以包含很多页,一个region 由一个数据结构 VAD进行描述和管理。VAD 是一个结构体,包含 region 的范围、大小状态、权限等信息。图 5-1摘自《Windows Internals (第6版)》,描绘了一棵由 committed 和 reserved 状态的 VAD 组成的二叉树,如果一个页的地址范围不在树中,则它的状态是 free。

状态方面,VAD规定了一个region可以处于以下三种状态(state)之一:

  1. 空置 (free) :没有对应的VAD。操作系统认为这段虚拟地址空间对于该进程而言,当前是不可 用的,如果进程访问这个region中的地址,将引发一个异常。在未来,操作系统可以根据需要,为free region 分配一个VAD,将转换为reserved或committed。
  2. 提交(committed):操作系统认为这段虚拟地址空间对于该进程而言,当前是可用的,如果进 程访问这个region 中的地址,操作系统要么从物理内存中找到进程需要的内容,要么通过交换从硬 盘中找到进程需要的内容
  3. 保留(reserved):堆和栈是动态增长的,而且要求虚拟地址连续。当栈比较小时,可以将其后 续地址所在的 region 宣布为 reserved,从而为栈保留一定范围的虚拟地址以便将来变大后使用。如果进程访问reserved region 中的地址,将引发一个异常。

Reserved 可以理解为从free到committed的中间阶段,通过将free-committed这一个步骤,拆分 为free-reserved 和 reserved-committed 两个步骤,将“提交”延迟到被进程需要的最后时刻,可以降 低内存使用量,同时拥有虚拟地址连续的便利性。

​ 权限方面,VAD 规定了进程可在内存中进行何种类型的操作。例如,进程不能在只有 PAGE_READONLY 权限的区域上进行写操作或执行程序;也不能在只有 PAGE_EXECUTE 权限的 区域里进行读、写操作。而具有PAGE_NOACCESS权限的特殊区域,不允许进程进行任何操作。

​ 在进程装入之前,整个虚拟内存的地址空间都被设置为只有PAGE_NOACCESS权限的free区域。当系统装入进程代码和数据后,才将内存地址的空间标记为已调配区或保留区,并将诸如 EXECUTE、READWRITE和READONLY的权限与这些区域相关联。

Windows 提供了一整套API来访问VAD及相关管理结构,如表5-1所示。

在这里插入图片描述

其中Virtual QueryEX() API 填充的 MEMORY_BASIC_INFORMATION 结构,具体内容如表5-2 所示,其大部分信息都搜集自VAD。

在这里插入图片描述

​ MEMORY_BASIC_INFORMATION 结构,State 项表明这些区域是否为 free 区、commited 或 reserved 区;Protect 项则说明了 Windows系统为这些区域添加了何种访问保护;Type项则表明这些 区域是可执行镜像image、内存映射文件mapped还是简单的私有内存private。 提供虚拟内存分配功能的是VirtualAlloc()API。该 API支持用户向系统要求新的虚拟内存或改变 已分配内存的当前状态。

用户若想通过 VirtualAlloc() 函数使用虚拟内存,可以采用两种方式通知系 统:

  1. 简单地将内存内容保存在地址空间内;
  2. 请求系统返回带有物理存储区 (RAM的空间或换页文件) 的部分地址空间。

用户可以用 flAllocation Type 参数 (commit 和 reserve) 来定义这些方式,用户可以通知Windows 按只读、读写、不可读写、执行或特殊方式来处理新的虚拟内存。

  • 与 VirtualAlloc() 函数对应的是VirtualFree() 函数,其作用是释放虚拟内存中的已调配页或保留页。
  • 用户可利用dwFree Type参数将已调配页修改成保留页属性。
  • VirtualProtect() 是 VirtualAlloc() 的一个辅助函数,利用它可以改变虚拟内存区的保护规范。

二、实验目的

​ 了解Windows的内存结构和虚拟内存的管理,理解进程的虚拟内存空间和物理内存的映射关系。加深对操作系统内存管理、虚拟存储管理等理论知识的理解

三、实验内容

步骤与方法:了解和检测进程的虚拟内存空间

  • 步骤1:创建一个“实验五”工程,拷贝清单6-1中的程序,编译成可执行文件。
  • 步骤2:运行步骤1中生成的可执行文件。
  • 步骤3:根据运行结果,回答下列问题(在结果与分析中)。按 committed、reserved、free 等三种虚拟地址空间分别记录实验数据。其中“描述”是指对该组数据的简单描述。

代码如下

// 工程 vmwalker
#include <windows.h>
#include <iostream>
#include <shlwapi.h>
#include <iomanip>
#include<stdio.h>
#include<limits.h>
#pragma comment(lib, "Shlwapi.lib")

// 以可读方式对用户显示保护的辅助方法。
// 保护标记表示允许应用程序对内存进行访问的类型
// 以及操作系统强制访问的类型
inline bool TestSet(DWORD dwTarget, DWORD dwMask) {
	return ((dwTarget & dwMask) == dwMask);
}
# define SHOWMASK(dwTarget, type) \
	if (TestSet(dwTarget, PAGE_##type) ) \
	{std :: cout << ", " << #type; }
void ShowProtection(DWORD dwTarget) {
	SHOWMASK(dwTarget, READONLY);
	SHOWMASK(dwTarget, GUARD);
	SHOWMASK(dwTarget, NOCACHE);
	SHOWMASK(dwTarget, READWRITE);
	SHOWMASK(dwTarget, WRITECOPY);
	SHOWMASK(dwTarget, EXECUTE);
	SHOWMASK(dwTarget, EXECUTE_READ);
	SHOWMASK(dwTarget, EXECUTE_READWRITE);
	SHOWMASK(dwTarget, EXECUTE_WRITECOPY);
	SHOWMASK(dwTarget, NOACCESS);
}
// 遍历整个虚拟内存并对用户显示其属性的工作程序的方法
void WalkVM(HANDLE hProcess) {
	// 首先,获得系统信息
	SYSTEM_INFO si;
	::ZeroMemory(&si, sizeof(si));
	::GetSystemInfo(&si);
	// 分配要存放信息的缓冲区
	MEMORY_BASIC_INFORMATION mbi;
	::ZeroMemory(&mbi, sizeof(mbi));
	// 循环整个应用程序地址空间
	LPCVOID pBlock = (LPVOID)si.lpMinimumApplicationAddress;
	while (pBlock < si.lpMaximumApplicationAddress) {
		// 获得下一个虚拟内存块的信息
		if (::VirtualQueryEx(
			hProcess, // 相关的进程
			pBlock, // 开始位置
			&mbi, // 缓冲区
			sizeof(mbi)) == sizeof(mbi)) { // 大小的确认
			// 计算块的结尾及其大小
			LPCVOID pEnd = (PBYTE)pBlock + mbi.RegionSize;
			TCHAR szSize[MAX_PATH];
			::StrFormatByteSize(mbi.RegionSize, szSize, MAX_PATH);
			// 显示块地址和大小
			std::cout.fill('0');
			std::cout
				<< std::hex << std::setw(8) << (DWORD64)pBlock
				<< "-"
				<< std::hex << std::setw(8) << (DWORD64)pEnd
				<< (::strlen((char*)szSize) == 7 ? " (" : " (") << szSize
				<< ") ";
			// 显示块的状态
			switch (mbi.State) {
			case MEM_COMMIT:
				std::cout << "Committed";
				break;
			case MEM_FREE:
				std::cout << "Free";
				break;
			case MEM_RESERVE:
				std::cout << "Reserved";
				break;
			}
			// 显示保护
			if (mbi.Protect == 0 && mbi.State != MEM_FREE) {
				mbi.Protect = PAGE_READONLY;
			}
			ShowProtection(mbi.Protect);
			// 显示类型
			switch (mbi.Type) {
			case MEM_IMAGE:
				std::cout << ", Image";
				break;
			case MEM_MAPPED:
				std::cout << ", Mapped";
				break;
			case MEM_PRIVATE:
				std::cout << ", Private";
				break;
			}
			// 检验可执行的影像
			TCHAR szFilename[MAX_PATH];
			if (::GetModuleFileName(
				(HMODULE)pBlock, // 实际虚拟内存的模块句柄
				szFilename, //完全指定的文件名称
				MAX_PATH) > 0) { //实际使用的缓冲区大小
				// 除去路径并显示
				::PathStripPath(szFilename);
				std::cout << ", Module: " << szFilename;
			}
			std::cout << std::endl;
			// 移动块指针以获得下一下个块
			pBlock = pEnd;
		}
	}
}
void ShowVirtualMemory() {
	// 首先,让我们获得系统信息
	SYSTEM_INFO si;
	::ZeroMemory(&si, sizeof(si));
	::GetSystemInfo(&si);
	// 使用外壳辅助程序对一些尺寸进行格式化
	TCHAR szPageSize[MAX_PATH];
	::StrFormatByteSize(si.dwPageSize, szPageSize, MAX_PATH);
	DWORD dwMemSize = (DWORD64)si.lpMaximumApplicationAddress -
		(DWORD64)si.lpMinimumApplicationAddress;
	TCHAR szMemSize[MAX_PATH];
	::StrFormatByteSize(dwMemSize, szMemSize, MAX_PATH);
	// 将内存信息显示出来
	std::cout << "Virtual memory page size: " << szPageSize << std::endl;
	std::cout.fill('0');
	std::cout << "Minimum application address: 0x"
		<< std::hex << std::setw(8)
		<< (DWORD64)si.lpMinimumApplicationAddress
		<< std::endl;
	std::cout << "Maximum application address: 0x"
		<< std::hex << std::setw(8)
		<< (DWORD64)si.lpMaximumApplicationAddress
		<< std::endl;
	std::cout << "Total available virtual memory: "
		<< szMemSize << std::endl;
}
int main() {
	//显示虚拟内存的基本信息
	ShowVirtualMemory();
	// 遍历当前进程的虚拟内存
	::WalkVM(::GetCurrentProcess());
	return 0;
}

四、实验结果和分析

下面结果在 VS 跑出来结果不一样,然后我是在 DevC++ 跑出来的,但是就需要改一下 DevC++ 的编译选项,加入 -std=c++11 -fpermissive -lshlwapi ,如下:

结果如下:
bfab50275ae02b2b9156ee4fe4f1717c
结果运行如下:
在这里插入图片描述

问题回答如下

  • 为什么显示的地址都以 3 个 0 结尾:虚拟内存以 页(Page) 为单位进行管理,常见页大小为 4KB(4096 字节) 。由于 4KB 的十六进制表示为 0x1000,因此每个内存块的起始地址必须对齐到 0x1000 的整数倍,导致地址的最后三位十六进制数始终为 0
  • 虚拟内存每页容量为: 4.00kb,Windows 系统默认页大小为 4KB,与 Linux 一致
  • 应用程序有权限访问(读或写)的最小地址: 0x00010000(这是 32 位系统中用户模式默认的起始地址)
  • 应用程序有权限访问(读或写)的最大地址: 0x7fffffeffff (这是 32 位系统中用户模式的上限地址(约 2GB))
  • 应用程序有权限访问(读或写)的虚拟地址空间大小为:3.99GB(理论每个 Windows 应用程序可以独占的最大存储空间是 4G )
  • 当前计算机的实际内存大小为: 16G

​ 按committed、reserved、free 等三种虚拟地址空间分别记录实验数据。其中“描述”是指对该组数 据的简单描述,例如,对下列一组数据: 00010000 – 00012000 <8.00KB> Committed, READWRITE, Private 可描述为:具有READWRITE权限的已提交私有内存区。

表 1 系统分区自由区(free)虚拟地址空间表记录如下:

地址 大小 虚拟地址空间类型 访问权限 描述
00011000-00020000 60KB Free NOACCESS 未分配的自由内存区
00021000-00030000 60KB Free NOACCESS 未分配的自由内存区

表 2 已调配区(committed)虚拟地址空间表记录如下:

地址 大小 虚拟地址空间类型 访问权限 描述
00010000-00011000 4.00 KB Committed READWRITE, Mapped 可读写的数据段(可能为堆或栈)
00020000-00030000 64.0 KB Committed READWRITE, Mapped 可读写的动态数据段
00030000-00050000 128 KB Committed READONLY, Mapped 只读的共享映射区域
00050000-00054000 16.0 KB Committed READONLY, Mapped 只读的共享资源(如图标、字符串表)
00060000-00062000 8.00 KB Committed READWRITE, Private 可读写的私有内存(线程栈)
00070000-000a1000 196 KB Committed READONLY, Mapped 只读的共享库或内存池
000f0000-000f1000 4.00 KB Committed READONLY, Mapped 只读的共享资源(如图标、字符串表)
00400000-00401000 4.00 KB Committed READONLY, Image 只读的可执行映像 模块.exe
00401000-00473000 456 KB Committed EXECUTE_READ, Image 可执行且只读的代码段
00473000-00474000 4.00 KB Committed READWRITE, Image 可读写的动态数据段
00474000-00485000 68.0 KB Committed WRITECOPY, Image 写时复制的代码段
004af000-00501000 328 KB Committed READONLY, Image 只读的资源段
00708000-0070b000 12.0 KB Committed GUARD, READWRITE, Private 带保护的可读写私有内存

表 3 系统分区保留区(reserved)虚拟地址空间表记录如下:

地址 大小 虚拟地址空间类型 访问权限 描述
00011000-00012000 4.00 KB Reserved READONLY, Mapped 保留的映射内存(可能用于动态加载)
00102000-001c3000 772 KB Reserved READONLY, Private 保留的私有内存(可能用于堆扩展)
00200000-0028f000 572 KB Reserved READONLY, Private 保留的私有内存(可能用于堆分配)
0028f000-00298000 36.0 KB Reserved READONLY, Mapped 保留的映射内存(共享库或内存池)
00298000-00400000 1.40 MB Reserved READONLY, Private 保留的私有内存(可能用于线程栈)
004a9000-004ac000 12.0 KB Reserved READONLY, Image 保留的只读代码段
00c97000-00ca0000 36.0 KB Reserved READONLY, Private 保留的私有内存(线程栈预留)
028e2000-028f0000 56.0 KB Reserved READONLY, Private 保留的私有内存(系统内核对象)

在这里插入图片描述

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐