线程通信(ITC)
為什么要通信
通信是人的基本需求。而進程作為人的發明,自然脫離不了人的習性,也有通信需求。如果進程之間不進行任何通信,那么進程所能完成的任務就要大打折扣。 例如,父進程在創建子進程后,通常須要監督子進程的狀態,以便在子進程沒有完成給定的任務時,可以再創建一個子進程來繼續。這就需要父子進程間通信。
而線程間的通信則需要更多。由于一個進程通常包括多個線程,這多個線程之間因資源共享自然地就存在一種合作關系。這種合作關系雖然可以表現為相互獨立,但更多地時候是互相交互。這就是通信。就像舞臺上的多個演員,他們之間是一種合作關系,共同將戲演好。雖然這些演員在舞臺上的時候可以各自演各自的,不說話,也沒有肢體接觸,即沒有交互,但他們更多的時候會進行對白和擁抱等交互操作。
線程之間的交互我們就稱之為線程通信。線程通信是從進程通信演變而來的,進程通信有個專有縮寫,叫IPC( Inter-Process Communication)。由于每個進程至少有一個線程,進程的通信就是進程里面的線程通信。在隨后的討論中,我們將統一使用線程通信來進行講解。
那么線程之間的通信是如何進行的呢?
舞臺上的演員可以通過對白,手勢和擁抱等方法來交互通信。類似地,線程也可以同樣的方式來進行通信。下面我們就來看一下線程的這些交互方式。
管道、記名管道、套接字
演員最常使用的交互手段就是對白。對白就是一方發出聲音,另一方接受聲音。聲音的傳遞則通過空氣(當面或無線交談)、線纜(有線電話)進行傳遞。類似地,線程對白就是一個線程發出某種數據信息,另外一方接受數據信息,這些數據信息通過一片共享的存儲空間進行傳遞。
在這種方式下,一個線程向這片存儲空間的一端寫入信息,另一個線程從存儲空間的另外一端讀取信息。這看上去像什么?管道。管道所占的空間既可以是內存,也可以是磁盤。就像兩人對白的媒介可以是空氣,也可以是線纜一樣。要創建一個管道,一個線程只需調用管道創建的系統調用即可。
管道(無名管道)
從根本上說,管道是一個線性字節數組,類似文件。使用文件讀寫的方式進行訪問,但卻不是文件。因為通過文件系統看不到管道的存在。另外,我們前面說了,管道可以設在內存里,而文件很少設在內存里。創建管道在殼命令行下和在程序里是不同的。殼命令行下,只需要使用符號“|”即可。
在程序里面,創建管道需要使用系統調用popen()或者pipe()。popen需要提供一個目標進程作為參數,然后在調用該函數的的進程和給出的目標進程之間創建一個管道。這很像人們打電話時必須提供對方的號碼,才能創建連接一樣。
創建時還需要提供一個參數表明管道類型:讀管道或者是寫管道。而 pipe 調用將返回兩個文件描述符(文件描述符是用來識別一個文件流的一個整數,與句柄不同),其中一個用于從管道進行讀操作,一個用于寫入管道。也就是說, pipe將兩個文件描述符連接起來,使得一端可以讀,另一端可以寫。通常情況下,在使用pipe調用創建管道后,再使用fork產生兩個進程,這兩個進程使用pipe返回的兩個文件描述符進行通信。
例如,下述代碼段創建一個管道并利用它在父子進程間通信。int pp [2 ] ;
言
(無名)管道的一個重要特點是使用管道的兩個線程之間必須存在某種關系, 例如,使用popen需要提供另一端進程的文件名,使用pipe的兩個線程則分別隸屬于父子進程。
記名管道
如果要在兩個不相關的線程,如兩個不同進程里面的線程,之間進行管道通信,則需要使用記名管道。顧名思義,記名管道是一個有名字的通信管道。記名管道與文件系統共享一個名字空間,印我們可以從文件系統中看到記名管道。也就是說,記名管道的名字不能與文件系統里的任何文件名重名。
一個線程通過創一個記名管道后,另外一個線程可使用open來打開這個管道(無名管道則不能使用open),從而與另外一端進行交流。(或者使用已經存在的管道)。記名管道的名稱由兩部分組成,計算機名和管道名,例如\[主機名]\管道\[管道名]。
對于同一主機來講允許有多個同一命名管道的實例,并且可以由不同的進程打開,但是不同的管道都有屬于門己的管道緩沖區,而且有自己的通信環境,互不影響。命名管道可以支持多個客戶端連接一個服務器端。命名管道客戶端不但可以與本機上的服務器通信也可以同其他主機上的服務器通信。
管道和記名管道雖然具有簡單,無需特殊設計(指應用程序方面)就可以和另外一個進程遠行通信的優點,但其缺點也是顯然的。首先是管道和記名管道并不是所有操作系統都支特。主要支持管道通信方式的是UNIX和類UNIX(如Linux )的操作系統。 這樣,如果需要在其他操作系統上進行通信,管道機制就多半會力不從心了。其次,管道通信需要在相關的進程間進行(無名管道),或者需要知道按名字來打開(記名管道),而這在某些時候會十分不便。
套接字
套接字(socket)是另外一種可以用于進程間通信的機制!套接字首先在BSD中出現,隨后幾乎滲透到所有主流操作系統。套接字的功能非常強大,可以支持不同層面,不同應用,跨網絡的通信。使用套接字進行通信需要雙方均創建一個套接字,其中一方作為服務器方,另外一方作為客戶方。服務器方必須先創建一個服務器套接字,然后在該套接字上進行監聽,等待遠方的連接請求。欲與服務器通信的客戶則創建一個客戶套接字,然后向服務器套接字發送連接請求。服務器套接字在收到連接請求后,將在服務器機器上創建一個客戶套接字,與遠方的客戶機上的客戶套接字形成點到點的通信通道。之后,客戶端和服務器端就可以通過send和recv命令在這個創建的套接字通道上進行交流了。
服務器套接字有點類似于傳說中的蟲洞(worm hole)。蟲洞的一端是開放的,它在宇宙內或宇宙間漂移著,另外一端處于一個不同的宇宙,監聽是否有任何東西從蟲洞來。而欲使用蟲洞者需要找到蟲洞的開口端(發送連接請求),然后穿越蟲洞即可。
這里需要指出的是服務器套接字既不發送數據,也不接收數據(指不接受正常的用戶數據而不是連接請求數據),而僅僅是生產出“客戶"套接字。 當其他(遠方)的客戶套接字發出一個連接請求時,我們就創建一個客戶套接字。一旦客戶套接字clientsocket創建成功,與客戶的通信任務就交給了這個剛剛創建的客戶套接字。而原本的服務器套接字serversocket則回到其原來的監聽操作上。
套接字由于其功能強大而獲得了很大發展,并出現了許多種類。不同的操作系統均支持或實現了某種套接字功能。例如按照傳輸媒介是否為本地,套接字可以分為本地(UNIX域)套接字和網域套接字。而網域套接字又按照其提供的數據傳輸特性分為幾個大類,分別是:
- 數據流套接字(stream socket):揖供雙向,有序、可靠、非重復數據通信。
- 電報流套接字( datagram socket)|提供雙向消息流。數據不一定按序到達。
- 序列包套接字( sequential packet):提供雙向,有序、可靠連接,包有最大限制。
- 裸套接字(raw socket):提供對下層通信協議的訪問。
套接字從某種程度上來說非常繁雜,各種操作系統對其處理并不完全一樣。因此,如要了解某個特定套接字實現,讀者需要查閱關于該套接字實現的具體手冊或相關文檔。
信號
管道和套接字雖然提供了豐富的通信語義,并且也得到了廣泛應用,但它們也存在某些缺點,并且在某些時候,這兩種通信機制會顯得很不好使。
首先,如果使用管道和套接字方式來通信,必須事先在通信的進程間建立連接(創建管道或套接字),這需要消耗系統資源。其次,通信是自愿的。 即一方雖然可以隨意往管道或套接字發送信息,對方卻可以選擇接收的時機。即使對方對此充耳不聞,你也奈何不得。再次,由于建立連接消耗時間,一旦建立,我們就想進行盡可能多的通信。而如果通信的信息量微小,,如我們只是想通知一個進程某件事情的發生,則用管道和套接字就有點“殺雞用牛刀"的味道,效率十分低下。
因此,我們需要一種不同的機制來處理如下通信需求:
- 想迫使一方對我們的通信立即作出回應。
- 我們不愿事先建立任何連接,面是臨時突然覺得需要向某個進程通信。
- 傳輸的信息量微小,使用管道或套接字不劃算。
應付上述需求,我們使用的是信號( signal )。
那么信號是什么呢?在計算機里,信號就是一個內核對象,或者說一個內核數據結構。發送方將該數據結構的內容填好,并指明該信號的目標進程后,發出特定的軟件中斷。操作系統接受到特定的中斷請求后,知道是有進程要發送信號,于是到特定的內核數據結構里查找信號接受方,并進行通知。接到通知的進程則對信號進行相應處理。如果對方選擇不對信號作出反應,則將終止操作系統運行。
信號量
信號量( Semaphore)是由荷蘭人E W. Dijkstra 在60年代所構思出的一種程序設計構造。其原型來源于鐵路的運行:在一條單軌鐵路上,任何時候只能有一列列車行駛在上面。而管理這條鐵路的系統就是信號量。任何一列火車必須等到表明鐵路可以行駛的信號后才能進入軌道。當一列火車進入單軌運行后,需要將信號改為禁止進人,從而防止別的火車同時進入軌道。面當火車駛出單軌后,則需要將信號變回到允許進入狀態。
在計算機里,信號量實際上就是一個簡單整數。一個進程在信號變為0或者1的情況下推進,并且將信號變為1或0來防止別的進程推進。當進程完成任務后,則將信號再改變為0或1,從而允許其他進程執行。
信號量不光是一種通信機制,更是一種同步機制。
共享內存
管道,套接字,信號,信號量,雖然滿足了多種通信需要,但還是有一種需要未能滿足。這就是兩個進程需要共享大量數據。這就像兩個人,他們互相喜歡,并想要一起生活時(共享大量數據),打電話,握手,對白等就顯得不夠了,這個時候需要的是擁抱,只有將其緊緊擁抱于懷,感覺才最到位,也才能盡可能地共享。
進程的擁抱就是共享內存。共享內存就是兩個進程共同擁有同一片內存。 這片內存中的任何內容,二者均可以訪問。要使用共享內存進行通信,一個進程首先創建一片內存空間專門作為通信用,而其他進程則將該片內存映射到自己的(虛擬)地址空間。這樣,讀寫自己地址空間中對應共享內存的區域時,就是在和其他進程進行通信。
乍一看,共享內存有點像管道,有些管道不也是一片共享內存嗎?這是形似而神不似。首先,使用共享內存機制通信的兩個進程必須在同一臺物理機器上;其次,共享內存的訪問方式是隨機的,而不是只能從一端寫,另一端讀。還有一點,就是管道中的數據一讀就沒有了(只能讀一次),而共享內存中的數據可以反復讀(只要不被覆蓋,刪除)因此其靈活性比管道和套接字大很多,能夠傳遞的信息也復雜得多。
共享內存的缺點是管理復雜,且兩個進程必須在同一臺物理機器上才能使用這種通信方式。共享內存的另外一個缺點是安全性脆弱。因為兩個進程存在一片共享的內存,如果一個進程染有病毒,很容易就會傳給另外一個進程。就像兩個緊密接觸的人,一個人的病毒是很容易傳給另外一個人的。
這里需要提請讀者注意的是,使用全局變量在同一個進程的線程間實現通信不稱為共享內存。
消息隊列
消息隊列是一列具有頭和尾的消息排列,新來的消息放在隊列尾部,而讀取消息則從隊列頭部開始。
乍一看,這不是管道嗎?一頭兒讀、一頭兒寫?沒錯。這的確看上去像管道。但它不是管道。首先它無需固定的讀寫進程,任何進程都可以讀寫(當然是有權限的講程)。其次,它可以同時支持多個進程,多個進程可以讀寫消息隊列。即所謂的多對多,而不是管道的點對點。另外,消息隊列只在內存中實現。
最后,它并不是只在UNIX和類UNIX操作系統實現。幾乎所有主流操作系統都支持消息隊列。
其他通信機制
除了上面介紹的主流通信方式外,有些操作系統還提供了一些其操作系統所特有的通信機制,例如Windows支持的進程通信方式就有剪貼板(clipboard), COM/DCOM,動態數據交換(DDE) ,郵箱( mailslots) ;而 solaris則有所謂的solaris 門機制,讓客戶通過輕量級(16KB)系統調用使用服務器的服務。
雖然進程之間的通信機制繁多,且每種機制有著自己獨特的特性,但歸根結底都來源于AT&T的 UNIX V系統。該系統在1983年加入了對共享內存、信號量和消息隊列的支持。這三者就是眾所周知的System V IPC( POSIX IPC 也是源于該系統并成為當前IPC的標準)。因此,雖然不同操作系統的IPC機制可能不盡相同,但其基本原理則并無大的不同。如果需要了解具體操作系統的IPC機制的實現。
總結
- 上一篇: 腰椎!
- 下一篇: CF120F Spiders 题解