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

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

但是,處於安全性的考慮,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,就和互斥鎖的含義很相同了。


聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.