进程间通信(IPC,Inter-Process Communication)是指在不同进程之间的数据传输。进程是操作系统分配资源和调度的独立单位,每个进程都有自己独立的地址空间,一个进程无法直接访问另一个进程的数据。因此,当需要数据共享或任务协作时,就必须使用IPC机制。
常见的进程间通信方法
常见的进程间通信方法有:
- 管道(Pipe)
- 消息队列
- 共享内存
- 信号量
- 套接字
下面,我们将详细介绍管道的原理以及具体实现。
什么是管道?
管道是共享内存的一部分,用于促进进程之间的通信。
Windows支持两种类型的管道:匿名管道和命名管道。
这两种管道虽然在使用场景和功能上有所区别,但底层实现原理都是基于操作系统的内核管理,涉及内存管理、数据同步和进程间安全通信。
原理
管道是一种存储在文件系统之外的文件,没有名称或任何其他特定属性。但它有两个文件描述符,我们可以像处理文件一样处理它。
内核空间的数据缓冲区
Windows操作系统中的管道通信依赖于内核空间中的缓冲区来暂存从一个进程输出的数据,直到另一个进程读取这些数据。这些缓冲区完全由操作系统的内核控制,并且与任何特定进程的用户空间内存隔离。
- 数据的写入和读取
- 当一个进程使用
WriteFile
系统调用向管道写入数据时,数据首先从该进程的用户空间复制到内核空间的缓冲区。 - 另一个进程通过
ReadFile
系统调用从管道中读取数据,实际上是从内核空间的缓冲区获取数据。
- 缓冲区关联
- 缓冲区的大小通常在创建管道时指定,这影响数据传输的效率和进程阻塞的行为。
- 如果缓冲区已满,写操作会阻塞,直到有足够的空间可用;如果缓冲区为空,读操作同样会阻塞,直到缓冲区中有数据可读。
通过管道写入的数据不存在任何进程的内存中,而是由操作系统内核管理的内存。
文件描述符和句柄
Windows系统使用文件句柄来代表管道。这些句柄提供了对管道的引用,使得应用程序可以通过标准的文件I/O接口与管道交互。
- 句柄的作用:句柄抽象了对管道的操作,允许进程通过调用如
CreatePipe
(创建匿名管道)或CreateNamedPipe
(创建命名管道)等API来创建和管理管道。
同步和异步操作
为了支持高校的数据传输,Windows的管道机制支持同步和异步操作模式。这些模式决定了操作系统如何处理进程间的数据传输和进程的执行状态。
- 同步操作
- 在同步模式下,进程在执行读写操作时如果遇到阻塞(如等待数据可用或缓冲区有空间),则进程会挂起,直到操作可以继续为止。
- 异步操作
- 异步操作允许进程继续执行其他任务,而不是在管道操作完成前阻塞。这是通过使用重叠I/O实现的,其中管道操作与进程的其他活动并行进行。
安全性和访问控制
Windows的管道机制包括复杂的安全属性和访问控制列表(ACL),这些功能确保了管道通信的安全性。
- 安全属性:在创建命名管道时,可以指定安全属性,这些属性定义了哪些用户或进程可以访问管道,以及他们的访问权限。
匿名管道和命名管道
匿名管道
- 用途和特点
- 匿名管道通常用于父子进程间的通信。
- 只支持半双工通信(单向数据流),即数据只能在一个方向上流动。
- 匿名管道没有名称,因此不能跨越不相关的进程使用,仅在有共同祖先的进程间有效
- 实现和使用
- 在类Unix系统下,匿名管道通过
pipe()
创建 - 在Windows系统下,通过
CreatePipe()
创建 - 由于管道是无名的,所以它们在文件系统中没有表示
- 生命周期
- 匿名管道的生命周期通常与创建它们的进程相绑定,当这些进程结束或显式地关闭管道时,管道也会被销毁。
命名管道
- 用途和特点
- 命名管道可以在没有直接关系的进程之间使用,例如不同用户运行的进程。
- 支持全双工通信(双向数据流)。
- 命名管道通过文件系统中的名称识别,这允许不同的进程通过打开同一个名字的管道来进行通信。
- 实现和使用
- 在类Unix系统下,通过
mkfifo()
创建命名管道。 - 在Windows系统下,通过
CreateNamedPipe()
创建命名管道。 - 命名管道可以配置为持久存在,即使创建它的进程已经终止。
- 网络通信
- 命名管道除了支持单机内不同进程间的通信外,也可以配置用于网络中不同计算机间的通信。
匿名管道和命名管道的主要区别
- 生命周期:匿名管道通常在进程结束时销毁,而命名管道可以独立于创建它的进程存在。
- 通信范围:匿名管道仅限于有共同祖先的进程间通信,而命名管道可以在任何能访问给定名称的进程间使用。
- 通信能力:匿名管道仅支持半双工通信,命名管道支持全双工通信。
- 命名和可见性:匿名管道无名,只能通过文件描述符或句柄在相关进程间传递;命名管道有固定名称,通过文件系统访问。
实现
这里只实现Windows系统下的命名通道。
注意:
Windows系统下,命名通道的名称有规范限制:
\\.\pipe\
作为管道名称的前缀,后面跟着通道的具体名称,例如:
\\.\pipe\Mypipe
服务端程序代码
#include <windows.h>
#include <iostream>
int pipeServer()
{
HANDLE hPipe;
/*
* Windows系统下,管道名称命名规范:
* \\.\pipe\pipename
* 其中'\\.\pipe\'是固定的前缀,表示这是一个命名管道
*/
LPCWSTR pipeName = L"\\\\.\\pipe\\MyPipe";
char buffer[1024];
DWORD bytesRead;
DWORD bytesWritten;
BOOL success;
// 创建命名管道
hPipe = CreateNamedPipe(
pipeName, // 管道名称
PIPE_ACCESS_DUPLEX, // 双向访问
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, // 数据类型和读取模式
1, // 最大实例数
1024, // 输出缓冲区大小
1024, // 输入缓冲区大小
0, // 客户端超时时间
NULL // 默认安全属性
);
if (hPipe == INVALID_HANDLE_VALUE) {
std::cerr << "Failed to create pipe." << std::endl;
return 1;
}
std::cout << "Named pipe created successfully." << std::endl;
// 等待客户端连接
std::cout << "Waiting for client connection..." << std::endl;
success = ConnectNamedPipe(hPipe, NULL) ?
TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
if (success) {
std::cout << "Client connected." << std::endl;
}
else {
CloseHandle(hPipe);
std::cerr << "Failed to make connection on named pipe." << std::endl;
return 1;
}
// 读写循环
while (true) {
// 从客户端读取消息
success = ReadFile(
hPipe,
buffer,
sizeof(buffer),
&bytesRead,
NULL
);
if (!success || bytesRead == 0) {
if (GetLastError() == ERROR_BROKEN_PIPE) {
std::cerr << "Client disconnected." << std::endl;
}
else {
std::cerr << "Read failed." << std::endl;
}
break;
}
buffer[bytesRead] = '\0'; // 确保字符串结束符
std::cout << "Client says: " << buffer << std::endl;
// 检查是否退出
if (strcmp(buffer, "quit") == 0) {
std::cout << "Client initiated termination." << std::endl;
break;
}
// 发送响应到客户端
const char* response = "Hello, client!";
success = WriteFile(
hPipe,
response,
(DWORD)strlen(response),
&bytesWritten,
NULL
);
if (!success) {
std::cerr << "Failed to send data." << std::endl;
break;
}
}
// 关闭句柄
CloseHandle(hPipe);
std::cout << "Connection closed." << std::endl;
return 0;
}
int main()
{
pipeServer();
std::cin.get();
}
输出结果:
客户端程序代码
#include <windows.h>
#include <iostream>
int pipeClient() {
HANDLE hPipe;
/*
* Windows系统下,管道名称命名规范:
* \\.\pipe\pipename
* 其中'\\.\pipe\'是固定的前缀,表示这是一个命名管道
*/
LPCWSTR pipeName = L"\\\\.\\pipe\\MyPipe";
BOOL success;
char buffer[1024];
DWORD bytesWritten;
DWORD bytesRead;
// 尝试连接到命名管道
hPipe = CreateFile(
pipeName, // 管道名称
GENERIC_READ | GENERIC_WRITE, // 读写访问
0, // 不共享
NULL, // 默认安全属性
OPEN_EXISTING, // 打开现有的管道
0, // 默认属性
NULL // 不使用模板文件
);
if (hPipe == INVALID_HANDLE_VALUE) {
std::cerr << "Failed to connect to the pipe." << std::endl;
return 1;
}
std::cout << "Connected to the pipe." << std::endl;
// 向服务端发送消息
const char* message = "Hello, server!";
success = WriteFile(
hPipe, // 管道句柄
message, // 发送的数据
(DWORD)strlen(message), // 数据大小
&bytesWritten, // 写入的字节数
NULL // 不重叠
);
if (!success) {
std::cerr << "Failed to send data." << std::endl;
CloseHandle(hPipe);
return 1;
}
std::cout << "Message sent to server, waiting for response..." << std::endl;
// 从服务端读取响应
success = ReadFile(
hPipe, // 管道句柄
buffer, // 接收缓冲区
sizeof(buffer), // 缓冲区大小
&bytesRead, // 读取的字节数
NULL // 不重叠
);
if (!success || bytesRead == 0) {
if (GetLastError() == ERROR_BROKEN_PIPE) {
std::cerr << "Server has disconnected." << std::endl;
}
else {
std::cerr << "Read failed." << std::endl;
}
CloseHandle(hPipe);
return 1;
}
buffer[bytesRead] = '\0'; // 确保字符串结束符
std::cout << "Received: " << buffer << std::endl;
// 关闭句柄
CloseHandle(hPipe);
return 0;
}
int main()
{
pipeClient();
std::cin.get();
}
输出结果: