windows下進程間通信

   
轉自:http://www.rosoo.net/a/201110/15096.html 

在Windows下各個任務是以不同的進程來完成的,當一個進程啓動後,操作系統爲其分配了4GB的私有地址空間,由於位於同一個進程中的線程共享同一個 地址空間,所以線程間的通信很簡單,就像兩個人如果在同一個房間裏說話的話就比較容易,只要動動嘴皮子就OK了, 但是如果在兩個國家裏就比較麻煩,必須藉助於一些其他的手段,比如打電話等. 以下介紹四種進程通信方式,雖然是在windows下的環境但是在其他的操作系統裏也遵循着同樣的原理,不信的話可以把大學裏的操作系統教材拿出來看看, 它們分別是剪貼板、 匿名管道、命名管道和郵槽。

1. 剪貼板(clipboard)
其實這個東西我們每天操作電腦的時候都在接觸,我們經常實用ctrl+c和ctrl+v就是基於了剪貼板的方式來實現了兩個進程間的通信, 就拿我現在來說吧,我在寫這篇文章的時候是在notepad下寫的, 一會我要把這篇文章裏的所有文字都粘貼到csdn的網頁上, 這裏就是兩個進程,一個是notepad進程和一個IE進程進行通信, 它們要傳輸的數據格式是TEXT,當然你也可以把這些內容拷貝到word、Excel、PowerPoint甚至是另一個notepad上面(你要清楚再 啓動一個notepad,這個跟前一個notepad是兩個進程,雖然它們長得很像),這就說明剪貼板是所有程序都可以訪問的,如果你對多線程編程比較了 解的話, 你就會明白一個數據一旦要被很多線程訪問,如果這些線程中有一些需要求改這個數據,就要對這個數據加鎖來保證數據的正確性了,剪貼板也是一樣的,當我把這 段文字ctrl+c時,它就要先對系統中的剪貼板加鎖,然後把內容放進去,再釋放鎖,如果你明白了以上的一些道理,那麼請你繼續往下看,如果還沒太明白那 也請你繼續往下看, 也許你對文字的理解能力已經落後於對代碼的理解了.
BOOL OpenClipboard()
windows提供的一個API函數,作用是打開剪貼板,如果程序打開了剪貼板,則其他程序經不能修改剪貼板(道理上面講了),直到 CloseClipboard(), 在windows中所有帶有Open這個單詞的函數都會有一個與之對應的帶有Close這個單詞的函數, 而且你在open之後一定不要忘記close,你可以自己試試看,只調用OpenClipboard()而不去執行CloseClipboard()會有 什麼效果,至今我還沒有發現例外的情況,如果你發現了請你告訴我.
HANDLE SetClipboardData(UINT uFormat, HANDLE hMem)
它的作用是將hMem所“代表”的內存中的內容以uFormat的格式放到剪貼板上,詳細的參數說明去查MSDN吧,這裏你可能有一些疑問,hMem是個 句柄而內存是用指針來訪問的,你說的沒錯,所以我用了“代表”這個詞而沒有用“指向”,在windows裏很多資源都會有一個HANDLE以它來標識各個 資源一遍於操作系統的管理,內存也一樣,我們一般動態開闢(用new, malloc)的heap都不會被操作系統任意移動,因爲它是一個進程的私有空間,而如果你開闢全局Heap數據的話,操作系統很可能會移動它,如果這個 時候你已然使用指針的話,那麼操作系統一旦移動了一塊全局Heap數據就要修改到所有指向這塊內存的指針,這顯然不現實,而這個時候如果你已然使用你的指 針來管理那塊內存的話,那就出了大麻煩,因爲那塊內存已經被移走了,而如果使用句柄來標識這塊內存的話則會解決這個問題,因爲它只是一個標籤,並沒有實際 的物理意義,就像如果你使用一個人的家庭住址來標識這個人的話就會有麻煩,因爲一旦他搬走了,你就找錯人了, 但是以身份證號就OK了, 詳細的情況可以參考GlobalAlloc這個函數。
BOOL IsClipboardFormatAvailable(UINT uFormat)
這個函數的作用就是要檢查一下剪貼板中的數據是否是uFormat形式的,比如我現打開了mspaint(畫圖板)程序畫了幾筆,然後Ctrl+C,再打 開notepad程序Ctrl+V,你當然知道這不會成功,它就是使用了這個API函數在粘貼前判斷了一下剪貼板中的數據類型是否是我所需要的.

