1. 程式人生 > >Linux程序間通訊的基本原理、通訊方式及其同步方式的理解

Linux程序間通訊的基本原理、通訊方式及其同步方式的理解

***基本原理***:
通常情況下,程式只能訪問自身的資料,和其它程序沒有溝通,每個程序都是一個單獨存在的個體,程序之間不需要協作就可以完成自身的任務了。但隨著需要解決問題複雜性的增加,一個程序不可能完成所有的工作,必須由多個程序之間互相配合才能更快、更好、更強的解決問題,如同人與人之間的協作可以做出更大的事情一樣。

但是,處於安全性的考慮,OS會限制程序只能訪問自身的資料,不能把“手”伸到其它程序的內部,這怎麼辦呢?程序間的溝通問題怎麼解決呢?

有問題,就需要溝通;需要溝通,就需要有溝通的媒介;為了公平和公正,溝通的媒介的控制權不應該屬於溝通的任何一方。以此推論下去,在計算機系統中,承擔溝通媒介控制任務的就只有OS自身了。

所以,程序間通訊的基本原理就是:OS提供了溝通的媒介供程序之間“對話”用。既然要溝通,如同人類社會的溝通一樣,溝通要付出時間和金錢,計算機中也一樣,必然有溝通需要付出的成本。出於所解決問題的特性,OS提供了多種溝通的方式,每種方式的溝通成本也不盡相同,使用成本和溝通效率也有所不同。我們經常聽到的 管道、訊息佇列、共享記憶體都是OS提供的供程序之間對話的方式。

既然是溝通,必然是溝通雙方有秩序的說話,否則就成吵架了,誰也聽不到對方說什麼。如同法庭中法官控制控辯雙方的發言時機和發言時間一樣,OS也必須提供此類的管制方式使得程序的溝通顯的有序和諧。我們經常聽到的 互斥鎖、條件變數、記錄鎖、檔案鎖、訊號燈均屬此列。

溝通的媒介是什麼? 

上面我們提到管道、訊息佇列、共享記憶體都是OS所提供的對話的方式,程序所說出去的“話”至少需要暫時儲存在某個地方,然後才能被別的程序取走(聽到)。不同的實現對話方式,儲存中間資訊的媒介從邏輯上分有檔案系統,核心和記憶體三個部分。其實儲存在核心中也是儲存在記憶體中的,只是這部分記憶體只能OS自己訪問,普通程序不能直接讀寫。

***通訊方式***:

經常使用到的程序間通訊有:管道、訊息佇列和共享記憶體。

>>>管道
最開始出現的管道是用於具有血緣關係的程序之間共享資料的。父程序首先建立一個管道,然後再fork()出子程序,子程序自動共享對管道的訪問許可權。這種管道是沒有名字的(只有程序中的一個識別符號進行標識),因此也稱為匿名管道。
如我們在shell中執行 ls * | grep foo 時,shell就會建立一個匿名管道,並把ls的stdout重定向到該管道的輸入端,把grep的stdin重定向到管道的輸出端,ls 的輸出就自動的稱為grep的輸入。這一切對 ls 和 grep 都是透明的,它們並不知道管道的存在。
後來就出現了FIFO(first in first out),也稱為有名管道。有名和無名是針對OS來說的,OS能直接管理到就是有名的,否則就是無名的。FIFO出現後,管道就不僅僅用於具有血緣關係程序的通訊了,而是可以用於任意程序之間的通訊。
管道的生命週期是跟隨程序的。當所有使用管道的程序退出或者所有程序都顯示的呼叫close()後,管道就會被丟棄掉(其實程序退出等效於呼叫close(),因為程序退出時所有開啟的資源識別符號都會被OS關掉)。如果這時候管道中仍然有未讀取的資料,這些資料同樣被丟棄掉。而且在往管道中寫入資料時,必須存在讀取資料的程序,不然就沒有任何意義了。

>>>
訊息佇列
訊息佇列的作用與管道類似,有足夠寫許可權的程序可往訊息佇列中放置訊息,有足夠讀許可權的的程序可以從訊息佇列中讀取訊息。與管道不同的是在某個程序寫入訊息之前,並不需要另外某個程序在該佇列上等待訊息的到達。這是因為訊息佇列的宣告週期是跟隨核心的。只要核心不刪除訊息佇列,即使程序退出了,訊息佇列依然是存在的。(不過在現有的OS的實現中,訊息佇列是通過引用計數的方式記錄當前有多少個程序已經打開了自己,所有程序退出時,引用計數為0時,此時OS也會自動的刪除訊息佇列)。

