天天看點

程序通信機制總結

文章目錄

  • ​​一、什麼是程序通信​​
  • ​​二、管道​​
  • ​​1. 匿名管道​​
  • ​​2. 有名管道​​
  • ​​三、消息隊列​​
  • ​​四、共享記憶體​​
  • ​​五、信号量和 PV 操作​​
  • ​​六、信号​​
  • ​​七、socket​​
  • ​​八、總結​​
程式通信機制總結

一、什麼是程序通信

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

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

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

程式通信機制總結

二、管道

1. 匿名管道

各位如果學過 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 隻能用于父程序讀、子程序寫。管道是用 「環形隊列」 實作的,資料從寫端流入從讀端流出,這就實作了父子程序之間的雙向通信

程式通信機制總結

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

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

2. 有名管道

匿名管道由于沒有名字,隻能用于父子程序間的通信。為了克服這個缺點,提出了有名管道,也稱做​

​FIFO​

​,因為資料是先進先出的傳輸方式。

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

使用 Linux 指令 ​

​mkfifo​

​ 來建立有名管道:

$ mkfifo mypipe      

​myPipe​

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

$ echo "hello" > mypipe      

執行這行指令後,你會發現它就阻塞在這了,這是因為管道裡的内容沒有被讀取,隻有當管道裡的資料被讀完後,指令才可以正常退出

程式通信機制總結

于是,我們執行另外一個指令來讀取這個有名管道裡的資料:

程式通信機制總結

三、消息隊列

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

程式通信機制總結

「消息隊列的本質就是存放在記憶體中的消息的連結清單,而消息本質上是使用者自定義的資料結構」。如果程序從消息隊列中讀取了某個消息,這個消息就會被從消息隊列中删除。對比一下管道機制:

  • 消息隊列允許一個或多個程序向它寫入或讀取消息。
  • 消息隊列可以實作消息的 「随機查詢」,不一定非要以先進先出的次序讀取消息,也可以按消息的類型讀取。比有名管道的先進先出原則更有優勢。
  • 對于消息隊列來說,在某個程序往一個隊列寫入消息之前,并不需要另一個程序在該消息隊列上等待消息的到達。而對于管道來說,除非讀程序已存在,否則先有寫程序進行寫入操作是沒有意義的。
  • 消息隊列的生命周期随核心,如果沒有釋放消息隊列或者沒有關閉作業系統,消息隊列就會一直存在。而匿名管道(父子程序之間使用)随程序的建立而建立,随程序的結束而銷毀。

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

四、共享記憶體

為了避免像消息隊列那樣頻繁的拷貝消息、進行系統調用,共享記憶體機制出現了。

顧名思義,共享記憶體就是允許不相幹的程序将同一段實體記憶體連結到它們各自的位址空間中,使得這些程序可以通路同一個實體記憶體,這個實體記憶體就稱為共享記憶體。如果某個程序向共享記憶體寫入資料,所做的改動将 「立即」 影響到可以通路同一段共享記憶體的任何其他程序。

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

程式通信機制總結

不同于消息隊列頻繁的系統調用,對于共享記憶體機制來說,僅在建立共享記憶體區域時需要系統調用,一旦建立共享記憶體,所有的通路都可作為正常記憶體通路,無需借助核心。這樣,資料就不需要在程序之間切換CPU狀态來回拷貝,是以這是最快的一種程序通信方式

程式通信機制總結

五、信号量和 PV 操作

實際上,對具有多 CPU 系統的最新研究表明,在這類系統上,消息傳遞的性能其實是要優于共享記憶體的,因為**「消息隊列無需避免沖突,而共享記憶體機制可能會發生沖突」** 。也就是說如果多個程序同時修改同一個共享記憶體,先來的那個程序寫的内容就會被後來的覆寫。

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

舉個例子,如果有 A、B 兩個程序分别負責讀和寫資料的操作,這兩個線程是互相合作、互相依賴的。那麼寫資料應該發生在讀資料之前。而實際上,由于異步性的存在,可能會發生先讀後寫的情況,而此時由于緩沖區還沒有被寫入資料,讀程序 A 沒有資料可讀,是以讀程序 A 被阻塞

程式通信機制總結

是以,為了解決上述這兩個問題,保證共享記憶體在任何時刻隻有一個程序在通路(互斥),并且使得程序們能夠按照某個特定順序通路共享記憶體(同步),我們就可以使用程序的同步與互斥機制,常見的比如信号量與 PV 操作