好了我們下面來寫兩個進程來實現它們的通信, 事先說明我寫的只是關鍵代碼並不能直接運行
發送方:

  1. void Send(char* pSnd) 
  2. if (OpenClipboard()) 
  3. HANDLE hClip; 
  4. char *pBuf = NULL; // 對一個指針變量以NULL來初始化是個很好的習慣 
  5. EmptyClipboard(); // 清空剪貼板上的內容 
  6. hClip = GlobalAlloc(GMEM_MOVEABLE, strlen(pSnd) + 1); 
  7. pBuf = (char *)GlobalLock(hClip); 
  8. // 得到句柄標識的內存的實際物理地址,lock後系統就不能把它亂移動了 
  9. strcpy(pBuf, pSnd); 
  10. GlobalUnloak(hClip); // 跟open和close的關係是一樣的,有lock的也不要忘記unlock 
  11. SetClipboardData(CF_TEXT, hClip); 
  12. CloseClipboard(); // 有open就不要忘記close 

在你的程序中加入以上這段話,它就把pSnd中的內容發到了剪貼板上,相當於你作了Ctrl+C, 不信你可以執行這段程序後,打開一個notepad然後手動Ctrl+v看看是不是很驚奇.

  1. void Receive() 
  2. if (OpenClipboard()) 
  3. if (IsClipboardFormatAvailable(CF_TEXT))//判斷剪貼板中的數據是否是文本 
  4. HANDLE hClip; 
  5. char *pBuf = NULL; 
  6. hClip = GetClipboardData(CF_TEXT); // 根據編程中的對稱原則,這個我就不介紹了 
  7. pBuf = (char *)GlobalLock(hClip); 
  8. GlobalUnlock(hClip); 
  9. MessageBox(pBuf); //顯示出來 
  10. }
  11.  
  12. CloseClipboard(); 

上面這段程序就相當於你執行了Ctrl+V操作,它把剪貼板中的數據取了出來;

剪貼板是系統提供的,所有進程都可以訪問它,它就是一段全局內存區,操作系統中的每個進程就都會像線程訪問共享變量一樣的使用它,很簡單,但是問題很多, 正是因爲所有的進程都可以訪問它,所以如果你的兩個進程間的通信如果使用這種方式的話,第一,通信效率不高;第二,會影響到其他進程的執行, 如果我現在Ctrl+C了一段文字,再執行Ctrl+V的時候卻出現了一些亂七八糟的東西的話那就會很麻煩, 所以可以基於剪貼板來做一個簡單的病毒程序,如果你有興趣的話;

