高頻考點,六大程式通訊機制總結

飛天小牛肉發表於2021-02-26

 

? 盡人事,聽天命。博主東南大學碩士在讀,熱愛健身和籃球,樂於分享技術相關的所見所得,關注公眾號 @ 飛天小牛肉,第一時間獲取文章更新,成長的路上我們一起進步

? 本文已收錄於 「CS-Wiki」Gitee 官方推薦專案,現已累計 1.4k+ star,致力打造完善的後端知識體系,在技術的路上少走彎路,歡迎各位小夥伴前來交流學習

? 如果各位小夥伴春招秋招沒有拿得出手的專案的話,可以參考我寫的一個專案「開源社群系統 Echo」Gitee 官方推薦專案,目前已累計 250+ star,基於 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 並提供詳細的開發文件和配套教程。公眾號後臺回覆 Echo 可以獲取配套教程,目前尚在更新中

 

初學作業系統的時候,我就一直懵逼,為啥程式同步與互斥機制裡有訊號量機制,程式通訊裡又有訊號量機制,然後你再看網路上的各種面試題彙總或者部落格,你會發現很多都是千篇一律的程式通訊機制有哪些?程式同步與互斥機制鮮有人問津。看多了我都想把 CSDN 屏了.....,最後知道真相的我只想說為啥不能一篇部落格把東西寫清楚,沒頭沒尾真的浪費時間。

希望這篇文章能夠拯救某段時間和我一樣被繞暈的小夥伴。上篇文章我已經講過程式間的同步與互斥機制,各位小夥伴看完這個再來看程式通訊比較好。

全文脈絡思維導圖如下:

高頻考點,六大程式通訊機制總結

1. 什麼是程式通訊

顧名思義,程式通訊( InterProcess Communication,IPC)就是指程式之間的資訊交換。實際上,程式的同步與互斥本質上也是一種程式通訊(這也就是待會我們會在程式通訊機制中看見訊號量和 PV 操作的原因了),只不過它傳輸的僅僅是訊號量,通過修改訊號量,使得程式之間建立聯絡,相互協調和協同工作,但是它缺乏傳遞資料的能力

雖然存在某些情況,程式之間交換的資訊量很少,比如僅僅交換某個狀態資訊,這樣程式的同步與互斥機制完全可以勝任這項工作。但是大多數情況下,程式之間需要交換大批資料,比如傳送一批資訊或整個檔案,這就需要通過一種新的通訊機制來完成,也就是所謂的程式通訊。

再來從作業系統層面直觀的看一些程式通訊:我們知道,為了保證安全,每個程式的使用者地址空間都是獨立的,一般而言一個程式不能直接訪問另一個程式的地址空間,不過核心空間是每個程式都共享的,所以程式之間想要進行資訊交換就必須通過核心

高頻考點,六大程式通訊機制總結

下面就來我們來列舉一下 Linux 核心提供的常見的程式通訊機制:

  • 管道(也稱作共享檔案)

  • 訊息佇列(也稱作訊息傳遞)

  • 共享記憶體(也稱作共享儲存)

  • 訊號量和 PV 操作

  • 訊號

  • 套接字(Socket)

2. 管道

匿名管道

各位如果學過 Linux 命令,那對管道肯定不陌生,Linux 管道使用豎線 | 連線多個命令,這被稱為管道符。

$ command1 | command2

以上這行程式碼就組成了一個管道,它的功能是將前一個命令(command1)的輸出,作為後一個命令(command2)的輸入,從這個功能描述中,我們可以看出管道中的資料只能單向流動,也就是半雙工通訊,如果想實現相互通訊(全雙工通訊),我們需要建立兩個管道才行。

另外,通過管道符 | 建立的管道是匿名管道,用完了就會被自動銷燬。並且,匿名管道只能在具有親緣關係(父子程式)的程式間使用,。也就是說,匿名管道只能用於父子程式之間的通訊

在 Linux 的實際編碼中,是通過 pipe 函式來建立匿名管道的,若建立成功則返回 0,建立失敗就返回 -1:

int pipe (int fd[2]);

該函式擁有一個儲存空間為 2 的檔案描述符陣列:

  • fd[0] 指向管道的讀端,fd[1] 指向管道的寫端

  • fd[1] 的輸出是 fd[0] 的輸入

粗略的解釋一下通過匿名管道實現程式間通訊的步驟:

