1. 程式人生 > 其它 >程序間通訊的方式及應用場景

程序間通訊的方式及應用場景

開頭

  每個程序的使用者地址空間都是獨立的,程序與程序之間,內部空間是隔離的,程序 A 不可能直接使用程序 B 的變數名的形式得到程序 B 中變數的值。但核心空間是每個程序都共享的,所以程序之間要通訊必須通過核心。實現程序與程序之間的通訊,常用的方式主要有:管道、訊息佇列、共享記憶體、訊號量、訊號、socket等等。

一、管道

  在 Linux 命令中,常見的“|”符號就是一種管道。比如:

 ps auxf | grep mysql

  上面的命令中,“|”的功能是將前一個命令(ps auxf)的輸出,作為後一個命令(grep mysql)的輸入。這種管道沒有名字,匿名管道,用完就銷燬。命名管道

也被叫做 FIFO,因為資料的傳輸方式是先進先出(first in first out)。

  管道傳輸資料是單向的,如果想相互通訊,需要建立兩個管道才行。

管道建立、寫入、讀取

建立

mkfifo myPipe

  myPipe 是新建立的管道的名稱,基於 Linux 一切皆檔案的理念,管道也是以檔案的方式存在,可以用 ls 看到檔案型別是 p,也就是 pipe(管道) 的意思:

$ ls -l
prw-r--r--. 1 root    root         0 Jul 17 02:45 myPipe
echo "hello" > myPipe  # 將資料寫進管道。程式會阻塞,只有當管道里的資料被讀完後,程式才會正常繼續。
管道寫入資料
cat < myPipe  # 讀取管道里的資料
# hello
管道讀取資料

管道的優缺點

  缺點:管道的通訊方式效率低,不適合程序間頻繁地交換資料。

  優點:簡單。

二、訊息佇列

  前面說到管道的通訊方式效率很低,因此管道不適合程序間頻繁地交換資料。

  對於這個問題,訊息佇列可以解決。比如,A 程序要給 B 程序傳送訊息,A 程序將資料存入訊息佇列,B 程序只需要讀取資料即可。反之亦如此。

  訊息佇列的本質是儲存在核心中的一種訊息連結串列,在傳送資料時,會分成獨立的資料單元,也就是訊息體(資料塊)。訊息體是使用者自定義的資料型別,訊息的傳送方和接收方必須約定好訊息體的資料型別,所以每個訊息體都是固定大小的儲存塊,不像管道是無格式的位元組流資料。如果程序從訊息佇列中讀取了訊息體,核心就會把這個訊息體刪除。

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

  訊息這種模型,兩個程序之間的通訊就像平時發郵件一樣,你來一封,我回一封,可以頻繁溝通。但郵件的通訊方式存在不足的地方有兩點:一是通訊不及時,二是附件也有大小限制,這同樣也是訊息佇列通訊不足的點。

訊息佇列的優缺點

  缺點:

  1. 通訊不及時
  2. 不適合比較大資料的傳輸,因為在核心中每個訊息體都有一個最大長度的限制,同時所有佇列所包含的全部訊息體的總長度也是有上限。在 Linux 核心中,會有兩個巨集定義 MSGMAX 和 MSGMNB,它們以位元組為單位,分別定義了一條訊息的最大長度和一個佇列的最大長度。
  3. 訊息佇列通訊過程中,存在使用者態與核心態之間的資料拷貝開銷,因為程序寫入資料到核心中的訊息佇列時,會發生從使用者態拷貝資料到核心態的過程,同理另一程序讀取核心中的訊息資料時,會發生從核心態拷貝資料到使用者態的過程。

  優點:

  1. 可以頻繁地交換資料
  2. 可以自定義資料型別

三、共享記憶體

  訊息佇列的讀取和寫入的過程,都會有發生使用者態與核心態之間的訊息拷貝過程。而共享記憶體就很好的解決了這一問題。

  現代作業系統,對於記憶體管理,採用的是虛擬記憶體技術,也就是每個程序都有自己獨立的虛擬記憶體空間,不同程序的虛擬記憶體對映到不同的實體記憶體中。所以,即使程序 A 和 程序 B 的虛擬地址是一樣的,其實訪問的是不同的實體記憶體地址,對於資料的增刪查改互不影響。

  共享記憶體的機制,就是拿出一塊虛擬地址空間來,對映到相同的實體記憶體中。這樣這個程序寫入的東西,另外一個程序馬上就能看到,大大提高了程序間通訊的速度。

四、訊號量

  用了共享記憶體通訊方式,帶來新的問題:如果多個程序同時修改同一個共享記憶體,很有可能發生衝突。例如兩個程序都同時寫一個地址,先寫的程序會的內容會被覆蓋。

  為了防止多程序競爭共享資源而造成的資料錯亂,需要一種保護機制,使得共享的資源在任意時刻只能被一個程序訪問。訊號量就實現了這一保護機制。

  訊號量本質是一個整型的計數器,主要用於實現程序間的互斥與同步,而不是用於快取程序間通訊的資料。

