Windows 的IPC(處理序間通訊)機制主要是非同步管道和具名管道。(至於其他的IPC方式,例如記憶體映射、郵槽等這裡就不介紹了)
管道(pipe)是用於處理序間通訊的共用記憶體地區。建立管道的進程稱為管道伺服器,而串連到這個管道的進程稱為管道用戶端。一個進程向管道寫入資訊,而另外一個進程從管道讀取資訊。
非同步管道是基於字元和半雙工的(即單向),一般用於程式輸入輸出的重新導向;具名管道則強大地多,它們是面向訊息和全雙工系統的,同時還允許網路通訊,用於建立用戶端/伺服器系統。
一、非同步管道(實現比較簡單,直接通過執行個體來講解)
實驗目標:當前有sample.cpp, sample.exe, sample.in這三個檔案,sample.exe為sample.cpp的執行程式,sample.cpp只是一個簡單的程式樣本(簡單求和),如下:
代碼:
#include <iostream.h>int main(){ int a, b ; while ( cin >> a >> b && ( a || b ) ) cout << a + b << endl ; return 0;}
Sample.in檔案是輸入檔案,內容:
32 433
542 657
0 0
要求根據sample.exe和它的輸入資料,把輸出資料重新導向到sample.out
流程分析:實際這個實驗中包含兩個部分,把輸入資料重新導向到sample.exe 和把輸出資料重新導向到sample.out。在命令列下可以很簡單的實現這個功能“sample <sample.in >sample.out”,這個命令也是利用管道特性實現的,現在我們就根據非同步管道的實現原理自己來實現這個功能。
管道是基於半雙工(單向)的,這裡有兩個重新導向的過程,顯然需要建立兩個管道,下面給出流程圖:
非同步管道實現的流程圖說明:
1)。父進程是我們需要實現的,其中需要建立管道A,管道B,和子進程,整個實現流程分為4個操作。
2)。管道A:輸入管道
3)。管道B:輸出管道
4)。操作A:把輸入檔案sample.in的資料寫入輸入管道(管道A)
5)。操作B:子進程從輸入管道中讀取資料,作為該進程的加工原料。通常,程式的輸入資料由標準的輸入裝置輸入,這裡實現輸入重新導向,即把輸入管道作為輸入裝置。
6)。操作C:子進程把加工後的成品(輸出資料)輸出到輸出管道。通常,程式的輸出資料會輸出到標準的輸出裝置,一般為螢幕,這裡實現輸出重新導向,即把輸出管道作為輸出裝置。
7)。操作D:把輸出管道的資料寫入輸出檔案
需要注意的是,管道的本質只是一個共用的記憶體地區。這個實驗中,管道地區處於父進程的地址空間中,父進程的作用是提供環境和資源,並協調子進程進行加工。
程式源碼:
代碼:
#include <windows.h> #include <iostream.h>const int BUFSIZE = 4096 ; HANDLE hChildStdinRd, hChildStdinWr, hChildStdinWrDup, hChildStdoutRd,hChildStdoutWr,hChildStdoutRdDup, hSaveStdin, hSaveStdout; BOOL CreateChildProcess(LPTSTR); VOID WriteToPipe(LPTSTR); VOID ReadFromPipe(LPTSTR); VOID ErrorExit(LPTSTR); VOID ErrMsg(LPTSTR, BOOL); void main( int argc, char *argv[] ) { // 處理輸入參數 if ( argc != 4 ) return ; // 分別用來儲存命令列,輸入檔案名稱(CPP/C),輸出檔案名(儲存編譯資訊) LPTSTR lpProgram = new char[ strlen(argv[1]) ] ; strcpy ( lpProgram, argv[1] ) ; LPTSTR lpInputFile = new char[ strlen(argv[2]) ]; strcpy ( lpInputFile, argv[2] ) ; LPTSTR lpOutputFile = new char[ strlen(argv[3]) ] ; strcpy ( lpOutputFile, argv[3] ) ; SECURITY_ATTRIBUTES saAttr; saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); saAttr.bInheritHandle = TRUE; saAttr.lpSecurityDescriptor = NULL; /************************************************ * redirecting child process's STDOUT * ************************************************/ hSaveStdout = GetStdHandle(STD_OUTPUT_HANDLE); if (! CreatePipe(&hChildStdoutRd, &hChildStdoutWr, &saAttr, 0)) ErrorExit("Stdout pipe creation failed\n"); if (! SetStdHandle(STD_OUTPUT_HANDLE, hChildStdoutWr)) ErrorExit("Redirecting STDOUT failed"); BOOL fSuccess = DuplicateHandle( GetCurrentProcess(), hChildStdoutRd, GetCurrentProcess(), &hChildStdoutRdDup , 0, FALSE, DUPLICATE_SAME_ACCESS); if( !fSuccess ) ErrorExit("DuplicateHandle failed"); CloseHandle(hChildStdoutRd); /************************************************ * redirecting child process's STDIN * ************************************************/ hSaveStdin = GetStdHandle(STD_INPUT_HANDLE); if (! CreatePipe(&hChildStdinRd, &hChildStdinWr, &saAttr, 0)) ErrorExit("Stdin pipe creation failed\n"); if (! SetStdHandle(STD_INPUT_HANDLE, hChildStdinRd)) ErrorExit("Redirecting Stdin failed"); fSuccess = DuplicateHandle( GetCurrentProcess(), hChildStdinWr, GetCurrentProcess(), &hChildStdinWrDup, 0, FALSE, DUPLICATE_SAME_ACCESS); if (! fSuccess) ErrorExit("DuplicateHandle failed"); CloseHandle(hChildStdinWr); /************************************************ * 建立子進程(即啟動SAMPLE.EXE) * ************************************************/ fSuccess = CreateChildProcess( lpProgram ); if ( !fSuccess ) ErrorExit("Create process failed"); // 父進程輸入輸出資料流的還原設定 if (! SetStdHandle(STD_INPUT_HANDLE, hSaveStdin)) ErrorExit("Re-redirecting Stdin failed\n"); if (! SetStdHandle(STD_OUTPUT_HANDLE, hSaveStdout)) ErrorExit("Re-redirecting Stdout failed\n"); WriteToPipe( lpInputFile ) ; ReadFromPipe( lpOutputFile ); delete lpProgram ; delete lpInputFile ; delete lpOutputFile ;} BOOL CreateChildProcess( LPTSTR lpProgram ) { PROCESS_INFORMATION piProcInfo; STARTUPINFO siStartInfo; BOOL bFuncRetn = FALSE; ZeroMemory( &piProcInfo, sizeof(PROCESS_INFORMATION) ); ZeroMemory( &siStartInfo, sizeof(STARTUPINFO) ); siStartInfo.cb = sizeof(STARTUPINFO); bFuncRetn = CreateProcess ( NULL, lpProgram, NULL, NULL, TRUE, \ 0, NULL, NULL, &siStartInfo, &piProcInfo); if (bFuncRetn == 0) { ErrorExit("CreateProcess failed\n"); return 0; } else { CloseHandle(piProcInfo.hProcess); CloseHandle(piProcInfo.hThread); return bFuncRetn; }}VOID WriteToPipe( LPTSTR lpInputFile ) { HANDLE hInputFile = CreateFile(lpInputFile, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); if (hInputFile == INVALID_HANDLE_VALUE) return ; BOOL fSuccess ; DWORD dwRead, dwWritten; CHAR chBuf[BUFSIZE] = {0} ; for (;;) { fSuccess = ReadFile( hInputFile, chBuf, BUFSIZE, &dwRead, NULL) ; if ( !fSuccess || dwRead == 0) break; fSuccess = WriteFile( hChildStdinWrDup, chBuf, dwRead, &dwWritten, NULL) ; if ( !fSuccess ) break; } if (! CloseHandle(hChildStdinWrDup)) ErrorExit("Close pipe failed\n"); CloseHandle ( hInputFile ) ;} VOID ReadFromPipe( LPTSTR lpOutputFile ) { HANDLE hOutputFile = CreateFile( lpOutputFile, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hOutputFile == INVALID_HANDLE_VALUE) return ; BOOL fSuccess ; DWORD dwRead, dwWritten; CHAR chBuf[BUFSIZE] = { 0 }; if (!CloseHandle(hChildStdoutWr)) ErrorExit("Closing handle failed"); for (;;) { fSuccess = ReadFile( hChildStdoutRdDup, chBuf, BUFSIZE, &dwRead, NULL) ; if( !fSuccess || dwRead == 0) { break; } fSuccess = WriteFile( hOutputFile, chBuf, dwRead, &dwWritten, NULL) ; if ( !fSuccess ) break; } CloseHandle ( hOutputFile ) ;} VOID ErrorExit (LPTSTR lpszMessage) { MessageBox( 0, lpszMessage, 0, 0 ); }
二、具名管道
具名管道具有以下幾個特徵:
(1)具名管道是雙向的,所以兩個進程可以通過同一管道進行互動。
(2)具名管道不但可以面向位元組流,還可以面向訊息,所以讀取進程可以讀取寫進程發送的不同長度的訊息。
(3)多個獨立的管道執行個體可以用一個名稱來命名。例如幾個用戶端可以使用名稱相同的管道與同一個伺服器進行並發通訊。
(4)具名管道可以用於網路間兩個進程的通訊,而其實現的過程與本地進程通訊完全一致。
實驗目標:在用戶端輸入資料a和b,然後發送到伺服器並計算a+b,然後把計算結果發送到用戶端。可以多個用戶端與同一個伺服器並行通訊。
介面設計:
痛點所在:
實現的過程比較簡單,但有一個痛點。原本當服務端使用ConnectNamedPipe函數後,如果有用戶端串連,就可以直接進行互動。原來我在實現過程中,當管道空閑時,管道的線程函數會無限(INFINITE)阻塞。若現在需要停止服務,就必須結束所有的線程,TernimateThread可以作為一個結束線程的方法,但我基本不用這個函數。一旦使用這個函數之後,目標線程就會立即結束,但如果此時的目標線程正在操作互斥資源、核心調用、或者是操作共用DLL的全域變數,可能會出現互斥資源無法釋放、核心例外狀況等現象。這裡我用重疊I/0來解決這個問題,在建立PIPE時使用FILE_FLAG_OVERLAPPED標誌,這樣使用ConnectNamedPipe後會立即返回,但線程的阻塞由等待函數WaitForSingleObject來實現,等待OVERLAPPED結構的事件對象被設定。
用戶端主要代碼:
代碼:
void CMyDlg::OnSubmit() { // 開啟管道 HANDLE hPipe = CreateFile("\\\\.\\Pipe\\NamedPipe", GENERIC_READ | GENERIC_WRITE, \ 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL) ; if ( hPipe == INVALID_HANDLE_VALUE ) { this->MessageBox ( "開啟管道失敗,伺服器尚未啟動,或者用戶端數量過多" ) ; return ; } DWORD nReadByte, nWriteByte ; char szBuf[1024] = {0} ; // 把兩個整數(a,b)格式化為字串 sprintf ( szBuf, "%d %d", this->nFirst, this->nSecond ) ; // 把資料寫入管道 WriteFile ( hPipe, szBuf, strlen(szBuf), &nWriteByte, NULL ) ; memset ( szBuf, 0, sizeof(szBuf) ) ; // 讀取伺服器的反饋資訊 ReadFile ( hPipe, szBuf, 1024, &nReadByte, NULL ) ; // 把返回資訊格式化為整數 sscanf ( szBuf, "%d", &(this->nResValue) ) ; this->UpdateData ( false ) ; CloseHandle ( hPipe ) ;}
服務端主要代碼:
代碼:
// 啟動服務void CMyDlg::OnStart() { CString lpPipeName = "\\\\.\\Pipe\\NamedPipe" ; for ( UINT i = 0; i < nMaxConn; i++ ) { // 建立管道執行個體 PipeInst[i].hPipe = CreateNamedPipe ( lpPipeName, PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED, \ PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_WAIT, nMaxConn, 0, 0, 1000, NULL ) ; if ( PipeInst[i].hPipe == INVALID_HANDLE_VALUE ) { DWORD dwErrorCode = GetLastError () ; this->MessageBox ( "建立管道錯誤!" ) ; return ; } // 為每個管道執行個體建立一個事件對象,用於實現重疊IO PipeInst[i].hEvent = CreateEvent ( NULL, false, false, false ) ; // 為每個管道執行個體分配一個線程,用於響應用戶端的請求 PipeInst[i].hTread = AfxBeginThread ( ServerThread, &PipeInst[i], THREAD_PRIORITY_NORMAL ) ; } this->SetWindowText ( "具名管道執行個體之伺服器(運行)" ) ; this->MessageBox ( "服務啟動成功" ) ;}// 停止服務void CMyDlg::OnStop() { DWORD dwNewMode = PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_NOWAIT ; for ( UINT i = 0; i < nMaxConn; i++ ) { SetEvent ( PipeInst[i].hEvent ) ; CloseHandle ( PipeInst[i].hTread ) ; CloseHandle ( PipeInst[i].hPipe ) ; } this->SetWindowText ( "具名管道執行個體之伺服器" ) ; this->MessageBox ( "停止啟動成功" ) ;}// 線程服務函數UINT ServerThread ( LPVOID lpParameter ){ DWORD nReadByte = 0, nWriteByte = 0, dwByte = 0 ; char szBuf[MAX_BUFFER_SIZE] = {0} ; PIPE_INSTRUCT CurPipeInst = *(PIPE_INSTRUCT*)lpParameter ; OVERLAPPED OverLapStruct = { 0, 0, 0, 0, CurPipeInst.hEvent } ; while ( true ) { memset ( szBuf, 0, sizeof(szBuf) ) ; // 具名管道的串連函數,等待用戶端的串連(只針對NT) ConnectNamedPipe ( CurPipeInst.hPipe, &OverLapStruct ) ; // 實現重疊I/0,等待OVERLAPPED結構的事件對象 WaitForSingleObject ( CurPipeInst.hEvent, INFINITE ) ; // 檢測I/0是否已經完成,如果未完成,意味著該事件對象是人工設定,即服務需要停止 if ( !GetOverlappedResult ( CurPipeInst.hPipe, &OverLapStruct, &dwByte, true ) ) break ; // 從管道中讀取用戶端的請求資訊 if ( !ReadFile ( CurPipeInst.hPipe, szBuf, MAX_BUFFER_SIZE, &nReadByte, NULL ) ) { MessageBox ( 0, "讀取管道錯誤!", 0, 0 ) ; break ; } int a, b ; sscanf ( szBuf, "%d %d", &a, &b ) ; pMyDlg->nFirst = a ; pMyDlg->nSecond = b ; pMyDlg->nResValue = a + b ; memset ( szBuf, 0, sizeof(szBuf) ) ; sprintf ( szBuf, "%d", pMyDlg->nResValue ) ; // 把反饋資訊寫入管道 WriteFile ( CurPipeInst.hPipe, szBuf, strlen(szBuf), &nWriteByte, NULL ) ; pMyDlg->SetDlgItemInt ( IDC_FIRST, a, true ) ; pMyDlg->SetDlgItemInt ( IDC_SECOND, b, true ) ; pMyDlg->SetDlgItemInt ( IDC_RESULT, pMyDlg->nResValue, true ) ; // 斷開用戶端的串連,以便等待下一客戶的到來 DisconnectNamedPipe ( CurPipeInst.hPipe ) ; } return 0 ;}