2. 匿名管道(Pipe)
現在大多數都是基於管道通信的,因爲每兩個進程都可以共享一個管道來進行單獨的對話,就象打電話單獨佔用一條線路一樣,而不必擔心像剪貼板一樣會有串音, 匿名管道是一種只能在本地機器上實現兩個進程間通信的管道,它只能用來實現一個父進程和一個子進程之間實現數據傳輸.其實它是非常有用的,我做過一個實際 的項目就是利用匿名管道,項目就是讓我寫一個Ping程序來監測網絡的通信狀況,並且要把統計結果和執行過程顯示在我們的軟件裏, windows有一個自帶的ping程序,而且有執行過程和統計,所以我沒必要再發明一個(重複發明就等於犯罪----程序員要牢記阿), 只是windows的那個Ping程序的執行結果都顯示在了CMD的界面上了,我需要把它提取出來顯示在我們的軟件界面上,於是我就利用了匿名管道實現了 這個程序, 當我們的軟件要啓動Ping任務時,我就先CreatePipe創建匿名管道,再CreateProcess啓動了windows下面的Ping程序(它 作爲我們軟件的子進程),當然要把管道的讀寫句柄一起傳給子進程,這樣我就可以輕鬆的把Ping的執行結果了寫入到我的Buffer裏了,是不是很 easy。
BOOL CreatePipe(PHANDLE hReadPipe, PHANDLE hWritePipe, LPSECURITY_ATTRIBUTES lpPipeAttributes, DWORD nSize)
這個API函數是有用來創建匿名管道的,它返回管道的讀寫句柄(hReadPipe,hWritePipe), 記住lpPipeAttributes不能爲NULL,因爲這意味着函數的返回句柄不能被子進程所繼承,你要知道匿名管道可是實現父子進程通信的阿,只有 當一個子進程從其父進程中繼承了匿名管道句柄後,這兩個進程纔可以通信,lpPipeAttributes不爲NULL還遠不 夠,LPSECURITY_ATTRIBUTES這個結構體的內容去查MSDN吧,我只告訴你其中的BOOL bInheritHandle這個成員變量要賦值爲TRUE, 這樣才真正實現了子進程可以從父進程中繼承匿名管道.
BOOL CreateProcess(...) 
這個系統API函數是用來在你的進程中啓動一個子進程用的,它的參數實在太多了,你還是去查MSDN吧,別怪我太懶惰,我只說幾個關鍵的地方,不想說的太詳細.
下面我就在寫一個程序利用匿名管道來通信
父進程的實現:

  1. Class CParent 
  2. .... 
  3. private
  4. HANDLE m_hWrite; 
  5. HANDLE m_hRead; 
  6. void CParent::onCreatePipe() 
  7. SECURITY_ATTRIBUTES sa; // 父進程傳遞給子進程的一些信息 
  8. sa.bInheritHandle = TRUE; // 還記得我上面的提醒吧,這個來允許子進程繼承父進程的管道句柄 
  9. sa.lpSecurityDescriptor = NULL; 
  10. sa.nLength = sizeof(SECURITY_ATTRIBUTES); 
  11. if (!CreatePipe(&m_hRead, &m_hWrite, &sa, 0) 
  12. return
  13.  
  14. STARTUPINFO sui; 
  15. PROCESS_INFOMATION pi; // 保存了所創建子進程的信息 
  16. ZeroMemory(&sui, sizeof(STARTUPINFO));
  17. // 對一個內存區清零,最好用ZeroMemory, 它的速度要快於memset 
  18. sui.cb = sizeof(STARTUPINFO); 
  19. sui.dwFlags = STARTF_USESTDHANDLES; 
  20.  
  21. sui.hStdInput = m_hRead; 
  22. sui.hstdOutput = m_hWrite; 
  23. /*以上兩行也許大家要有些疑問,爲什麼把管道讀句柄(m_hRead)賦值給了hStdInput, 因爲管道是雙向的
  24. ,對於父進程寫的一端正好是子進程讀的一端,而m_hRead就是父進程中對管道讀的一端, 自然要把這個句柄
  25. 給子進程讓它來寫數據了(sui是父進程傳給子進程的數據結構,裏面包含了一些父進程要告訴子進程的一些信
  26. 息),反之一樣*/ 
  27. sui.hStdError = GetStdHandle(STD_ERROR_HANDLE); 
  28.  
  29. if (!CreateProcess("Child.exe", NULL, NULL, NULL, TRUE, 0, NULL, NULL, &sui, &pi)) 
  30. CloseHandle(m_hRead); 
  31. CLoseHandle(m_hWrite); 
  32. return
  33. else 
  34. CloseHandle(pi.hProcess); // 子進程的進程句柄 
  35. Closehandle(pi.hThread); 
  36. // 子進程的線程句柄,windows中進程就是一個線程的容器,每個進程至少有一個線程在執行 
  37.  
  38. void CPraent::OnPiepRead() 
  39. char buf[100]; 
  40. DWORD dwRead; 
  41. if (!ReadFile(hRead, buf, 100, &dwRead, NULL))// 從管道中讀取數據 
  42. /* 這種讀取管道的方式非常不好,最好在實際項目中不要使用,因爲它是阻塞式的,如果這個時候管道中
  43. 沒有數據他就會一直阻塞在那裏, 程序就會被掛起,而對管道來說一端正在讀的時候,另一端是無法寫的
  44. ,也就是說父進程阻塞在這裏後,子進程是無法把數據寫入到管道中的, 在調用ReadFile之前最好調用
  45. PeekNamePipe來檢查管道中是否有數據,它會立即返回, 或者使用重疊式讀取方式,那麼ReadFile的最
  46. 後一個參數不能爲NULL*/ 
  47. return
  48. Messagebox(buf) 
  49.  
  50. void CParent::onPipeWrite(char *pBuf) 
  51. ASSERT(pBuf != NULL); // 這個很重要 
  52. DWORD dwWrite; 
  53. if (!WriteFile(hWrite, pBuf, strlen(pBuf) + 1, &dwWrite, NULL))// 向管道中寫數據 
  54. return

子進程的實現:

  1. Class Child 
  2. ...... 
  3. private
  4. HANDLE m_hRead; 
  5. HANDLE m_hWrite; 
  6. void CChild :: CChild() 
  7. m_hRead = GetStdHandle(STD_INPUT_HANDLE); 
  8. m_hWrite = GetStdhandle(STD_OUTPUT_HANDLE); 
  9. /* GetStdhandle獲得標準輸入輸出句柄,如果你希望你的程序也能跟其他父進程通信的話最好也這麼作
  10. ,並不是所有的程序被創建了後都能跟父進程通信的, 我用過很多老外寫的小程序,它們都提供了標準的
  11. 對外通信接口,這樣很便於你的使用特別對程序員*/ 
  12. void CChild::OnReadPipe() 
  13. void CChild::OnWritePipe() /* 這兩個函數與CParent中的相同 */ 

匿名管道由於是匿名的方式所以它不能實現兩個同級的進程進行通信,因爲一個進程創建了一個管道後,另一個線程並不知道如何找到這個管道,所以它只能通過父 進程直接把管道讀寫柄直接傳遞給子進程的方式進行進程通信,至於爲什麼有了命名管道還要保留匿名管道的問題, 我想主要是因爲父子進程通信的方式已然被廣泛的採用,而這種方式無疑要比命名管道消耗的資源更少,效率更高,就像自己自己寫的進程調用了自己寫的一個函數 一樣。

3. 命名管道(Pipe)
命名管道不僅可以在本機上實現兩個進程間的通信,還可以跨網絡實現兩個進程間的通信,就像我現在正使用MSN跟我遠方的同學聊天一樣!其實如果你用過 Socket編寫網絡程序的話,你就會明白所謂的命名管道之間的通信就相當於把計算機低層網絡網絡通信部分給封裝了起來,使用戶使用起來不必瞭解那麼多網 絡通信的知識,總之一句話就是用起來簡單,其實我們在爲別人提供函數庫的時候都應該遵循這個規律,把低層煩瑣,複雜,抽象的都封裝起來,對高層提供統一的 接口.
在Windows2000/NT以後,都可以在創建管道時指定據有訪問權限的用戶使用管道,進一步保證了安全性,而如果你要是自己使用Socket實現這 個功能的話就太麻煩了,當然很多程序員已然會自己實現它,他們的理由很可能是因爲windows都不安全.命名管道實現進程間的通信也跟網絡通信一樣是 C/S結構的,服務器進程負責創建命名管道及接受客戶機的連接請求,就象socket中Server部分要實現bind、linstening和 accept一樣, 而客戶端只負責連接,對應於socket中的connect一樣.
命名管道提供了兩種基本通信模式:字節模式和消息模式,在字節模式下,數據以一個連續的字節流的形式在server於client之間流動,而消息模式 下,客戶機和服務器則通過一系列不連續的數據單位進行數據收發,每次管道上發出了一條消息後,它必須作爲一條完整的消息讀入,是不是很像TCP和UDP.

HANDLE CreateNamePipe(....)
創建命名管道的API, 我依然不想解釋它的具體參數含義,我只解釋它的第一個參數LPCTSTR lpName,它的字符串格式是"\\\\.\\pipe\\pipename"
爲什麼這麼多\, 其實一共就4個,可你看到有8個是因爲C/C++中字符串中如果包含一個'\'就必須"\\"才能表達它的意思,你還記得嗎?它的實際格式是"\\. \pipe\pipename",它的'.'表示的是本機地址,如果是要與遠程服務器連接,就在這個'.'處指定服務器的名稱,接下來的pipe是固定的 不要改,pipename就是你要命名的管道名字.

BOOL ConnectNamedPipe(HANDLE hNamePipe, LPOVERLAPPED lpOverlapped)
初看這個函數的名字你一定認爲這個是客戶端用來連接服務器管道的,事物的表面總是欺騙我們,恰恰相反它是服務器用來等待遠程連接的,類似於socket中的listen.
BOOL WaitNamedPipe(LPCTSTR lpNamedPipeName, DWORD nTimeOut)
有了上面那個函數的教訓,如果我問題這個函數是作什麼的你一定不會立即回答,是的,它是在客戶端來判斷是否有可以利用的命名管道的,每個客戶端最開始都應該使用它判斷一些,就像socket中的connect要判斷一下server是否已經啓動了.

下面是服務器代碼:

  1. class CNamePipeServer 
  2. ... 
  3. private
  4. HANDLE m_hPipe; 
  5. /* 創建命名管道等待客戶端連接 */ 
  6. void CNamePipeServer::NamePipeCreated() 
  7. m_hPipe = CreateNamedPipe("\\\\.\\pipe\\MyPipe"
  8.  
  9. PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, 
  10. 0, 1, 1024, 1024, 0, NULL); 
  11. if (INVALID_HANDLE_VALUE == m_hPipe) 
  12. return
  13. HANDLE hEvent; 
  14. hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); // 創建一個事件 
  15. if (INVALID_HANDLE_VALUE == hEvent) 
  16. return
  17.  
  18. OVERLAPPED ovlap; 
  19. ZeroMemory(&ovlap, sizeof(OVERLAPPED)); 
  20. ovlap。hEvent = hEvent; 
  21. /* 等待客戶連接 , 採用了重疊方式, 該函數會立即返回不會阻塞*/ 
  22. if (!ConnectNamePipe(hPipe, &ovlap)) 
  23. /* 由於函數會立即返回,所以在沒有連接的時候不會阻塞會返回,這個時候要判斷錯誤失敗的原因*/ 
  24. if (ERROR_IO_PENDING != GetLastError()) 
  25. .... 
  26. return
  27. /* 一個連接到來的時候,Event會立即變爲有信號狀態 */ 
  28. if (WAIT_FAILED == WaitForSingleObject(hEvent, INFINTE)) 
  29. ... 
  30. return
  31. CloseHandle(hEvent); 
  32. void CNamePipeServer::OnReadPipe() 
  33. void CNamePipeServer::OnWritePipe() 

