在 Windows 系統編程中,匿名管道是實現父子進程通信的常用機制。然而,許多開發者在使用 ReadFile 讀取管道數據時,會遇到函數調用阻塞、程序停止響應的情況。這篇文章將通過分析一個典型的代碼示例,深入探討導致阻塞的根本原因,並提供解決方案。
父進程創建匿名管道,啟動子進程,並將子進程的標準輸入/輸出重定向到管道句柄:
// 父進程關鍵代碼片段
HANDLE hRead, hWrite;
SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), NULL, TRUE };
CreatePipe(&hRead, &hWrite, &sa, 0);
// 啟動子進程,將 stdin/stdout 重定向
STARTUPINFO si = { sizeof(STARTUPINFO) };
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = hRead;
si.hStdOutput = hWrite;
CreateProcess(NULL, L"ChildProcess.exe", NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
// 寫入數據
WriteFile(hWrite, "測試數據", strlen("測試數據"), &written, NULL);
子進程嘗試從標準輸入讀取:
// 子進程代碼
HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE);
char buffer[100];
DWORD bytesRead;
ReadFile(hInput, buffer, 100, &bytesRead, NULL); // 這裡發生阻塞
ReadFile 在管道中沒有可用數據時,默認會進入阻塞狀態,等待數據寫入或寫入端關閉。這是同步 I/O 的標準語義。
在上述代碼中,父進程創建管道後,同時持有讀端(hRead)和寫端(hWrite)。當父進程調用 WriteFile 寫入數據後,如果不關閉寫端句柄,管道管理器認為「仍有寫入者可能發送數據」,因此即使數據已讀完,ReadFile 也不會返回 EOF(即返回 0 字節),而是繼續等待。
由於 bInheritHandle=TRUE,子進程繼承了父進程的寫端句柄(作為 StdOutput)。這意味著即使父進程關閉了自己的 hWrite,子進程仍持有該管道的寫端引用,導致管道無法達到「所有寫端均已關閉」的狀態,ReadFile 永遠無法讀到 EOF。
CloseHandle(hWrite);PeekNamedPipe 檢查數據可用性:避免無限期阻塞。WriteFile(hWrite, data, len, &written, NULL);
CloseHandle(hWrite); // 關鍵:通知管道無更多寫入者
// 此時可安全調用 ReadFile(hRead, ...) 讀取子進程輸出,或等待子進程退出
匿名管道阻塞的核心在於管道寫端句柄未正確關閉,導致讀端無法檢測到數據流結束。正確管理句柄生命週期——「用完即關,專用專關」——是避免此類問題的關鍵。同時,理解同步 I/O 的阻塞語義,結合 PeekNamedPipe 或異步機制,能構建更健壯的進程間通信程序。