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

初學操做系統的時候,我就一直懵逼,爲啥進程同步與互斥機制裏有信號量機制,進程通訊裏又有信號量機制,而後你再看網絡上的各類面試題彙總或者博客,你會發現不少都是千篇一概的進程通訊機制有哪些?進程同步與互斥機制鮮有人問津。看多了我都想把 CSDN 屏了.....,最後知道真相的我只想說爲啥不能一篇博客把東西寫清楚,沒頭沒尾真的浪費時間。git

但願這篇文章可以拯救某段時間和我同樣被繞暈的小夥伴。上篇文章我已經講過進程間的同步與互斥機制,各位小夥伴看完這個再來看進程通訊比較好。web

全文脈絡思惟導圖以下:面試

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 操做就會致使共享內存永遠得不到釋放、處於等待態的進程永遠得不到喚醒。

實現進程同步

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

舉個例子,如下兩個進程 P一、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 也能完成同主機上的進程通訊。

總結完畢!



😁 點擊下方卡片關注公衆號「飛天小牛肉」(專一於分享計算機基礎、Java 基礎和麪試指南的相關原創技術好文,幫助讀者快速掌握高頻重點知識,有的放矢),與小牛肉一塊兒成長、共同進步 

🎉 並向你們強烈推薦我維護的 Gitee 倉庫 「CS-Wiki」(Gitee 推薦項目,目前已 1.0k+ star。致力打造完善的後端知識體系,在技術的路上少走彎路。相比公衆號,該倉庫擁有更健全的知識體系,歡迎給位小夥伴前來交流學習,倉庫地址 https://gitee.com/veal98/CS-Wiki。也可直接下方掃碼訪問


原創不易,讀完有收穫不妨點贊|分享|在看支持

本文分享自微信公衆號 - 飛天小牛肉(CS-Wiki)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索