命名管道讀寫的方式與匿名管道的相同, 不再冗述。
客戶端實現:

  1. clase CNamePipeClient 
  2. ... 
  3. private
  4. HANDLE m_hPipe; 
  5. void CNamePipeClient::OnPipeConnect() 
  6. if (!WaitNamedPipe("\\\\.\\pipe\\MyPipe", NMPWAIT_WAIT_FOREVER)) 
  7. return
  8. /* 打開命名管道,與服務器進行通信 , CreateFile這個函數是不是很熟悉,是的我們寫文件的時候都用
  9. 這個API,其實不僅是創建文件,只要是句柄標識的資源似乎都可以用它來來創建,如 與硬件(COM口)之間
  10. 的通信等,這就是對下層具體實現封裝,對上提供統一接口的好處,不然不知道我們又要多記多少個API函數*/ 
  11. m_hPipe = CreateFile("\\\\.\\pipe\\MyPipe", GENERIC_READ | GENERIC_WRITE, 0, NULL
  12. , OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); 
  13. if (INVALID_HANDLE_VALUE == m_hPipe) 
  14. reutrn; 
  15. void CNamePipeClient::OnReadPipe() 
  16. void CNamePipeClient::OnWritePipe() 

同上.

命名管道我沒有在實際中使用過,所以對它的一些特點理解的並不是很透徹,不能爲大家提供更多的建議了.