>>>共享記憶體
共享記憶體也有兩種:匿名共享記憶體和有名共享記憶體。它們的區別和上面提到的管道和FIFO一樣,匿名共享記憶體只能用於具有血緣關係的程序間通訊,有名共享記憶體則沒有此限制。
共享記憶體的宣告週期也是跟隨核心的。與上述兩種方式最大的不同在於,使用共享記憶體溝通的程序之間進行資料交換是不經過系統呼叫進行程序--核心的資料copy過程的,所有的資料交換都是在記憶體中完成的。相比於前兩種方式,共享記憶體的方式是速度最快的。

***同步方式***:

為了能夠有效的控制多個程序之間的溝通過程,保證溝通過程的有序和和諧,OS必須提供一定的同步機制保證程序之間不會自說自話而是有效的協同工作。比如在共享記憶體的通訊方式中,兩個或者多個程序都要對共享的記憶體進行資料寫入,那麼怎麼才能保證一個程序在寫入的過程中不被其它的程序打斷,保證資料的完整性呢?又怎麼保證讀取程序在讀取資料的過程中資料不會變動,保證讀取出的資料是完整有效的呢?

常用的同步方式有: 互斥鎖、條件變數、讀寫鎖、記錄鎖(檔案鎖)和訊號燈。

>>>互斥鎖
顧名思義,鎖是用來鎖住某種東西的,鎖住之後只有有鑰匙的人才能對鎖住的東西擁有控制權(把鎖砸了,把東西偷走的小偷不在我們的討論範圍了)。所謂互斥,從字面上理解就是互相排斥。因此互斥鎖從字面上理解就是一點程序擁有了這個鎖,它將排斥其它所有的程序訪問被鎖住的東西,其它的程序如果需要鎖就只能等待,等待擁有鎖的程序把鎖開啟後才能繼續執行。
在實現中,鎖並不是與某個具體的變數進行關聯,它本身是一個獨立的物件。進(線)程在有需要的時候獲得此物件,用完不需要時就釋放掉。
互斥鎖的主要特點是互斥鎖的釋放必須由上鎖的進(線)程釋放,如果擁有鎖的進(線)程不釋放,那麼其它的進(線)程永遠也沒有機會獲得所需要的互斥鎖。互斥鎖主要用於執行緒之間的同步。
條件變數
上文中提到,對於互斥鎖而言,如果擁有鎖的進(線)程不釋放鎖,其它進(線)程永遠沒機會獲得鎖,也就永遠沒有機會繼續執行後續的邏輯。在實際環境下,一個執行緒A需要改變一個共享變數X的值,為了保證在修改的過程中X不會被其它的執行緒修改,執行緒A必須首先獲得對X的鎖。現在假如A已經獲得鎖了,由於業務邏輯的需要,只有當X的值小於0時,執行緒A才能執行後續的邏輯,於是執行緒A必須把互斥鎖釋放掉,然後繼續“忙等”。如下面的虛擬碼所示:

// get x lock
while(x <= 0){
// unlock x ;
// wait some time
// get x lock
}

// unlock x

 這種方式是比較消耗系統的資源的,因為程序必須不停的主動獲得鎖、檢查X條件、釋放鎖、再獲得鎖、再檢查、再釋放,一直到滿足執行的條件的時候才可以。因此我們需要另外一種不同的同步方式,當執行緒X發現被鎖定的變數不滿足條件時會自動的釋放鎖並把自身置於等待狀態,讓出CPU的控制權給其它執行緒。其它執行緒此時就有機會去修改X的值,當修改完成後再通知那些由於條件不滿足而陷入等待狀態的執行緒。這是一種通知模型的同步方式,大大的節省了CPU的計算資源,減少了執行緒之間的競爭,而且提高了執行緒之間的系統工作的效率。這種同步方式就是條件變數。

坦率的說,從字面意思上來將,“條件變數”這四個字是不太容易理解的。我們可以把“條件變數”看做是一個物件,一個鈴鐺,一個會響的鈴鐺。當一個執行緒在獲得互斥鎖之後,由於被鎖定的變數不滿足繼續執行的條件時,該執行緒就釋放互斥鎖並把自己掛到這個“鈴鐺”上。其它的執行緒在修改完變數後,它就搖搖“鈴鐺”,告訴那些掛著的執行緒:“你們等待的東西已經變化了,都醒醒看看現在的它是否滿足你們的要求。”於是那些掛著的執行緒就知道自己醒來看自己是否能繼續跑下去了。