1)父程式建立兩個匿名管道,管道 1(fd1[0]fd1[1])和管道 2(fd2[0]fd2[1]);

因為管道的資料是單向流動的,所以要想實現資料雙向通訊,就需要兩個管道,每個方向一個。

2)父程式 fork 出子程式,於是對於這兩個匿名管道,子程式也分別有兩個檔案描述符指向匿名管道的讀寫兩端;

3)父程式關閉管道 1 的讀端 fd1[0] 和 管道 2 的寫端 fd2[1],子程式關閉管道 1 的寫端 fd1[1] 和 管道 2 的讀端 fd2[0],這樣,管道 1 只能用於父程式寫、子程式讀;管道 2 只能用於父程式讀、子程式寫。管道是用環形佇列實現的,資料從寫端流入從讀端流出,這就實現了父子程式之間的雙向通訊。

高頻考點,六大程式通訊機制總結

看完上面這些講述,我們來理解下管道的本質是什麼:對於管道兩端的程式而言,管道就是一個檔案(這也就是為啥管道也被稱為共享檔案機制的原因了),但它不是普通的檔案,它不屬於某種檔案系統,而是自立門戶,單獨構成一種檔案系統,並且只存在於記憶體中。

簡單來說,管道的本質就是核心在記憶體中開闢了一個緩衝區,這個緩衝區與管道檔案相關聯,對管道檔案的操作,被核心轉換成對這塊緩衝區的操作

有名管道

匿名管道由於沒有名字,只能用於父子程式間的通訊。為了克服這個缺點,提出了有名管道,也稱做 FIFO,因為資料是先進先出的傳輸方式。

所謂有名管道也就是提供一個路徑名與之關聯,這樣,即使與建立有名管道的程式不存在親緣關係的程式,只要可以訪問該路徑,就能夠通過這個有名管道進行相互通訊。

使用 Linux 命令 mkfifo 來建立有名管道:

$ mkfifo myPipe

myPipe 就是這個管道的名稱,接下來,我們往 myPipe 這個有名管道中寫入資料:

$ echo "hello" > myPipe

執行這行命令後,你會發現它就停在這了,這是因為管道里的內容沒有被讀取,只有當管道里的資料被讀完後,命令才可以正常退出。於是,我們執行另外一個命令來讀取這個有名管道里的資料:

$ cat < myPipe
hello

3. 訊息佇列

可以看出,管道這種程式通訊方式雖然使用簡單,但是效率比較低,不適合程式間頻繁地交換資料,並且管道只能傳輸無格式的位元組流。為此,訊息傳遞機制(Linux 中稱訊息佇列)應用而生。比如,A 程式要給 B 程式傳送訊息,A 程式把資料放在對應的訊息佇列後就可以正常返回了,B 程式在需要的時候自行去訊息佇列中讀取資料就可以了。同樣的,B 程式要給 A 程式傳送訊息也是如此。

高頻考點,六大程式通訊機制總結

訊息佇列的本質就是存放在記憶體中的訊息的連結串列,而訊息本質上是使用者自定義的資料結構。如果程式從訊息佇列中讀取了某個訊息,這個訊息就會被從訊息佇列中刪除。對比一下管道機制:

  • 訊息佇列允許一個或多個程式向它寫入或讀取訊息。

  • 訊息佇列可以實現訊息的隨機查詢,不一定非要以先進先出的次序讀取訊息,也可以按訊息的型別讀取。比有名管道的先進先出原則更有優勢。

  • 對於訊息佇列來說,在某個程式往一個佇列寫入訊息之前,並不需要另一個程式在該訊息佇列上等待訊息的到達。而對於管道來說,除非讀程式已存在,否則先有寫程式進行寫入操作是沒有意義的。

  • 訊息佇列的生命週期隨核心,如果沒有釋放訊息佇列或者沒有關閉作業系統,訊息佇列就會一直存在。而匿名管道隨程式的建立而建立,隨程式的結束而銷燬。

需要注意的是,訊息佇列對於交換較少數量的資料很有用,因為無需避免衝突。但是,由於使用者程式寫入資料到記憶體中的訊息佇列時,會發生從使用者態拷貝資料到核心態的過程;同樣的,另一個使用者程式讀取記憶體中的訊息資料時,會發生從核心態拷貝資料到使用者態的過程。因此,如果資料量較大,使用訊息佇列就會造成頻繁的系統呼叫,也就是需要消耗更多的時間以便核心介入