4. 郵槽(Mailslot)
郵槽是基於廣播通信設計出來的,採用不可靠的數據傳輸,它是一種單向通信機制,創建郵槽的服務器進程讀取數據,打開郵槽的客戶端進程寫入數據,據說郵槽廣泛的應用於網絡會議系統.
服務器進程

  1. void MailslotRecv() 
  2. HANDLE hMailslot; 
  3. /* 創建郵槽 */ 
  4. hMailslot = Createmailslot("\\\\.\\mailsolt\\MyMailslot", 0, MAILSLOT_WAIT_FOREVER, NULL); 
  5. if (INVALID_HANDLE_VALUE 
  6.  
  7. == hMailslot) 
  8. return
  9.  
  10. char buf[100] 
  11. DWORD dwRead; 
  12. /* ReadFile在讀取郵槽中的數據的時候,如果暫時沒有數據它會阻塞在那裏,但是一旦有了數據後就立刻返回,
  13. 它在本端的讀操作不影響另一端的寫操作, 這一點不同於Pipe*/ 
  14. if (!ReadFile(hMailslot, buf, 100, &dwRead, NULL)) 
  15. ... 
  16. return
  17. MessageBox(buf); 
  18. CloseHandle(hMailslot); 

客戶端進程:

  1. void MailslotSnd(char *pBuf) 
  2. ASERRT(pBuf != NULL); 
  3. HANDLE hMailslot; 
  4. /* 又是CreateFile,啥也不說了,太帥了*/ 
  5. hMailslot = CreateFile("\\\\.\\mailslot\\MyMailslot", ENERIC_READ | GENERIC_WRITE
  6. , 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); 
  7. if (INVALID_HANDLE_VALUE == hMailslot) 
  8. return
  9.  
  10. DWORD dwWrite; 
  11. if (!WriteFile(hMailslot, pBuf, strlen(pBuf) + 1, &dwWrite, NULL)) 
  12. .... 
  13. return
  14. CloseHandle(hMailslot); 

郵槽的使用是不是更簡單, 我同樣也沒有在實際的項目中使用過它,依然不作過多的評價.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章