Windows 匿名管道通信
Windows 匿名管道通信(Anonymous Pipe)实战教程
本文将系统讲解如何在 Windows 平台下,使用匿名管道实现进程间通信。通过逐步升级,我们将从最基本的收发通信,进阶到循环读写,再到实现非阻塞键盘监听与 ESC 退出等,帮助你全面理解匿名管道的实际用法。
❓ 为什么是两个 main()
文件?
💡 这个问题非常好,其实是 Windows 和 Linux 在匿名管道父子进程通信机制上的差异导致了目前这种“两份代码”的写法。
✅ 原因:Windows 没有 fork()
,无法像 Linux 那样复制进程
-
在 Linux 中,我们使用
fork()
创建子进程,子进程自动继承父进程的匿名管道文件描述符,所以只需一个main()
,用fork()
分出父子逻辑即可。👉 所以 Linux 写法是:一个文件,一个进程分支(if (fork() == 0))
-
而在 Windows 中,不能用
fork()
,只能用CreateProcess()
来启动一个“独立的可执行文件”作为子进程。👉 所以需要:
- 编写一个父进程程序(创建匿名管道并调用
CreateProcess
) - 编写一个子进程程序(读取匿名管道)
- 编写一个父进程程序(创建匿名管道并调用
🔁 因此在 Windows 下实现“父子进程通信”通常需要两个可执行程序(两个 main()
),它们由 CreateProcess()
连接。
✅ 基本的父子进程通信
🎯 目标
- 父进程向子进程发送一条消息
- 子进程读取并输出这条消息
📁 项目结构
ipc_demo_windows/
├── anon_pipe_parent.cpp
├── anon_pipe_child.cpp
🛠️ 编译与运行
-
打开“开始菜单”,搜索并启动:
x64 Native Tools Command Prompt for VS 2022
-
在该命令行中运行:
cd D:\WorkCode\Demo\ipc_demo_windows cl anon_pipe_parent.cpp /Fe:anon_pipe_parent.exe cl anon_pipe_child.cpp /Fe:anon_pipe_child.exe anon_pipe_parent.exe
ps:修改控制台编码为
UTF-8
(在控制台输入chcp 65001
)
📄 anon_pipe_parent.cpp
#include <windows.h>
#include <iostream>int main() {// 设置控制台编码为 UTF-8SetConsoleOutputCP(CP_UTF8);// 匿名管道的读端和写端句柄HANDLE hRead, hWrite;// 设置安全属性,允许句柄被子进程继承SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), NULL, TRUE };// 创建匿名管道,hRead为读端,hWrite为写端CreatePipe(&hRead, &hWrite, &sa, 0);STARTUPINFO si = { sizeof(STARTUPINFO) }; // 子进程启动信息结构体PROCESS_INFORMATION pi; // 子进程信息结构体char szCmd[] = "anon_pipe_child.exe"; // 要启动的子进程程序路径// 设置子进程的标准输入为管道读端si.dwFlags = STARTF_USESTDHANDLES;si.hStdInput = hRead; // 子进程从这个读端读取数据si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); // 保持子进程输出显示到当前控制台si.hStdError = GetStdHandle(STD_ERROR_HANDLE); // 保持子进程错误输出显示到当前控制台// 启动子进程(读取端)CreateProcess(NULL, // 应用程序名(可为 NULL)szCmd, // 命令行字符串NULL, NULL,// 进程/线程安全属性(不需要)TRUE, // 继承句柄,必须为 TRUE,否则子进程无法访问管道0, // 创建标志NULL, // 使用父进程环境变量NULL, // 使用父进程工作目录&si, // 启动信息&pi // 返回的进程信息);CloseHandle(hRead); // 父进程不需要读,关闭读端// 父进程写入消息到管道(子进程会从读端接收)const char* msg = "Hello from parent process!\n";DWORD dwWritten;WriteFile(hWrite, msg, strlen(msg), &dwWritten, NULL);// 清理资源:关闭写端管道句柄和子进程句柄CloseHandle(hWrite);CloseHandle(pi.hProcess);CloseHandle(pi.hThread);return 0;
}
📄 anon_pipe_child.cpp
#include <windows.h>
#include <iostream>int main() {char buffer[128]; // 用于存放从管道读取的数据DWORD dwRead; // 实际读取到的字节数// 从标准输入读取数据(父进程通过匿名管道写入)if (ReadFile(GetStdHandle(STD_INPUT_HANDLE), buffer, sizeof(buffer) - 1, &dwRead, NULL)) {// 添加字符串终止符,确保输出不会超界或乱码buffer[dwRead] = '\0';// 打印接收到的消息std::cout << "子进程收到消息: " << buffer << std::endl;} else {std::cerr << "ReadFile failed." << std::endl;}return 0;
}
✅ 循环收发消息(阻塞读写)
🎯 目标
- 父进程持续输入
- 子进程持续读取
- 输入 “exit” 结束通信
🔁 改进点
- 使用循环 +
std::getline()
持续输入 - 关闭写端后,子进程自动结束读取
📄 父进程循环发送消息
#include <string>while (true) {std::getline(std::cin, msg);if (msg == "exit") break;msg += "\n";WriteFile(hWrite, msg.c_str(), msg.length(), &dwWritten, NULL);
}
CloseHandle(hWrite);
📄 子进程循环读取消息
while (true) {BOOL bSuccess = ReadFile(GetStdHandle(STD_INPUT_HANDLE), buffer, sizeof(buffer)-1, &dwRead, NULL);if (!bSuccess || dwRead == 0) break;buffer[dwRead] = '\0';std::cout << "收到消息: " << buffer;
}
✅ 模拟非阻塞输入 + ESC 键退出
🎯 目标
- 支持 ESC 键随时退出
- 回车发送消息
- 支持退格键删除
📄 父进程增强版输入逻辑
#include <conio.h>std::string inputBuffer;
while (true) {// 检查是否有键盘按键按下(非阻塞)if (_kbhit()) {char ch = _getch(); // 读取按下的字符(不显示到屏幕)if (ch == 27) {// ESC 键退出循环(ASCII 27)break;} else if (ch == '\r') {// 回车键被按下,表示用户输入完成一行std::cout << "\n"; // 回显换行// 构造消息并通过匿名管道写入子进程std::string msgToSend = inputBuffer + "\n"; // 添加换行符WriteFile(hWrite, msgToSend.c_str(), msgToSend.length(), &dwWritten, NULL);// 清空缓冲区,准备下一次输入inputBuffer.clear();} else if (ch == '\b') {// 用户按下退格键if (!inputBuffer.empty()) {inputBuffer.pop_back(); // 从缓冲区移除最后一个字符std::cout << "\b \b"; // 清除控制台上最后一字符(回退一格、覆盖空格、再回退)}} else {// 正常字符输入,追加到缓冲区并回显inputBuffer += ch;std::cout << ch;}}Sleep(10); // 避免 CPU 占用过高,休眠 10ms
}
✅ 双向通信(全双工)
🎯 目标
- 父进程:从键盘输入发送消息给子进程,并接收子进程回传消息
- 子进程:读取父进程发送的内容,并回传响应信息
- 双向通信:两个匿名管道实现两个方向的消息传递
🗂️ 架构设计图
父进程 子进程┌────────┐ ┌────────────┐输入 →│ Write A│ ─── 匿名管道 A ──▶───▶──│ Read A ││ │ │ 处理消息 ││ Read B │◀── 匿名管道 B ◀───◀────│ Write B │└────────┘ └────────────┘↑ 打印响应 ↑ 输入响应
📄 父进程
#include <windows.h>
#include <iostream>
#include <string>int main() {// 设置控制台编码为 UTF-8SetConsoleOutputCP(CP_UTF8);HANDLE hParentWriteA, hChildReadA; // 父写 子读HANDLE hChildWriteB, hParentReadB; // 子写 父读SECURITY_ATTRIBUTES sa = { sizeof(sa), NULL, TRUE };// Pipe A:父写,子读CreatePipe(&hChildReadA, &hParentWriteA, &sa, 0);// Pipe B:子写,父读CreatePipe(&hParentReadB, &hChildWriteB, &sa, 0);// 禁止父进程继承子进程的读/写端句柄SetHandleInformation(hParentWriteA, HANDLE_FLAG_INHERIT, 0);SetHandleInformation(hParentReadB, HANDLE_FLAG_INHERIT, 0);// 启动子进程STARTUPINFO si = { sizeof(si) };PROCESS_INFORMATION pi;si.dwFlags = STARTF_USESTDHANDLES;si.hStdInput = hChildReadA;si.hStdOutput = hChildWriteB;si.hStdError = GetStdHandle(STD_ERROR_HANDLE);char cmd[] = "anon_pipe_child.exe";if (!CreateProcess(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {std::cerr << "CreateProcess failed\n";return 1;}CloseHandle(hChildReadA);CloseHandle(hChildWriteB);std::string input;char buffer[256];DWORD dwRead, dwWritten;std::cout << "输入消息(输入 exit 退出):\n";while (true) {std::getline(std::cin, input);if (input == "exit") break;input += "\n";WriteFile(hParentWriteA, input.c_str(), input.size(), &dwWritten, NULL);// 等待子进程回传if (ReadFile(hParentReadB, buffer, sizeof(buffer) - 1, &dwRead, NULL) && dwRead > 0) {buffer[dwRead] = '\0';std::cout << "子进程回应: " << buffer;}}CloseHandle(hParentWriteA);CloseHandle(hParentReadB);CloseHandle(pi.hProcess);CloseHandle(pi.hThread);return 0;
}
📄 子进程
#include <windows.h>
#include <iostream>
#include <string>int main() {char buffer[256];DWORD dwRead, dwWritten;while (true) {BOOL b = ReadFile(GetStdHandle(STD_INPUT_HANDLE), buffer, sizeof(buffer) - 1, &dwRead, NULL);if (!b || dwRead == 0) break;buffer[dwRead] = '\0';std::string response = std::string("收到: ") + buffer;// 回传给父进程,通过 STD_OUTPUT_HANDLE(即 Pipe B)WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), response.c_str(), response.size(), &dwWritten, NULL);}return 0;
}
✅ 子进程存活检测(心跳检测)
🎯 目标
- 父进程每隔一段时间检测子进程是否还活着
- 如果子进程崩溃、退出、异常关闭 → 父进程能检测到并终止通信
- 防止 ReadFile 阻塞、写入失败导致父进程“挂死”
📄 交互触发检测
以下是在原来父进程基础上增加子进程活跃检测的代码片段。
while (true) {// 检查子进程是否仍然活着DWORD waitResult = WaitForSingleObject(pi.hProcess, 0);if (waitResult == WAIT_OBJECT_0) {std::cout << "[警告] 子进程已退出,终止通信。\n";break;}std::getline(std::cin, input);if (input == "exit") break;input += "\n";WriteFile(hParentWriteA, input.c_str(), input.size(), &dwWritten, NULL);// 读取子进程回应if (ReadFile(hParentReadB, buffer, sizeof(buffer)-1, &dwRead, NULL) && dwRead > 0) {buffer[dwRead] = '\0';std::cout << "子进程回应: " << buffer;}Sleep(50); // 降低 CPU 占用
}
📄 定时检测
#include <windows.h>
#include <iostream>
#include <string>
#include <thread>
#include <atomic>std::atomic_bool g_bRunning = true;// 子进程存活监控线程函数
void MonitorChildProcess(HANDLE hChildProcess) {while (g_bRunning) {DWORD waitResult = WaitForSingleObject(hChildProcess, 0);if (waitResult == WAIT_OBJECT_0) {std::cout << "\n[监控] 子进程已退出,终止通信。\n";g_bRunning = false;break;}std::this_thread::sleep_for(std::chrono::milliseconds(200)); // 每200ms检测一次}
}int main() {HANDLE hParentWriteA, hChildReadA; // 父写 子读HANDLE hChildWriteB, hParentReadB; // 子写 父读SECURITY_ATTRIBUTES sa = { sizeof(sa), NULL, TRUE };// Pipe A:父写,子读CreatePipe(&hChildReadA, &hParentWriteA, &sa, 0);// Pipe B:子写,父读CreatePipe(&hParentReadB, &hChildWriteB, &sa, 0);// 禁止父进程继承这些不需要的句柄SetHandleInformation(hParentWriteA, HANDLE_FLAG_INHERIT, 0);SetHandleInformation(hParentReadB, HANDLE_FLAG_INHERIT, 0);STARTUPINFO si = { sizeof(si) };PROCESS_INFORMATION pi;si.dwFlags = STARTF_USESTDHANDLES;si.hStdInput = hChildReadA;si.hStdOutput = hChildWriteB;si.hStdError = GetStdHandle(STD_ERROR_HANDLE);char cmd[] = "anon_pipe_child.exe";if (!CreateProcess(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {std::cerr << "CreateProcess failed\n";return 1;}CloseHandle(hChildReadA);CloseHandle(hChildWriteB);// 启动子进程存活检测线程std::thread monitorThread(MonitorChildProcess, pi.hProcess);std::string input;char buffer[256];DWORD dwRead, dwWritten;std::cout << "输入消息(输入 exit 退出):\n";while (g_bRunning) {std::getline(std::cin, input);if (input == "exit") break;input += "\n";WriteFile(hParentWriteA, input.c_str(), input.size(), &dwWritten, NULL);if (ReadFile(hParentReadB, buffer, sizeof(buffer) - 1, &dwRead, NULL) && dwRead > 0) {buffer[dwRead] = '\0';std::cout << "子进程回应: " << buffer;}}g_bRunning = false;if (monitorThread.joinable()) monitorThread.join();CloseHandle(hParentWriteA);CloseHandle(hParentReadB);CloseHandle(pi.hProcess);CloseHandle(pi.hThread);return 0;
}
❌ 设置子进程的“父进程属性”
🎯 目标
- 任务管理器中显示“可折叠的父子进程结构”
🧠 关键技术点
- 使用
STARTUPINFOEX
代替普通的STARTUPINFO
- 使用
InitializeProcThreadAttributeList
和UpdateProcThreadAttribute
- 设置属性
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
- 在
CreateProcess
中使用EXTENDED_STARTUPINFO_PRESENT
标志
📄 父进程
#include <windows.h>
#include <iostream>
#include <string>int main() {// 创建匿名管道 A(父写,子读) 和 B(子写,父读)HANDLE hParentWriteA, hChildReadA;HANDLE hChildWriteB, hParentReadB;SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), NULL, TRUE };CreatePipe(&hChildReadA, &hParentWriteA, &sa, 0);CreatePipe(&hParentReadB, &hChildWriteB, &sa, 0);SetHandleInformation(hParentWriteA, HANDLE_FLAG_INHERIT, 0);SetHandleInformation(hParentReadB, HANDLE_FLAG_INHERIT, 0);// 构造 STARTUPINFOEX + 属性列表(设置可折叠父子进程关系)STARTUPINFOEXA siex = {};siex.StartupInfo.cb = sizeof(STARTUPINFOEXA);siex.StartupInfo.dwFlags = STARTF_USESTDHANDLES;siex.StartupInfo.hStdInput = hChildReadA;siex.StartupInfo.hStdOutput = hChildWriteB;siex.StartupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);SIZE_T attrListSize = 0;InitializeProcThreadAttributeList(NULL, 1, 0, &attrListSize);LPPROC_THREAD_ATTRIBUTE_LIST lpAttrList =(LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attrListSize);InitializeProcThreadAttributeList(lpAttrList, 1, 0, &attrListSize);HANDLE hRealParent = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId());UpdateProcThreadAttribute(lpAttrList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hRealParent, sizeof(HANDLE), NULL, NULL);siex.lpAttributeList = lpAttrList;// 创建子进程(支持可折叠显示)PROCESS_INFORMATION pi;char cmd[] = "anon_pipe_child.exe";if (!CreateProcess(NULL, cmd, NULL, NULL, TRUE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &siex.StartupInfo, &pi)) {std::cerr << "CreateProcess failed: " << GetLastError() << "\n";return 1;}// 创建 Job 对象以确保任务管理器识别父子绑定关系HANDLE hJob = CreateJobObject(NULL, NULL);AssignProcessToJobObject(hJob, pi.hProcess);// 关闭子进程用的管道句柄CloseHandle(hChildReadA);CloseHandle(hChildWriteB);// 循环消息交互(写入 Pipe A,读取 Pipe B)std::string input;char buffer[256];DWORD dwRead, dwWritten;std::cout << "输入消息(输入 exit 退出):\n";while (true) {std::getline(std::cin, input);if (input == "exit") break;input += "\n";WriteFile(hParentWriteA, input.c_str(), input.size(), &dwWritten, NULL);if (ReadFile(hParentReadB, buffer, sizeof(buffer) - 1, &dwRead, NULL) && dwRead > 0) {buffer[dwRead] = '\0';std::cout << buffer;}}// 清理资源CloseHandle(hParentWriteA);CloseHandle(hParentReadB);CloseHandle(pi.hProcess);CloseHandle(pi.hThread);CloseHandle(hRealParent);CloseHandle(hJob);DeleteProcThreadAttributeList(lpAttrList);HeapFree(GetProcessHeap(), 0, lpAttrList);return 0;
}
问题
:没有在任务管理器显示折叠
📌 总结
通过上面实现,我们完成了从基础收发到灵活控制的匿名管道通信开发。