4. 共享記憶體

為了避免像訊息佇列那樣頻繁的拷貝訊息、進行系統呼叫,共享記憶體機制出現了。

顧名思義,共享記憶體就是允許不相干的程式將同一段實體記憶體連線到它們各自的地址空間中,使得這些程式可以訪問同一個實體記憶體,這個實體記憶體就成為共享記憶體。如果某個程式向共享記憶體寫入資料,所做的改動將立即影響到可以訪問同一段共享記憶體的任何其他程式。

集合記憶體管理的內容,我們來深入理解下共享記憶體的原理。首先,每個程式都有屬於自己的程式控制塊(PCB)和邏輯地址空間(Addr Space),並且都有一個與之對應的頁表,負責將程式的邏輯地址(虛擬地址)與實體地址進行對映,通過記憶體管理單元(MMU)進行管理。兩個不同程式的邏輯地址通過頁表對映到物理空間的同一區域,它們所共同指向的這塊區域就是共享記憶體

高頻考點,六大程式通訊機制總結

不同於訊息佇列頻繁的系統呼叫,對於共享記憶體機制來說,僅在建立共享記憶體區域時需要系統呼叫,一旦建立共享記憶體,所有的訪問都可作為常規記憶體訪問,無需藉助核心。這樣,資料就不需要在程式之間來回拷貝,所以這是最快的一種程式通訊方式。

高頻考點,六大程式通訊機制總結

5. 訊號量和 PV 操作

實際上,對具有多 CPU 系統的最新研究表明,在這類系統上,訊息傳遞的效能其實是要優於共享記憶體的,因為訊息佇列無需避免衝突,而共享記憶體機制可能會發生衝突。也就是說如果多個程式同時修改同一個共享記憶體,先來的那個程式寫的內容就會被後來的覆蓋。

並且,在多道批處理系統中,多個程式是可以併發執行的,但由於系統的資源有限,程式的執行不是一貫到底的, 而是走走停停,以不可預知的速度向前推進(非同步性)。但有時候我們又希望多個程式能密切合作,按照某個特定的順序依次執行,以實現一個共同的任務。

舉個例子,如果有 A、B 兩個程式分別負責讀和寫資料的操作,這兩個執行緒是相互合作、相互依賴的。那麼寫資料應該發生在讀資料之前。而實際上,由於非同步性的存在,可能會發生先讀後寫的情況,而此時由於緩衝區還沒有被寫入資料,讀程式 A 沒有資料可讀,因此讀程式 A 被阻塞。

高頻考點,六大程式通訊機制總結

因此,為了解決上述這兩個問題,保證共享記憶體在任何時刻只有一個程式在訪問(互斥),並且使得程式們能夠按照某個特定順序訪問共享記憶體(同步),我們就可以使用程式的同步與互斥機制,常見的比如訊號量與 PV 操作。

程式的同步與互斥其實是一種對程式通訊的保護機制,並不是用來傳輸程式之間真正通訊的內容的,但是由於它們會傳輸訊號量,所以也被納入程式通訊的範疇,稱為低階通訊

下面的內容和上篇文章【看完了程式同步與互斥機制,我終於徹底理解了 PV 操作】中所講的差不多,看過的小夥伴可直接跳到下一標題。

訊號量其實就是一個變數 ,我們可以用一個訊號量來表示系統中某種資源的數量,比如:系統中只有一臺印表機,就可以設定一個初值為 1 的訊號量。

使用者程式可以通過使用作業系統提供的一對原語來對訊號量進行操作,從而很方便的實現程式互斥或同步。這一對原語就是 PV 操作:

1)P 操作:將訊號量值減 1,表示申請佔用一個資源。如果結果小於 0,表示已經沒有可用資源,則執行 P 操作的程式被阻塞。如果結果大於等於 0,表示現有的資源足夠你使用,則執行 P 操作的程式繼續執行。

可以這麼理解,當訊號量的值為 2 的時候,表示有 2 個資源可以使用,當訊號量的值為 -2 的時候,表示有兩個程式正在等待使用這個資源。不看這句話真的無法理解 V 操作,看完頓時如夢初醒。

2)V 操作:將訊號量值加 1,表示釋放一個資源,即使用完資源後歸還資源。若加完後訊號量的值小於等於 0,表示有某些程式正在等待該資源,由於我們已經釋放出一個資源了,因此需要喚醒一個等待使用該資源(就緒態)的程式,使之執行下去。