>>>讀寫鎖
互斥鎖是排他性鎖,條件變量出現後和互斥鎖配合工作能夠有效的節省系統資源並提高執行緒之間的協同工作效率。互斥鎖的目的是為了獨佔,條件變數的目的是為了等待和通知。但是現實世界是很複雜di,我們要解決的問題也是多種多樣di.從功能上來說,互斥鎖和條件變數能夠解決基本上所有的問題,但是效能上就不一定完全滿足了。人的無休止的慾望促使人發明出針對性更強、效能更好的同步機制來。讀寫鎖就是這麼一個玩意兒。
考慮一個檔案有多個程序要讀取其中的內容,但只有1個程序有寫的需求。我們知道讀檔案的內容不會改變檔案的內容,這樣即使多個程序同時讀相同的檔案也沒什麼問題,大家都能和諧共存。當寫程序需要寫資料時,為了保證資料的一致性,所有讀的程序就都不能讀資料了,否則很可能出現讀出去的資料一半是舊的,一半是新的狀況,邏輯就亂掉了。
為了防止讀資料的時候被寫入新的資料,讀程序必須對檔案加上鎖。現在假如我們有2個程序都同時讀,如果我們使用上面的互斥鎖和條件變數,當其中一個程序在讀取資料的時候,另一個程序只能等待,因為它得不到鎖。從效能上考慮,等待程序所花費的時間是完全的浪費,因為這個程序完全可以讀檔案內容而不會影響第一個,但是這個程序沒有鎖,所以它什麼也做不了,只能等,等到花兒都謝了。
所以呢,我們需要一種其它型別的同步方式來滿足上面的需求,這就是讀寫鎖。
讀寫鎖的出現能夠有效的解決多程序並行讀的問題。每一個需要讀取的程序都申請讀鎖,這樣大家互不干擾。當有程序需要寫如資料時,首先申請寫鎖。如果在申請時發現有讀(或者寫)鎖存在,則該寫程序必須等待,一直等到所有的讀(寫)鎖完全釋放為止。讀程序在讀取之前首先申請讀鎖,如果所讀資料被寫鎖鎖定,則該讀程序也必須等待讀鎖被釋放位置。

很自然的,多個讀鎖是可以共存的,但寫鎖是完全互相排斥的。

>>>記錄鎖(檔案鎖)
為了增加並行性,我們可以在讀寫鎖的基礎上進一步細分被鎖物件的粒度。比如一個檔案中,讀程序可能需要讀取該檔案的前1k個位元組,寫程序需要寫該檔案的最後1k個位元組。我們可以對前1k個位元組上讀鎖,對最後1k個自己上寫鎖,這樣兩個程序就可併發工作了。記錄鎖中的所謂“記錄”其實是“內容”的概念。使用讀寫鎖可以鎖定一部分,而不是整個檔案。

檔案鎖可以認為是記錄鎖的一個特例,當使用記錄鎖鎖定檔案的所有內容時,此時的記錄鎖就可以稱為檔案鎖了。

>>>訊號燈
訊號燈可以說是條件變數的升級版。條件變數相當於鈴鐺,鈴鐺響後每個掛起的程序還需要自己獲得互斥鎖並判斷所需條件是否滿足,訊號燈把這兩步操作糅合到一起。
在Posix.1基本原理一文聲稱,有了互斥鎖和條件變數還提供訊號燈的原因是:“本標準提供訊號燈的而主要目的是提供一種程序間同步的方式;這些程序可能共享也可能不共享記憶體區。互斥鎖和條件變數是作為執行緒間的同步機制說明的;這些執行緒總是共享(某個)記憶體區。這兩者都是已廣泛使用了多年的同步方式。每組原語都特別適合於特定的問題”。儘管訊號燈的意圖在於程序間同步,互斥鎖和條件變數的意圖在於執行緒間同步,但是訊號燈也可用於執行緒間,互斥鎖和條件變數也可用於程序見。應當根據實際的情況進行決定。

訊號燈最有用的場景是用以指明可用資源的數量。比如含有10個元素的陣列,我們可以建立一個訊號燈,初始值為0.每當有程序需要讀陣列中元素時(假設每次僅能讀取1個元素),就申請使用該訊號燈(訊號燈的值減1),當有程序需要寫元素時,就申請掛出該訊號等(訊號燈值加1)。這樣訊號燈起到了可用資源數量的作用。如果我們限定訊號燈的值只能取0和1,就和互斥鎖的含義很相同了。