[C++] 匿名管道的理解與實現
匿名管道用於程序之間通訊,且僅限於本地父子程序之間通訊,結構簡單,類似於一根水管,一端進水另一端出水(單工)。相對於命名管道,其佔用小實現簡單,在特定情況下,比如實現兩圍棋引擎本地對戰可以使用匿名管道。
怎樣實現匿名管道雙向通訊?
由於匿名管道是單工的,所以為實現父子程序雙向通訊需要建立兩根管道,並由子程序繼承一根管道的讀控制代碼和另一根管道的寫控制代碼。
如何理解匿名管道的雙向通訊?
管道相當於一段記憶體,一個程序輸入,一個程序讀出。
在程序通訊時一般會產生程序同步問題(程序同步講解請見作業系統類書籍):父子程序各自均具有讀寫功能,在管道為空時,相應讀程序應該被阻塞起來,直到管道被寫入為止才被喚醒。
這種空管道不允許讀的特性應當加一個鎖,但匿名管道自帶了這種功能,所以不需要對讀寫進行限制,其能自動阻塞。
在VS2017下實現匿名管道
對幾個基本點進行介紹
#include
<windows.h>
匿名管道需要包含此標頭檔案
首先我們需要了解一下最後程式實現中我想要的效果:父程序輸入任意長數字(當然侷限於匿名管道的最大大小4MB)通過匿名管道傳給子程序,由子程序對該字串(由於在管道中以字元流形式存在)的各位數進行加和,把這個加和的結果返回父程序。
在實際製作時,我將子程序這個計算函式做成動態連結庫的形式進行鏈入。所以在實際程式碼中將以一行程式碼的形式呈現:
int
Bitadd(char *ary1, char *ary2, unsigned long len, int Lcount);
其中ary1為子程序接收到的字串、ary2為計算結果、len是接收到的字串長度、Lcount為計算結果長度。
建立管道
函式原型:
BOOL WINAPI CreatePipe(
_Out_PHANDLE
hReadPipe,
_Out_PHANDLE
hWritePipe,
_In_opt_LPSECURITY_ATTRIBUTES
lpPipeAttributes,
_In_DWORD
nSize);
實際呼叫形式:
CreatePipe(&read,
&write, &sa, 0);
其中read是讀控制代碼,write是寫控制代碼,sa是管道安全屬性,0代表管道緩衝設定為系統預設值。
由上函式可知在建立管道之前,需要先設定管道安全屬性。
設定管道安全屬性
物件原型:
typedef struct _SECURITY_ATTRIBUTES { DWORD nLength; //結構體的大小,可用SIZEOF取得 LPVOID lpSecurityDescriptor; //安全描述符 BOOL bInheritHandle ;//安全描述的物件能否被新建立的程序繼承 } SECURITY_ATTRIBUTES,* PSECURITY_ATTRIBUTES;
在程式中僅需如下設定即可:(ParentView為我建立的父程序管道類)
void ParentView::CreateATTRIBUTES() // 設定管道安全屬性 { sa.bInheritHandle = TRUE; // TRUE為管道可以被子程序所繼承 sa.lpSecurityDescriptor = NULL; // 預設為NULL sa.nLength = sizeof(SECURITY_ATTRIBUTES); }
各引數在原型中已有很好的註釋。
建立好管道後,可以考慮建立子程序,使其繼承父程序的管道控制代碼。
建立子程序
先貼程式碼:
TCHAR szCmdline[] = TEXT("../../child/Debug/child.exe"); // 設定子程序路徑 PROCESS_INFORMATION pi; // 用來接收新程序的識別資訊 STARTUPINFO si; // 用於決定新程序的主窗體如何顯示 BOOL bSuccess = FALSE; // 設定PROCESS_INFORMATION ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); // 用0填充記憶體區域 // 設定STARTUPINFO ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); // 結構大小 //*************** 控制代碼繼承設定****************** // 建立了兩個管道 // 管道1由父程序讀,子程序寫 // 管道2由父程序寫,子程序讀 si.hStdError = write1; // 錯誤輸出控制代碼(在寫控制代碼中寫回父程序) si.hStdOutput = write1; // 子程序繼承管道1寫控制代碼 si.hStdInput = read2; // 子程序繼承管道2讀控制代碼 //*************** 控制代碼繼承設定****************** si.dwFlags |= STARTF_USESTDHANDLES; // 使用hStdInput 、hStdOutput 和hStdError 成員 // 建立子程序 // 摘自msdn: // If lpApplicationName is NULL, // the first white space–delimited token of the command line specifies the module name. bSuccess = CreateProcess( NULL, // lpApplicationName szCmdline, // command line // 以上兩個欄位都可以建立目標子程序 NULL, // process security attributes NULL, // primary thread security attributes TRUE, // bInheritHandles:指示新程序是否從呼叫程序處繼承了控制代碼 0, // creation flags:指定附加的、用來控制優先類和程序的建立的標誌。 // 設定為 CREATE_NEW_CONSOLE 可顯示子視窗 NULL, // use parent's environment NULL, // use parent's current directory &si, // STARTUPINFO :指向一個用於決定新程序的主窗體如何顯示的STARTUPINFO結構體 &pi // PROCESS_INFORMATION :指向一個用來接收新程序的識別資訊的PROCESS_INFORMATION結構體 ); // If an error occurs, exit the application. if (!bSuccess) cout << "建立子程式失敗" << endl; else { // 關閉一些子程序用的控制代碼 CloseHandle(pi.hProcess); CloseHandle(pi.hThread); CloseHandle(write1); CloseHandle(read2); }
首先設定子程序所在路徑,子程序為一個exe可執行程式。然後會用到兩個型別STARTUPINFO和PROCESS_INFORMATION,有興趣的朋友可自行百度,檢視兩種類中的引數。
這裡也不貼CreateProcess的函式原型了,程式碼塊中有較好的註釋。
其實對於管道建立和子程序建立都是一個模版框架。
讀寫函式請見github原始碼
實現雙向通訊
在父程序中建立兩個匿名管道。此時父程序共有六個控制代碼Read1,Write1,Read2,Write2,標準輸入輸出控制代碼。
由圖所示,標準輸入輸出控制代碼用於在Dos視窗的輸入和輸出。
然後我們需要讓建立的子程序繼承Write1控制代碼和Read2控制代碼。
子程序初始化控制代碼程式碼
read = GetStdHandle(STD_INPUT_HANDLE); // 繼承控制代碼 write = GetStdHandle(STD_OUTPUT_HANDLE); if ((read == INVALID_HANDLE_VALUE) || (write == INVALID_HANDLE_VALUE)) cout << "繼承控制代碼無效" << endl;
可以看到,子程序的標準輸入輸出控制代碼已經被繼承的Write1控制代碼和Read2控制代碼所覆蓋。
因此無法實現在子程序的Dos視窗進行顯示,子程序視窗將是永遠黑窗,可以在父程式中註釋掉子程序所繼承的寫控制代碼進行對比,並將CreateProcess函式中的一個引數設定為顯示子程序視窗(註釋中有)。
需要注意的坑點:
-
si.dwFlags |= STARTF_USESTDHANDLES;
-
若要實現雙向通訊,子程序Dos是黑窗。但是可以將子程序收到的結果寫到檔案。