1、创建进程
进程是计算机中正在运行的程序实例,它不同于存储在磁盘上的程序文件。程序是一组计算机指令的集合,而进程则是这些指令在执行时的实际体现。进程作为独立的运行单位,会申请并使用系统资源,如CPU时间、内存空间等。进程由内核对象和地址空间组成,内核对象用于操作系统管理进程,而地址空间则包括程序代码、数据和动态分配的内存。尽管进程是执行环境,但它本身并不执行任何操作,实际执行代码的是线程。线程在进程提供的环境中运行,完成具体的任务。因此,进程是线程执行的基础,而线程则是进程内执行代码的实体。
Windows操作系统的进程创建与控制机制是一个复杂而关键的部分,它确保了多任务环境中各个进程的独立运行和资源分配。进程是操作系统中运行程序的一个实例,是系统资源分配和调度的基本单位。每个进程都有自己独立的地址空间、状态信息(如打开的文件、内存映射等),以及一个或多个线程来执行代码。
在Windows环境下,进程的创建主要通过调用Win32 API中的CreateProcess函数实现。这个函数允许启动一个新的进程,并且可以控制其继承属性、优先级、环境变量等参数。CreateProcess函数的参数包括应用程序名称、命令行字符串、进程和线程的安全属性、是否继承父进程的属性、创建标志、新环境块的指针、当前目录名、传递给新进程的信息以及新进程返回的信息。
Windows操作系统通过对象管理器以一致的方法创建和管理所有的对象类型,包括进程。每个对象都有一个对象头和一个对象体,对象管理器控制对象头,各执行体组件控制它们自己创建的对象类型的对象体。进程在Windows中定义为表示操作系统所要做的工作,是操作系统用于组织其必须完成工作的手段。NT中的进程由一个可执行的程序、一个私用的虚地址空间、系统资源和至少一个执行线程等四部分组成。
进程控制块(PCB)是操作系统核心中的一种数据结构,主要表示进程状态。它的作用是使一个程序成为一个能够独立运行的基本单位,并且可以并发执行的进程。通过这些机制,Windows操作系统实现了进程的创建与管理,确保了多任务环境中各个进程的独立运行和资源分配。
在本文中,我们采用了基于MFC框架实现的processA,通过调用CreateProcess函数来创建processB。相关代码如下:
// 启动ProcessB,并传递读取端管道句柄
STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi;
si.cb = sizeof(si);
si.wShowWindow = SW_SHOW; // 显示窗口
si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES; // 启动标志
si.hStdInput = hReadPipe; // 将读取端传递给子进程
// 假设 ProcessB.exe 与 ProcessA 在同一目录下
CString strCmdLine = _T("./Debug/processB.exe");
// 创建 ProcessB 进程
if (!CreateProcess(NULL, strCmdLine.GetBuffer(), NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi))
{
AfxMessageBox(_T("CreateProcess failed."));
CloseHandle(hReadPipe);
CloseHandle(m_hWritePipe);
return;
}
此外还构建了一个简单的图形化界面,如图1.1所示。用户只需点击processA界面中的“创建子进程”按钮,即可成功创建process。
图 1.1 创建进程
2、消息通信实现进程间通信
消息队列实际上是操作系统在内核中为我们创建的一个队列,它通过一个唯一的标识符(key)来识别。每一个进程都可以通过这个标识符来打开相应的消息队列,并进行读写操作。这种机制允许不同的进程向队列中插入或获取消息,从而完成它们之间的通信,而不必关心彼此的直接连接。
具体来说,数据的传输过程是这样的:首先,用户组织一个带有类型的消息块,并将它添加到消息队列中。然后,其他的进程可以从队列中获取这个消息块,进而读取其中的数据。这里,消息队列就像是一个中介,它在生产者(发送消息的进程)和消费者(接收消息的进程)之间传递信息。这种设计模式的优势在于它实现了进程间的解耦合,使得系统的模块化程度更高,也更容易维护和扩展。
此外,消息队列还支持全双工通信,这意味着它可以同时进行数据的发送和接收。这种灵活性使得消息队列在各种复杂的通信场景下都能表现出色。
然而,值得注意的是,消息队列的生命周期是由操作系统内核管理的。除非有明确的指令来删除消息队列或者操作系统被关闭,否则消息队列会一直存在。这一点对于理解和管理系统资源是非常重要的。
综上所述,消息队列作为一种高效的进程间通信机制,通过在操作系统内核中维护一个队列,实现了不同进程之间的安全、可靠的消息传递。它的应用大大简化了分布式系统中的通信复杂性,提高了系统的整体性能和稳定性。
本文通过在MFC框架下自定义结构体COPYDATASTRUCT来模拟进程间通信方式。该结构体的定义如下:
typedef struct tagCOPYDATASTRUCT {
ULONG_PTR dwData;
DWORD cbData;
_Field_size_bytes_(cbData) PVOID lpData;
} COPYDATASTRUCT, *PCOPYDATASTRUCT;
通过在processA中绑定SendMessage按钮并实现以下函数,可以将processA中的消息发送到processB中:
LRESULT copyDataResult;
CString strDataToSend; // 要传输的内容GetDlgItemText(IDC_EDIT1, strDataToSend); // 从控件获取内容
CWnd* pOtherWnd = CWnd::FindWindow(_T("#32770"), _T("processB")); // 获取进程句柄if (pOtherWnd)
{
COPYDATASTRUCT cpd; // 用于传输到其他进程的结构体
cpd.cbData = 2 * strDataToSend.GetLength(); // 数据长度,注意乘2
cpd.lpData = strDataToSend.GetBuffer(); // 数据指针
copyDataResult = pOtherWnd->SendMessage(WM_COPYDATA, (WPARAM)AfxGetApp()->m_pMainWnd->GetSafeHwnd(), (LPARAM)&cpd); // 发送
strDataToSend.ReleaseBuffer(); // 销毁
}else
{
AfxMessageBox(_T("没有找到子进程"));
}
测试结果如图2.1所示。
图 2.1 SendMessage通信
3、共享内存实现通信
具体来说,共享内存就是允许两个不相关的进程访问同一个逻辑内存,它是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常为同一块物理内存。进程可以将同一物理内存连接到它们自己的地址空间中,所有的进程都可以访问共享内存中的地址。如果一个进程对共享内存写入数据,所做的改动将立即影响到可以访问同一共享内存的其他进程。
在页面初始化函数中,使用CreateFileMapping创建共享内存,代码如下:
HANDLE hFile = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE | SEC_COMMIT, 0, 1000000, _T("Data Mutex"));
if (!hFile)//判断是否成功创建
{
AfxMessageBox(_T("共享内存创建失败!"));
return TRUE;
}
if (GetLastError() == ERROR_ALREADY_EXISTS)
{
AfxMessageBox(_T("创建共享内存成功,准备写入数据"));
return TRUE;
}
LPBYTE lpData = (LPBYTE)MapViewOfFile(hFile, FILE_MAP_WRITE, 0, 0, 0);//写模式访问共享内存
if (!lpData)
{
AfxMessageBox(_T("写入数据失败"));
CloseHandle(hFile);
return TRUE;
}
TCHAR buf[1024] = _T("这是要写入的内容");//要写入的内容
memcpy(lpData, buf, sizeof(buf));//写入
在processA中,通过按钮“写数据到内存映像文件”绑定的函数将文本框中的内容写入共享内存,代码如下:
HANDLE hFile = OpenFileMapping(FILE_MAP_WRITE, FALSE, _T("Data Mutex"));
if (!hFile)
{
AfxMessageBox(_T("打开共享内存失败"));
return;
}
LPBYTE lpData = (LPBYTE)MapViewOfFile(hFile, FILE_MAP_WRITE, 0, 0, 0);//写模式访问共享内存
if (!lpData)
{
AfxMessageBox(_T("写入数据失败"));
CloseHandle(hFile);
return;
}
CString strtext;
GetDlgItemText(IDC_EDIT1, strtext);//获取EDIT控件内容
TCHAR buf[1024];
wcscpy_s(buf, strtext);//格式转换
memcpy(lpData, buf, sizeof(buf));//写入共享内存
}
ProcessB中按钮“映像内存读文件”绑定以下函数实现从共享内存中读取数据。
HANDLE hFile = OpenFileMapping(FILE_MAP_READ, FALSE, _T("Data Mutex"));
if (!hFile)
{
AfxMessageBox(_T("打开共享内存失败"));
return;
}
LPBYTE lpData = (LPBYTE)MapViewOfFile(hFile, FILE_MAP_READ, 0, 0, 0);
if (!lpData)
{
AfxMessageBox(_T("读取共享内存失败"));
CloseHandle(hFile);
return;
}
TCHAR buf[1024];
memset(buf, 0, sizeof(buf));
memcpy(buf, lpData, sizeof(buf));
SetDlgItemText(IDC_STATIC, buf);
if (lpData)
UnmapViewOfFile(lpData);
if (hFile)
CloseHandle(hFile);
这样processA和processB就可以通过访问同一个内存Data Mutex进行进程间的通信。在processA中输入“这里是消息A”,再点击“写数据到映像内存”就可以把消息写进去,processB中点击“映像内存读文件”就可以读取刚刚存入的消息,测试结果如图3.1。
图 3.1 共享内存通信
4、管道通信
管道是操作系统中用于进程间通信的一种机制,它允许一个进程将数据传递给另一个进程,从而实现信息的交换和共享。根据管道的使用方式和特性,可以将其分为匿名管道和命名管道。在Windows操作系统中,匿名管道通常用于父进程和子进程之间的单向通信,而命名管道则支持更复杂的通信模式,包括跨网络的通信。
匿名管道的创建通常发生在父进程和子进程之间。父进程负责创建一个匿名管道,并获取该管道的读句柄和写句柄。然后,父进程会创建一个子进程,并将管道的读句柄或写句柄传递给子进程,这样父子进程之间就可以通过管道进行数据传输。匿名管道是单向的,即一个进程写入数据,另一个进程读取数据。为了实现双向通信,需要创建两个管道,一个用于父进程到子进程的数据流,另一个用于子进程到父进程的数据流。
在创建匿名管道时,操作系统会分配一个缓冲区用于临时存储数据。写进程通过写句柄向管道写入数据,这些数据会被存储在缓冲区中。读进程则通过读句柄从缓冲区中读取数据。如果缓冲区已满,写操作会被阻塞,直到有足够的空间。同样,如果缓冲区为空,读操作会被阻塞,直到有数据可读。这种阻塞机制确保了数据的完整性和顺序性。
匿名管道的生命周期与创建它的进程紧密相关。通常,当创建管道的父进程终止时,管道也会被关闭。如果子进程还在运行,它将无法继续使用管道进行通信。因此,匿名管道的使用场景通常限制在父子进程之间的短期通信。
总结来说,匿名管道在Windows中主要用于父子进程之间的单向数据传输。通过父进程创建管道并传递句柄给子进程,进程之间可以实现简单的数据交换。匿名管道的单向性和生命周期特性使其适用于一些特定的进程间通信需求,比如命令行应用程序中的输入输出重定向。
为了方便操作的连贯性,这里在窗口processA初始化的时候就创建好了匿名管道,代码如下:
// 创建匿名管道
HANDLE hReadPipe;
SECURITY_ATTRIBUTES saAttr;
saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
saAttr.bInheritHandle = TRUE; // 使句柄可继承
saAttr.lpSecurityDescriptor = NULL;
if (!CreatePipe(&hReadPipe, &m_hWritePipe, &saAttr, 0))
{
AfxMessageBox(_T("CreatePipe failed."));
return;
}
在processA中输入数据之后再点击“写入数据到管道”就可以完成数据的写入,然后在processB中点击“从管道中接受数据”就可以读入数据。
ProcessA中按钮函数:
// 检查管道句柄是否有效
if (m_hWritePipe == INVALID_HANDLE_VALUE || m_hWritePipe == NULL)
{
AfxMessageBox(_T("The write pipe handle is invalid."));
return;
}
// 获取编辑框中的文本
CString strText;
GetDlgItemText(IDC_EDIT1, strText);
// 获取文本的长度
DWORD dwTextLength = strText.GetLength() * sizeof(TCHAR);
// 写入数据到管道
DWORD dwWritten;
BOOL bSuccess = WriteFile(m_hWritePipe, strText, dwTextLength, &dwWritten, NULL);
// 检查写入是否成功
if (!bSuccess || dwWritten != strText.GetLength() * sizeof(TCHAR))
{
AfxMessageBox(_T("WriteFile failed or incomplete."));
}
else
{
AfxMessageBox(_T("Data written to pipe successfully."));
}
ProcessB按钮函数:
// 从标准输入读取数据
HANDLE hStdInput = GetStdHandle(STD_INPUT_HANDLE);
if (hStdInput == INVALID_HANDLE_VALUE)
{
AfxMessageBox(_T("GetStdHandle failed."));
return;
}
TCHAR buffer[1024];
DWORD dwRead;
if (!ReadFile(hStdInput, buffer, sizeof(buffer), &dwRead, NULL))
{
AfxMessageBox(_T("ReadFile failed."));
return;
}
CString strText(buffer, dwRead / sizeof(TCHAR));
// 将读取的数据设置到编辑框中
SetDlgItemText(IDC_STATIC, strText);
依次输入内容和点击按钮就有结果如图4.1。
图 4.1 管道通信
代码打包如下: