Windows 管道(Anonymous Pipe)笔记
1. 基本概念
- Windows 的匿名管道,就是从操作系统教科书上学到的那个最经典、最传统的“管道”概念。从它的API名就可以看出
CreatePipe()。匿名管道就是传统意义上的管道,下面用“管道”指代匿名管道
- 其本质是父进程与子进程之间的一块共享内存,用于实现进程间的单向数据通信。
- 不是说数据必须从父进程流向子进程,管道不是这样设计的。管道就是一块共享内存,只不过这块共享内存只能从写句柄写入,从读句柄读取。子进程和父进程一样都能通过句柄读写管道。
- 命名管道是管道的增强版,支持更多功能,但不同操作系统的实现有所差异(Windows 与 Linux 不同)。
注意:下文中的“管道”如无特殊说明,均指匿名管道。
2. 管道对象的内部结构
Windows 内核中的管道对象可以抽象为以下结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 内核中的管道对象(共享内存): ┌─────────────────────────────┐ │ 管道缓冲区 │ │ [数据流向: 写端 → 读端] │ │ │ │ 写端引用计数: 2 │ │ 读端引用计数: 2 │ └─────────────────────────────┘ ↑ ↑ │ │ ┌────┴────┐ ┌────┴────┐ │ 写句柄 │ │ 读句柄 │ │hWritePipe│ │hReadPipe │ │只写权限 │ │只读权限 │ └────┬────┘ └────┬────┘ │ │ 父进程持有 父进程持有 子进程继承 子进程继承
|
- 管道的读写两端均有引用计数,父进程和子进程各持有一份句柄。
- 当所有写句柄被关闭时,读操作会失败或返回 EOF;同理,所有读句柄关闭后,写操作将失败。
3. 核心 API
3.1 创建管道:CreatePipe
- 用于创建匿名管道,返回读句柄和写句柄。
- 可通过
SECURITY_ATTRIBUTES 控制句柄是否可被子进程继承。
3.2 创建子进程:CreateProcessW
函数原型(关键参数说明):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| WINBASEAPI BOOL WINAPI CreateProcessW( _In_opt_ LPCWSTR lpApplicationName, _Inout_opt_ LPWSTR lpCommandLine, _In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes, _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_ BOOL bInheritHandles, _In_ DWORD dwCreationFlags, _In_opt_ LPVOID lpEnvironment, _In_opt_ LPCWSTR lpCurrentDirectory, _In_ LPSTARTUPINFOW lpStartupInfo, _Out_ LPPROCESS_INFORMATION lpProcessInformation );
|
使用 STARTUPINFOW 结构时,必须设置 dwFlags = STARTF_USESTDHANDLES,并将 hStdInput、hStdOutput、hStdError 重定向到管道句柄,以实现子进程的标准 I/O 重定向。
| 参数 |
类型 |
方向 |
说明 |
lpApplicationName |
LPCWSTR |
输入(可选) |
要执行的 .exe 文件完整路径。若为 NULL,则从 lpCommandLine 中提取可执行文件名。若包含路径则直接使用,否则按系统搜索顺序查找。 |
lpCommandLine |
LPWSTR |
输入/输出(可选) |
命令行字符串(会被修改,不能传常量)。可包含程序名和参数。若 lpApplicationName 为 NULL,则此字符串的第一个标记作为可执行文件名。 |
lpProcessAttributes |
LPSECURITY_ATTRIBUTES |
输入(可选) |
进程的安全描述符,决定返回的进程句柄是否可被子进程继承。为 NULL 时使用默认安全性且句柄不可继承。 |
lpThreadAttributes |
LPSECURITY_ATTRIBUTES |
输入(可选) |
主线程的安全描述符,决定返回的线程句柄是否可被子进程继承。为 NULL 时使用默认安全性且句柄不可继承。 |
bInheritHandles |
BOOL |
输入 |
若为 TRUE,子进程将继承父进程中所有标记为可继承的句柄。 |
dwCreationFlags |
DWORD |
输入 |
控制进程创建方式,常用值:CREATE_SUSPENDED(挂起主线程)、CREATE_NEW_CONSOLE(新建控制台窗口)等。 |
lpEnvironment |
LPVOID |
输入(可选) |
新进程的环境块(以 \0\0 结尾的字符串列表)。为 NULL 时使用调用进程的环境块。 |
lpCurrentDirectory |
LPCWSTR |
输入(可选) |
新进程的当前工作目录路径。为 NULL 时使用调用进程的当前工作目录。 |
lpStartupInfo |
LPSTARTUPINFOW |
输入(必须) |
指定新进程的主窗口外观、标准句柄重定向等启动信息。调用前必须用 ZeroMemory 初始化并设置 cb 成员。 |
lpProcessInformation |
LPPROCESS_INFORMATION |
输出(必须) |
返回新进程和主线程的句柄及 ID。调用成功后必须调用 CloseHandle 关闭 hProcess 和 hThread,避免资源泄漏。 |
4. 代码示例
4.1 父进程代码(服务端)
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
| HANDLE hReadPipe; HANDLE hWritePipe;
void CAnonyPipeServerDlg::OnBnClickedButtonCreate() { SECURITY_ATTRIBUTES pipeSecurityAttributes; pipeSecurityAttributes.bInheritHandle = TRUE; pipeSecurityAttributes.lpSecurityDescriptor = NULL; pipeSecurityAttributes.nLength = sizeof(SECURITY_ATTRIBUTES);
BOOL bRet = CreatePipe(&hReadPipe, &hWritePipe, &pipeSecurityAttributes, 0); if (!bRet) { MessageBox(TEXT("匿名管道创建失败")); return; }
STARTUPINFO strStartUpInfo = {0}; strStartUpInfo.cb = sizeof(STARTUPINFO); strStartUpInfo.dwFlags = STARTF_USESTDHANDLES; strStartUpInfo.hStdInput = hReadPipe; strStartUpInfo.hStdOutput = hWritePipe; strStartUpInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE);
PROCESS_INFORMATION processInformation = {0};
bRet = CreateProcess( NULL, TEXT("AnonyPipeChildProcess.exe"), NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &strStartUpInfo, &processInformation );
if (bRet) { CloseHandle(processInformation.hProcess); CloseHandle(processInformation.hThread); processInformation.dwProcessId = 0; processInformation.dwThreadId = 0; processInformation.hThread = NULL; processInformation.hProcess = NULL; } else { CloseHandle(hReadPipe); CloseHandle(hWritePipe); hReadPipe = NULL; hWritePipe = NULL; MessageBox(TEXT("创建子进程失败")); } }
void CAnonyPipeServerDlg::OnBnClickedButtonSend() { char szSendBuf[100] = "我是父进程"; DWORD dwWriteNum; if (!WriteFile(hWritePipe, szSendBuf, strlen(szSendBuf) + 1, &dwWriteNum, NULL)) { MessageBox("Write Failed"); } }
void CAnonyPipeServerDlg::OnBnClickedButtonRecv() { char szRecvBuf[100] = {0}; DWORD dwReadNum; if (!ReadFile(hReadPipe, szRecvBuf, 100, &dwReadNum, NULL)) { MessageBox("Read Failed"); return; } TRACE("dwReadNum = %d\n", dwReadNum); MessageBox(szRecvBuf); }
|
4.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
| void CAnonyPipeClientDlg::OnBnClickedButtonSend() { HANDLE hWritePipe = GetStdHandle(STD_OUTPUT_HANDLE); char szSendBuf[100] = "我是子进程"; DWORD dwWriteNum; if (!WriteFile(hWritePipe, szSendBuf, strlen(szSendBuf) + 1, &dwWriteNum, NULL)) { MessageBox("Write Failed"); return; } }
void CAnonyPipeClientDlg::OnBnClickedButtonRecv() { HANDLE hReadPipe = GetStdHandle(STD_INPUT_HANDLE); char szRecvBuf[100] = {0}; DWORD dwReadNum; if (!ReadFile(hReadPipe, szRecvBuf, 100, &dwReadNum, NULL)) { MessageBox("Read Failed"); return; } TRACE("dwReadNum = %d\n", dwReadNum); MessageBox(szRecvBuf); }
|
5. 关键点总结
| 要点 |
说明 |
| 通信机制 |
基于共享内存,读写端分离 |
| 句柄继承 |
必须将 SECURITY_ATTRIBUTES.bInheritHandle 设为 TRUE,且 CreateProcess 的 bInheritHandles 参数为 TRUE |
| 标准 I/O 重定向 |
通过 STARTUPINFO 的 hStdInput / hStdOutput 实现 |
| 资源管理 |
父进程创建子进程后,应及时关闭 PROCESS_INFORMATION 中的句柄,避免资源泄漏 |
| 双向通信 |
管道本身是单向的,若需要双向通信,可创建两个管道(各负责一个方向) |