訊號量表示資源的數量,控制訊號量的方式有兩種原子操作:

  • P 操作:將訊號量減去 -1,相減後如果訊號量 < 0,則表明資源已被佔用,程序需阻塞等待;相減後如果訊號量 >= 0,則表明還有資源可使用,程序可正常繼續執行。

  • V 操作:將訊號量加上 1,相加後如果訊號量 <= 0,則表明當前有阻塞中的程序,於是將該程序喚醒執行;相加後如果訊號量 > 0,則表明當前沒有阻塞中的程序;

P 操作是用在進入共享資源之前,V 操作是用在離開共享資源之後,這兩個操作是必須成對出現的。

具體過程:

  • 程序 A 在訪問共享記憶體前,先執行 P 操作,由於訊號量的初始值為 1,故在程序 A 執行 P 操作後訊號量變為 0,表示共享資源可用,於是程序 A 就可以訪問共享記憶體。

  • 若此時,程序 B 也想訪問共享記憶體,執行了 P 操作,結果訊號量變為 -1,意味著臨界資源已被佔用,因此程序 B 被阻塞。

  • 程序 A 訪問完共享記憶體,執行 V 操作,使得訊號量恢復為 0,接著就會喚醒阻塞中的執行緒 B,使得程序 B 可以訪問共享記憶體,最後完成共享記憶體的訪問後,執行 V 操作,使訊號量恢復到初始值 1。

  訊號初始化為1,代表著是互斥訊號量,它可以保證共享記憶體在任何時刻只有一個程序在訪問,這就很好的保護了共享記憶體。

  另外,在多程序裡,每個程序並不一定是順序執行的,它們基本是以各自獨立的、不可預知的速度向前推進,但有時候我們又希望多個程序能密切合作,以實現一個共同的任務。

  例如,程序 A 是負責生產資料,而程序 B 是負責讀取資料,這兩個程序相互合作、相互依賴,程序 A 必須先生產了資料,程序 B 才能讀取到資料,所以執行是有前後順序的。這時候,就可以用訊號量來實現多程序同步的方式,我們可以初始化訊號量為0

具體過程:

  • 如果程序 B 比程序 A 先執行了,那麼執行到 P 操作時,由於訊號量初始值為 0,故訊號量會變為 -1,表示程序 A 還沒生產資料,於是程序 B 就阻塞等待;

  • 接著,當程序 A 生產完資料後,執行了 V 操作,就會使得訊號量變為 0,於是就會喚醒阻塞在 P 操作的程序 B;

  • 最後,程序 B 被喚醒後,意味著程序 A 已經生產了資料,於是程序 B 就可以正常讀取資料了。

可以發現,訊號初始化為0,就代表著是同步訊號量,它可以保證程序 A 應在程序 B 之前執行。

五、訊號

  上面說的程序間通訊,都是常規狀態下的工作模式。對於異常情況下的工作模式,需要用訊號的方式來通知程序。

  訊號跟訊號量雖然名字相似,但兩者用途完全不一樣,就好像 Java 和 JavaScript 的區別。

  在 Linux 作業系統中, 為了響應各種各樣的事件,提供了幾十種訊號,分別代表不同的意義。可以通過 kill -l命令檢視所有的訊號.

執行在 shell 終端的程序,我們可以通過鍵盤輸入某些組合鍵的時候,給程序傳送訊號。例如

  • Ctrl+C 產生SIGINT 訊號,表示終止該程序;

  • Ctrl+Z 產生 SIGINTSIGTSTP訊號,表示停止該程序,但還未結束;

如果程序在後臺執行,可以通過 kill 命令的方式給程序傳送訊號,但前提需要知道執行中的程序 PID 號,例如:

  • kill -9 1050 ,表示給 PID 為 1050 的程序傳送SIGKILL訊號,用來立即結束該程序;

所以,訊號事件的來源主要有硬體來源(如鍵盤 Cltr+C )和軟體來源(如 kill 命令)。

訊號是程序間通訊機制中唯一的非同步通訊機制,因為可以在任何時候傳送訊號給某一程序,一旦有訊號產生,我們就有下面這幾種,使用者程序對訊號的處理方式。

  1. 執行預設操作。Linux 對每種訊號都規定了預設操作,例如,上面列表中的 SIGTERM 訊號,就是終止程序的意思。Core 的意思是 Core Dump,也即終止程序後,通過 Core Dump 將當前程序的執行狀態儲存在檔案裡面,方便程式設計師事後進行分析問題在哪裡。
  2. 捕捉訊號。我們可以為訊號定義一個訊號處理函式。當訊號發生時,就執行相應的訊號處理函式。
  3. 忽略訊號。當我們不希望處理某些訊號的時候,就可以忽略該訊號,不做任何處理。有兩個訊號是應用程序無法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,它們用於在任何時候中斷或結束某一程序。

六、socket

  網路通訊