開頭
每個進程的用戶地址空間都是獨立的,進程與進程之間,內部空間是隔離的,進程 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 進程只需要讀取數據即可。反之亦如此。
消息隊列的本質是保存在內核中的一種消息鏈表,在發送數據時,會分成獨立的數據單元,也就是消息體(數據塊)。消息體是用戶自定義的數據類型,消息的發送方和接收方必須約定好消息體的數據類型,所以每個消息體都是固定大小的存儲塊,不像管道是無格式的字節流數據。如果進程從消息隊列中讀取了消息體,內核就會把這個消息體刪除。
消息隊列生命周期隨內核,如果沒有釋放消息隊列或者沒有關閉操作系統,消息隊列會一直存在,而前面提到的匿名管道的生命周期,是隨進程的創建而建立,隨進程的結束而銷毀。
消息這種模型,兩個進程之間的通信就像平時發郵件一樣,你來一封,我回一封,可以頻繁溝通。但郵件的通信方式存在不足的地方有兩點:一是通信不及時,二是附件也有大小限制,這同樣也是消息隊列通信不足的點。
消息隊列的優缺點
缺點:
- 通信不及時
- 不適合比較大數據的傳輸,因為在內核中每個消息體都有一個最大長度的限制,同時所有隊列所包含的全部消息體的總長度也是有上限。在 Linux 內核中,會有兩個宏定義 MSGMAX 和 MSGMNB,它們以字節為單位,分別定義了一條消息的最大長度和一個隊列的最大長度。
- 消息隊列通信過程中,存在用戶態與內核態之間的數據拷貝開銷,因為進程寫入數據到內核中的消息隊列時,會發生從用戶態拷貝數據到內核態的過程,同理另一進程讀取內核中的消息數據時,會發生從內核態拷貝數據到用戶態的過程。
優點:
- 可以頻繁地交換數據
- 可以自定義數據類型
三、共享內存
消息隊列的讀取和寫入的過程,都會有發生用戶態與內核態之間的消息拷貝過程。而共享內存就很好的解決了這一問題。
現代操作系統,對於內存管理,采用的是虛擬內存技術,也就是每個進程都有自己獨立的虛擬內存空間,不同進程的虛擬內存映射到不同的物理內存中。所以,即使進程 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 命令)。
信號是進程間通信機制中唯一的異步通信機制,因為可以在任何時候發送信號給某一進程,一旦有信號產生,我們就有下面這幾種,用戶進程對信號的處理方式。
- 執行默認操作。Linux 對每種信號都規定了默認操作,例如,上面列表中的 SIGTERM 信號,就是終止進程的意思。Core 的意思是 Core Dump,也即終止進程后,通過 Core Dump 將當前進程的運行狀態保存在文件里面,方便程序員事后進行分析問題在哪里。
- 捕捉信號。我們可以為信號定義一個信號處理函數。當信號發生時,就執行相應的信號處理函數。
- 忽略信號。當我們不希望處理某些信號的時候,就可以忽略該信號,不做任何處理。有兩個信號是應用進程無法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,它們用於在任何時候中斷或結束某一進程。
六、socket
網絡通信