我覺得已經講的足夠通俗了,不過對於 V 操作大家可能仍然有困惑,下面再來看兩個關於 V 操作的問答:

問:訊號量的值 大於 0 表示有共享資源可供使用,這個時候為什麼不需要喚醒程式

答:所謂喚醒程式是從就緒佇列(阻塞佇列)中喚醒程式,而訊號量的值大於 0 表示有共享資源可供使用,也就是說這個時候沒有程式被阻塞在這個資源上,所以不需要喚醒,正常執行即可。

問:訊號量的值 等於 0 的時候表示沒有共享資源可供使用,為什麼還要喚醒程式

答:V 操作是先執行訊號量值加 1 的,也就是說,把訊號量的值加 1 後才變成了 0,在此之前,訊號量的值是 -1,即有一個程式正在等待這個共享資源,我們需要喚醒它。

訊號量和 PV 操作具體的定義如下:

高頻考點,六大程式通訊機制總結

互斥訪問共享記憶體

兩步走即可實現不同程式對共享記憶體的互斥訪問:

  • 定義一個互斥訊號量,並初始化為 1

  • 把對共享記憶體的訪問置於 P 操作和 V 操作之間

高頻考點,六大程式通訊機制總結

P 操作和 V 操作必須成對出現。缺少 P 操作就不能保證對共享記憶體的互斥訪問,缺少 V 操作就會導致共享記憶體永遠得不到釋放、處於等待態的程式永遠得不到喚醒。

高頻考點,六大程式通訊機制總結

實現程式同步

回顧一下程式同步,就是要各併發程式按要求有序地執行。

舉個例子,以下兩個程式 P1、P2 併發執行,由於存在非同步性,因此二者交替推進的次序是不確定的。假設 P2 的 “程式碼4” 要基於 P1 的 “程式碼1” 和 “程式碼2” 的執行結果才能執行,那麼我們就必須保證 “程式碼4” 一定是在 “程式碼2” 之後才會執行。

高頻考點,六大程式通訊機制總結

如果 P2 的 “程式碼4” 要基於 P1 的 “程式碼1” 和 “程式碼2” 的執行結果才能執行,那麼我們就必須保證 “程式碼4” 一定是在 “程式碼2” 之後才會執行。

使用訊號量和 PV 操作實現程式的同步也非常方便,三步走:

  • 定義一個同步訊號量,並初始化為當前可用資源的數量

  • 在優先順序較的操作的面執行 V 操作,釋放資源

  • 在優先順序較的操作的面執行 P 操作,申請佔用資源

高頻考點,六大程式通訊機制總結

配合下面這張圖直觀理解下:

高頻考點,六大程式通訊機制總結

6. 訊號

注意!訊號和訊號量是完全不同的兩個概念

訊號是程式通訊機制中唯一的非同步通訊機制,它可以在任何時候傳送訊號給某個程式。通過傳送指定訊號來通知程式某個非同步事件的傳送,以迫使程式執行訊號處理程式。訊號處理完畢後,被中斷程式將恢復執行。使用者、核心和程式都能生成和傳送訊號。

訊號事件的來源主要有硬體來源和軟體來源。所謂硬體來源就是說我們可以通過鍵盤輸入某些組合鍵給程式傳送訊號,比如常見的組合鍵 Ctrl+C 產生 SIGINT 訊號,表示終止該程式;而軟體來源就是通過 kill 系列的命令給程式傳送訊號,比如 kill -9 1111 ,表示給 PID 為 1111 的程式傳送 SIGKILL 訊號,讓其立即結束。我們來檢視一下 Linux 中有哪些訊號:

高頻考點,六大程式通訊機制總結

7. Socket

至此,上面介紹的 5 種方法都是用於同一臺主機上的程式之間進行通訊的,如果想要跨網路與不同主機上的程式進行通訊,那該怎麼做呢?這就是 Socket 通訊做的事情了(當然,Socket 也能完成同主機上的程式通訊)。

高頻考點,六大程式通訊機制總結

Socket 起源於 Unix,原意是插座,在計算機通訊領域,Socket 被翻譯為套接字,它是計算機之間進行通訊的一種約定或一種方式。通過 Socket 這種約定,一臺計算機可以接收其他計算機的資料,也可以向其他計算機傳送資料。

