Win32 行程通訊的觀念與技術
窗子提供的永遠只是局部的風景。身為窗子的制造者以及使用者的我們不可能不明白這個道理;對於窗子的使用者日益挑剔的品味,窗子的制造者所能提供的不僅止於窗子的大小,往往是窗子的數量。的確,探出窗去看得將更多一些,外頭天空地寬朗朗白日,別的窗子也許正有我們想要的風景。
這篇文章談的是 Interprocess Communication (IPC),我將與你分享跨行程通訊的各項技術與資料交換的方法。
為什麽需要 IPC?
為什麽需要 Inter-process Communication?
顯而易見的,沒有一個視窗應用程式可以包辦全部的工作。為了避免資料重覆輸入的時間浪費與人為錯誤,各應用程式間的資料會有互相交換的需求。首先的壓力將來自於使用者,甚至於很可能是你自己。先不說別的,在寫這篇文章時,我就曾剪貼原來以 Delphi 撰寫的程式到文書編輯軟體,同時,也利用抓圖軟體幫我拍下執行畫面,最後,這些文章與范例程式得用壓縮程式壓起來,然後E-mail寄給雜志編輯。
使用 IPC 在某些情況下是不得不然的決定,有時候程式必須跨過機器邊界讓另一部機器內的程式明白該怎麽合作來共同完成工作,這同時也暗示我們可能面臨不同的作業系統的問題。
此外,IPC有助於系統的安全與穩定。由於Win32各個行程彼此獨立的特性,一個行程死掉了,其他的行程還可以繼續跑下去,對於某些穩定性要求很高的系統而言, 值得以額外的負擔(Overhead)交換系統的強固性(robustness)。嗯! 我的意思是說,因為系統對穩定性的需求要求較高,值得拆開來做甚至額外的備援系統,既然工作拆成兩個以上,此時必然需要IPC。
關於IPC,一般人可能會對其有「執行效率緩慢」的印象,這當然不能說是錯誤的,但絕不是公平的評語。這麽說吧:一個主管親自去做一件事,往往會比先說明再授權下屬去做來得快,這是單一工作時的情況;然而如果管理者同時有好幾件事在手上,托付別人去做才能使得整個公司的效能提高。換句話說,如果能善用 IPC,整體的系統效能不僅不會下降,反而可能因為充分利用整個運算群的能力而有提升。
我們的第一個 IPC 例子
每個圖形介面的視窗應用程式都接受並處理訊息(Message),因此,使用訊息伫列通知其他的行程是腦中很自然會浮現的第一個想法;換句話說,兩行程間彼此互相以 SendMessage() 或 PostMessage() 傳送訊息通知對方。
即然要互送訊息,就需要一個彼此都認得的訊息編號。於是,除了 Windows 標准的訊息編號之外,我們還需要額外定義一個(一些)訊息。
行程通訊間用來約定訊息編號常用的方法是呼叫RegisterWindowMessage() API函數。這個函數只有一個字串型別的引數,Windows系統會檢查我們傳入的訊息名稱並傳回一個安全不重覆的訊息編號,假如傳入的訊息名稱早已經登記有案,則系統傳回的是稍早傳給那個行程的相同編號。
換句話說,兩支程式只要彼此都用相同的訊息名稱呼叫RegisterWindowMessage() 注冊訊息,系統便會都給兩者一個相同的自訂訊息編號。
接下來要送出訊息了,可是,要送給誰呢?嗯,我在這 使用的方法是:第一次先用廣播的,每一個視窗程式都會收到通知,訊息的短三數(wParam)中寫明發訊視窗的 Handle 值,如果是同志,它自然明白這個訊息代表了什麽,並且也使用SendMessage() 回送約定的訊息表示收到。同樣的,訊息的短三數注明自己的Handle。於是,茫茫人海的小倆口終於得知對方的下落,以後就不再需要公開尋人可以透過Handle值直接與對方聯絡了。
除了訊息編號,訊息的wParam,lParam長短三數也可以用來進一步約定通訊的細節。事情進展得似乎十分順利,現在我們知道合作對象,也確信它明白我們的訊息代表什麽。雖然簡單,但是這種暗通款曲的方式是系統默許的。不過,我們還需要再多解決一個問題。
由於SendMessage 只有 wParam,lParam 兩個 DWORD 型別的長短三數,攜帶的資料量十分有限。很顯然的,我們需要能夠一次傳送更多資料的方法。Windows 也的確提供了許多交換資料的機制,我在這篇文章中將會一一說明,其中最簡便的方法是使用 WM_COPYDATA 訊息,作法如下
將資料內容指定到COPYDATASTRUCT這個資料結構中。
必須使用SendMessage()送出 WM_COPYDATA訊息,訊息的短三數是發訊端視窗的Handle值,長三數的內容則是指向COPYDATASTRUCT的指標。
受訊端行程收到訊息時,以長三數提供的線索依址取回資料。
小倆口書信往返時系統是居中牽線的紅娘。就在發訊視窗送出WM_COPYDATA訊息,受訊視窗取得內容之間,系統在背後默默接管記憶體管理的瑣事。有關WM_COPYDATA的使用有一點需要提醒讀者的,收訊端應該視這塊記憶體是唯讀的,如果後來程式處理需要這些資料,應該要先將之拷貝出來。
多虧有了這項特殊的性質,使得WM_COPYDATA與訊息溝通模型成為 Win32 平台上少數同時支援 16-bit與32-bit應用程式的IPC機制。你可以在WM_COPYDATA目錄找到范例程式TwinApp的完整原始程式。
IPC基本概念的討論
總結來說,上述的例子是兩個行程彼此利用RegisterWindowMessage()注冊所得的編號對送訊息,並且利用訊息的長短三數進一步協定通訊的內容與細節,對於資料量比較大的資料則使用WM_COPYDATA。
眼尖的讀者在檢視TwinApp時也許會察覺到一些DDE的影子。當然,比起DDE來說,TwinApp范例程式的訊息溝通模型實在陽春,缺點也不少。不過我的用意本來就不在於一開始就寫一個大型程式出來嚇唬人;相反的,我打算提供一個簡單的例子,並且從這個例子支解出有關行程通訊的幾個重要的觀念與特性,這些特性並不是TwinApp所獨有的,對於其他IPC機制的討論也有相同的價值,等我們扣緊了對IPC的感覺,再陸續討論其他 Win32 平台所支援的IPC機制。
話說內行的看門道,外行的看熱鬧。或許我算不得頂尖高手,但至少應該比看熱鬧的多看出一些東西來吧! :p 觀察TwinApp這個例子 --
行程之間彼此有共同的通訊協定
通訊的僅限於單機,稍候討論的IPC有些則是可以跨過機器邊界甚至網域.
Process在行程通訊中的角色扮演
一般來說,三與IPC的行程可以歸類成Client與Server兩類,所謂的Server指的是提供服務的行程;Client指的是使用或向Server要求服務的行程。
真實的世界中,人的角色扮演是隨情境而變的。我們會是別人的子女,但也同時是別人的爸媽; 即使同樣是夫妻,居家生活與外出場合的行為表現也有差異。界定某一程式是Client與Server的角色端視當時的情況而定並非絕對的。舉例來說,文書處理軟體可能向試算表要求庫存統計資料,此時試算表扮演的是Server的角色,但在試算表向庫存管理系統索取統計資料的場合,試算表則是Client。
以我們的第一個例子TwinApp來說,彼此既接收訊息,同時也主動發出訊息。既可以是Client也可以是Server,沒有明顯的主從之別,對於這樣的情況,有一個專有名詞叫「對等模式」(Peer-to-peer model) 。
同步與非同步的討論
TwinApp使用SendMessage()送出訊息,程式會暫停在SendMessage()那行等待訊息處理結束返回後再繼續下一列程式,這樣的情況屬於同步處理。同步(Synchronous)與非同步(Asynchronous)在IPC中是一個非常重要的論題,有必要先對這兩個名詞先做說明:
假設程序A呼叫程序B時,若是A先暫停一直等到程序B結束返回後再繼續程序A的下一動作,我們稱其為同步(Synchronous);另一種情況是 -- 如果A呼叫B之後,不等B執行完,就直接進行A的下一動作,則是所謂的不同步。
以提款機為例,我們會先插入卡片,輸入密碼,鍵入金額,然後是內部安全與帳務查核,最後收回卡片及金額,列印交易明細,一動接一動按步就班;同樣是提款這件事,某位老板可以交待會計小姐去提款,交待完之後他就迳自去忙別的事,等到會計小姐提款回來,再向老板回報,這樣的程序是所謂的非同步。
如果進一步觀察提款這個例子:會計小姐什麽時候出門什麽時候回來是算不得准的,假定這位老板除了會計小姐之外,另外還交辦旁人其他工作,可以預見的,不一定那一件工作會先做完。由於執行的次序無法預估,采用非同步方式設計的行程通訊將會多出許多協調與事件處理的工作,使得彼此之間總互相期待點什麽。
三與通訊的行程個數,訊息資料的流向
在TwinApp中,簡單的只有兩個端點。但在實際應用的場合,Server通常得同時應付好幾個Client的要求,如何妥善照顧到每一個Client同時要兼顧系統執行的效能,是門很大的學問。
當行程對行程搭起通訊的鵲挢時,這座挢是單行道或者是雙向通行,同樣也值得列入評估要素。不過有一點需要注意的: 不論選擇單工或雙工的IPC機制,並不構成我們建立雙向溝通無可跨越的天塹,話說山不轉路轉,蓋兩座單向的挢一樣可以有雙向通行的效果,不過就先天本質的特性來說,某些IPC機制確實比較容易作出雙工的效果,當然也有天生大嘴巴適合用來廣播的,例如本文稍後敘述的MailSlot。
資料的可視性與安全性交換的資料在行程之間當然必須是可見的,TwinApp是用WM_COPYDATA交出資料。IPC有些技術是可以讓行程共同存取資料的,稍候我們在 Shared memory 時將有討論.
是否需要有視窗或者純Console Application也能應用.TwinAPP是以SendMesasage()送出訊息,這表示需要有視窗才行得通。如果你設計的是純Console Mode 的應用程式,那麽,選用不需要視窗Handle也行得通的IPC機制(例如pipe)會比較適合。
關於執行效能的討論許多人耽心IPC的執行效能,的確,先不說別的,光是啟動另一個Process本身就比啟動一個Thread 的Overhead要高上很多。如果涉及協調的問題,建立一個Mutex的時間也比Critical section慢上不知多少倍。遺憾的是我們卻