「程序的同步與互斥其實是一種對程序通信的保護機制,并不是用來傳輸程序之間真正通信的内容的,但是由于它們會傳輸信号量,是以也被納入程序通信的範疇,稱為低級通信」

信号量其實就是一個變量 ,我們可以用一個信号量來表示系統中某種資源的數量,比如:系統中隻有一台列印機,就可以設定一個初值為 1 的信号量

使用者程序可以通過使用作業系統提供的一對原語來對信号量進行操作,進而很友善的實作程序互斥或同步。這一對原語就是 PV 操作:

1)「P 操作」:将信号量值減 1,表示 「申請占用一個資源」。如果結果小于 0,表示已經沒有可用資源,則執行 P 操作的程序被阻塞。如果結果大于等于 0,表示現有的資源足夠你使用,則執行 P 操作的程序繼續執行。

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

2)「V 操作」:将信号量值加 1,表示 「釋放一個資源」,即使用完資源後歸還資源。若加完後信号量的值小于等于 0,表示有某些程序正在等待該資源,由于我們已經釋放出一個資源了,是以需要喚醒一個等待使用該資源(就緒态)的程序,使之運作下去。

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

問:「信号量的值 大于 0 表示有共享資源可供使用,這個時候為什麼不需要喚醒程序」?

答:所謂喚醒程序是從就緒隊列(阻塞隊列)中喚醒程序,而信号量的值大于 0 表示有共享資源可供使用,也就是說這個時候沒有程序被阻塞在這個資源上,是以不需要喚醒,正常運作即可。

問:「信号量的值 等于 0 的時候表示沒有共享資源可供使用,為什麼還要喚醒程序」?

答:信号量的值為0并不代表沒有資源可以使用,比如一個程序A進行V操作前,信号量的值為-1,表示有1個程序在等待這個資源。此時程序A釋放資源,對信号量進行V操作,值變為0,此時需要喚醒等待資源的程序起來使用資源

信号量和 PV 操作具體的定義如下:

程式通信機制總結

互斥通路共享記憶體

兩步走即可實作不同程序對共享記憶體的互斥通路:

定義一個互斥信号量,并初始化為 1,把對共享記憶體的通路置于 P 操作和 V 操作之間

程式通信機制總結

「P 操作和 V 操作必須成對出現」,缺少 P 操作就不能保證對共享記憶體的互斥通路,缺少 V 操作就會導緻共享記憶體永遠得不到釋放、處于等待态的程序永遠得不到喚醒

程式通信機制總結

實作程序同步

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

程式通信機制總結

定義一個同步信号量,并初始化為目前可用資源的數量

在優先級較「高」的操作的「後」面執行 V 操作,釋放資源

在優先級較「低」的操作的「前」面執行 P 操作,申請占用資源

程式通信機制總結

配合下面這張圖直覺了解下:

程式通信機制總結

六、信号

注意!「信号和信号量是完全不同的兩個概念」!

信号是程序通信機制中唯一的 「異步」 通信機制,它可以在任何時候發送信号給某個程序。「通過發送指定信号來通知程序某個異步事件的發生,以迫使程序執行信号處理程式。信号處理完畢後,被中斷程序将恢複執行」。使用者、核心程序都能生成和發送信号

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

shen@ubuntu-vm:~$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX      

七、socket

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

程式通信機制總結

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

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

Socket 的本質其實是一個程式設計接口(API),是應用層與 TCP/IP 協定族通信的中間軟體抽象層,它對 TCP/IP 進行了封裝。它「把複雜的 TCP/IP 協定族隐藏在 Socket 接口後面」。對使用者來說,隻要通過一組簡單的 API 就可以實作網絡的連接配接。

程式通信機制總結

八、總結

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

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

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

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

對于共享記憶體機制來說,僅在建立共享記憶體區域時需要系統調用,一旦建立共享記憶體,所有的通路都可作為正常記憶體通路,無需借助核心。這樣,資料就不需要在程序之間來回拷貝,是以這是最快的一種程序通信方式。

4)共享記憶體速度雖然非常快,但是存在沖突問題,為此,我們可以使用信号量和 PV 操作來實作對共享記憶體的互斥通路,并且還可以實作程序同步。

繼續閱讀