從計算機網路層面來說,Socket 套接字是網路通訊的基石,是支援 TCP/IP 協議的網路通訊的基本操作單元。它是網路通訊過程中端點的抽象表示,包含進行網路通訊必須的五種資訊:連線使用的協議,本地主機的 IP 地址,本地程式的協議埠,遠地主機的 IP 地址,遠地程式的協議埠

Socket 的本質其實是一個程式設計介面(API),是應用層與 TCP/IP 協議族通訊的中間軟體抽象層,它對 TCP/IP 進行了封裝。它把複雜的 TCP/IP 協議族隱藏在 Socket 介面後面。對使用者來說,只要通過一組簡單的 API 就可以實現網路的連線。

高頻考點,六大程式通訊機制總結

8. 總結

簡單總結一下上面六種 Linux 核心提供的程式通訊機制:

1)首先,最簡單的方式就是管道,管道的本質是存放在記憶體中的特殊的檔案。也就是說,核心在記憶體中開闢了一個緩衝區,這個緩衝區與管道檔案相關聯,對管道檔案的操作,被核心轉換成對這塊緩衝區的操作。管道分為匿名管道和有名管道,匿名管道只能在父子程式之間進行通訊,而有名管道沒有限制。

2)雖然管道使用簡單,但是效率比較低,不適合程式間頻繁地交換資料,並且管道只能傳輸無格式的位元組流。為此訊息佇列應用而生。訊息佇列的本質就是存放在記憶體中的訊息的連結串列,而訊息本質上是使用者自定義的資料結構。如果程式從訊息佇列中讀取了某個訊息,這個訊息就會被從訊息佇列中刪除。

3)訊息佇列的速度比較慢,因為每次資料的寫入和讀取都需要經過使用者態與核心態之間資料的拷貝過程,共享記憶體可以解決這個問題。所謂共享記憶體就是:兩個不同程式的邏輯地址通過頁表對映到物理空間的同一區域,它們所共同指向的這塊區域就是共享記憶體。如果某個程式向共享記憶體寫入資料,所做的改動將立即影響到可以訪問同一段共享記憶體的任何其他程式。

對於共享記憶體機制來說,僅在建立共享記憶體區域時需要系統呼叫,一旦建立共享記憶體,所有的訪問都可作為常規記憶體訪問,無需藉助核心。這樣,資料就不需要在程式之間來回拷貝,所以這是最快的一種程式通訊方式。

4)共享記憶體速度雖然非常快,但是存在衝突問題,為此,我們可以使用訊號量和 PV 操作來實現對共享記憶體的互斥訪問,並且還可以實現程式同步。

5)訊號和訊號量是完全不同的兩個概念!訊號是程式通訊機制中唯一的非同步通訊機制,它可以在任何時候傳送訊號給某個程式。通過傳送指定訊號來通知程式某個非同步事件的傳送,以迫使程式執行訊號處理程式。訊號處理完畢後,被中斷程式將恢復執行。使用者、核心和程式都能生成和傳送訊號。

6)上面介紹的 5 種方法都是用於同一臺主機上的程式之間進行通訊的,如果想要跨網路與不同主機上的程式進行通訊,就需要使用 Socket 通訊。另外,Socket 也能完成同主機上的程式通訊。

總結完畢!

 

? 關注公眾號 | 飛天小牛肉,即時獲取更新

  • 博主東南大學碩士在讀,利用課餘時間運營一個公眾號『 飛天小牛肉 』,2020/12/29 日開通,專注分享計算機基礎(資料結構 + 演算法 + 計算機網路 + 資料庫 + 作業系統 + Linux)、Java 基礎和麵試指南的相關原創技術好文。本公眾號的目的就是讓大家可以快速掌握重點知識,有的放矢。希望大家多多支援哦,和小牛肉一起成長 ?

  • 並推薦個人維護的開源教程類專案: CS-Wiki(Gitee 推薦專案,現已累計 1.4k+ star), 致力打造完善的後端知識體系,在技術的路上少走彎路,歡迎各位小夥伴前來交流學習 ~ ?

  • 如果各位小夥伴春招秋招沒有拿得出手的專案的話,可以參考我寫的一個專案「開源社群系統 Echo」Gitee 官方推薦專案,目前已累計 250+ star,基於 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 並提供詳細的開發文件和配套教程。公眾號後臺回覆 Echo 可以獲取配套教程,目前尚在更新中。

相關文章