進程間通信方式
- 前言 / 概述
- 一、管道
- 管道
- 命名管道
- 二、消息隊列
- 三、共享內存
- 四、信號量
- 信號量概述
- 互斥訪問
- 條件同步
- 信號
- 五、socket
- 總結
前言 / 概述
??每個進程的用戶地址空間都是獨立的,?般而言是不能互相訪問的,但內核空間是每個進程都共享的,所以進程之間要通信必須通過內核。

?
Linux 內核提供了不少進程間通信(IPC,Inter-Process Communication)的機制:
?
??在實際學習面試中不僅需要熟練列出進程間通信的方式,這只是表面功夫,還需要進一步了解每種通信方式的優缺點及應用場景。
一、管道
管道
管道,通常指無名管道,是 UNIX 系統IPC最古老的形式。
1. 特點
-
它是半雙工的(即數據只能在一個方向上流動),具有固定的讀端和寫端。
-
它只能用于具有親緣關系的進程之間的通信(父子進程或者兄弟進程之間)
-
基于內存文件的通信機制,可以看成是一種特殊的文件,但是它不是普通的文件,并不屬于其他任何文件系統,并且只存在于內存中。
缺點: 管道這種通信方式效率低,不適合進程間頻繁地交換數據。
2. 原理
#include <unistd.h>
int pipe(int fd[2]); // 返回值:若成功返回0,失敗返回-1
當一個管道建立時,它會創建兩個文件描述符:fd[0]
為讀而打開,fd[1]
為寫而打開。如下圖:
要關閉管道只需將這兩個文件描述符關閉即可。
??其實,所謂的管道,就是內核里面的一串緩存。從管道的一端寫入的數據,實際上是緩存在內核中的,另一端讀取,也就是從內核中讀取這段數據。另外,管道傳輸的數據是無格式的流且大小受限。
怎么樣才能使得管道是跨過兩個進程的呢?
??使用 fork
創建子進程,創建的子進程會復制父進程的文件描述符,這樣就做到了兩個進程各有兩個fd[0]
與 fd[1]
,兩個進程就可以通過各自的 fd 寫入和讀取同一個管道文件實現跨進程通信了。

?
??但管道只能一端寫入,另?端讀出,所以上面這種模式容易造成混亂,因為父進程和子進程都可以同時寫入,也都可以讀出。那么,為了避免這種情況,通常的做法是:
- 父進程關閉讀取的
fd[0]
,只保留寫入的fd[1]
; - 子進程關閉寫入的
fd[1]
,只保留讀取的fd[0]
;

?
所以說如果需要雙向通信,則應該創建兩個管道。
命名管道
FIFO,也稱為命名管道,它是一種文件類型。
1、特點
- FIFO可以在無關進程之間的交換數據,與無名管道不同,因為命名管道,提前創建了一個類型為管道的設備文件,在進程里只要使用這個設備文件,就可以相互通信。
- FIFO有路徑名與之相關聯,它以一種特殊設備文件形式存在于文件系統中(緩存在內核中)。
2、原型
#include <sys/stat.h>
// 返回值:成功返回0,出錯返回-1
int mkfifo(const char *pathname, mode_t mode);
其中的 mode 參數與open
函數中的 mode
相同。一旦創建了一個 FIFO,就可以用一般的文件I/O函數操作它。
當 open 一個FIFO時,是否設置非阻塞標志(O_NONBLOCK
)的區別:
- 若沒有指定
O_NONBLOCK
(默認),只讀 open 要阻塞到某個其他進程為寫而打開此 FIFO。類似的,只寫 open 要阻塞到某個其他進程為讀而打開它。 - 若指定了
O_NONBLOCK
,則只讀 open 立即返回。而只寫 open 將出錯返回 -1,如果沒有進程已經為讀而打開該 FIFO,其errno置ENXIO。
??不管是匿名管道還是命名管道,進程寫入的數據都是緩存在內核中,另一個進程讀取數據時候自然也是從內核中獲取,同時通信數據都遵循先進先出原則,不支持 lseek 之類的文件定位操作。
二、消息隊列
- 消息隊列是由操作系統維護的以字節序列為基本單位的間接通信機制
- 消息隊列,是消息的鏈接表,存放在內核中。一個消息隊列由一個標識符(即隊列ID)來標識。
特點
- 消息隊列是面向記錄的,其中的消息具有特定的格式以及特定的優先級。
- 消息隊列獨立于發送與接收進程。進程終止時,消息隊列及其內容并不會被刪除。
- 消息隊列可以實現消息的隨機查詢,消息不一定要以先進先出的次序讀取,也可以按消息的類型讀取。
- 兩大缺點:通信不及時、數據大小類型有限制
詳細概述
-
前面說到管道的通信方式是效率低的,因此管道不適合進程間頻繁地交換數據。對于這個問題,消息隊列的通信模式就可以解決。比如,A 進程要給 B 進程發送消息,A 進程把數據放在對應的消息隊列后就可以正常返回了,B 進程需要的時候再去讀取數據就可以了。同理,B 進程要給 A 進程發送消息也是如此。
-
消息隊列是保存在內核中的消息鏈表,在發送數據時,會分成一個一個獨立的數據單元,也就是消息體(數據塊),消息體是用戶自定義的數據類型,消息的發送方和接收方要約定好消息體的數據類型,所以每個消息體都是固定大小的存儲塊,不像管道是無格式的字節流數據。如果進程從消息隊列中讀取了消息體,內核就會把這個消息體刪除。
-
消息隊列生命周期隨內核,如果沒有釋放消息隊列或者沒有關閉操作系統,消息隊列會一直存在,而前面匿名管道的生命周期是隨進程的創建而建立,隨進程的結束而銷毀。
-
消息這種模型,兩個進程之間的通信就像平時發郵件一樣,你來一封,我回一封,可以頻繁溝通了。但郵件的通信方式存在不足的地方有兩點,一是通信不及時,二是附件大小有限制,這同樣也是消息隊列通信不足的點。
-
所以消息隊列不適合比較大數據的傳輸,因為在內核中每個消息體都有一個最大長度的限制,同時所有隊列所包含的全部消息體的總長度也是有上限。在 Linux 內核中,會有兩個宏定義
MSGMAX
和MSGMNB
,它們以字節為單位,分別定義了?條消息的最大長度和一個隊列的最大長度。 -
消息隊列通信過程中,存在用戶態與內核態之間的數據拷貝開銷,因為進程寫?數據到內核中的消息隊列時,會發生從用戶態拷貝數據到內核態的過程,同理另一進程讀取內核中的消息數據時,會發生從內核態拷貝數據到用戶態的過程。
三、共享內存
消息隊列的讀取和寫入的過程,都會有發生用戶態與內核態之間的消息拷貝過程。那共享內存的方式,就很好的解決了這?問題。
- 共享內存是把同一個物理內存區域同時映射到多個進程的內存地址空間的通信機制。
- 進程都有私有的地址空間,因此利用共享內存進行進程間通信,需要明確設置共享內存段。
優點:快速、方便的共享數據
缺點:必須用額外的同步機制來協調數據訪問。
1. 特點
-
共享內存是最快的一種 IPC,因為進程是直接對內存進行存取(直接通信、不通過內核)。
-
因為多個進程可以同時操作,所以需要進行同步。
-
信號量+共享內存通常結合在一起使用,信號量用來同步對共享內存的訪問。
2. 內部實現機制
??共享內存的機制,就是拿出一塊虛擬地址空間來,映射到相同的物理內存中。這樣這個進程寫入的東西,另外一個進程馬上就能看到了,都不需要拷貝來拷貝去,傳來傳去,大大提高了進程間通信的速度。

?
四、信號量
信號量概述
用了共享內存通信方式,帶來新的問題,那就是如果多個進程同時修改同一個共享內存,很有可能就沖突了。例如兩個進程都同時寫一個地址,那先寫的那個進程會發現內容被別人覆蓋了。
為了防止多進程競爭共享資源,而造成的數據錯亂,所以需要保護機制,使得共享的資源,在任意時刻只能被一個進程訪問。正好,信號量就實現了這一保護機制。
信號量(semaphore)與已經介紹過的 IPC 結構不同,它是一個整型的計數器,主要用于實現進程間的互斥與同步,而不是用于緩存進程間通信的數據
特點:
- 信號量用于進程間同步,若要在進程間傳遞數據需要結合共享內存。
- 信號量基于操作系統的 PV 操作,程序對信號量的操作都是原子操作。
- 每次對信號量的 PV 操作不僅限于對信號量值加 1 或減 1,而且可以加減任意正整數。
- 支持信號量組。
缺點: 傳送的信息量小,只有一個信號類型
信號量表示資源的數量,控制信號量的方式有兩種原子操作:
-
一個是 P 操作,這個操作會把信號量減去 1,相減后如果信號量
< 0
,則表明資源已被占用,進程需阻塞等待;相減后如果信號量>= 0
,則表明還有資源可使用,進程可正常繼續執行。 -
另一個是 V 操作,這個操作會把信號量加上 1,相加后如果信號量
<= 0
,則表明當前有阻塞中的進程,于是會將該進程喚醒運行;相加后如果信號量> 0
,則表明當前沒有阻塞中的進程;
P 操作是用在進入共享資源之前,V 操作是用在離開共享資源之后,這兩個操作是必須成對出現的。
互斥訪問
信號量的使用1:互斥訪問
如果要使得兩個進程互斥訪問共享內存,我們可以初始化信號量為 1
。

具體的過程如下:
- 進程 A 在訪問共享內存前,先執行了 P 操作,由于信號量的初始值為 1,故在進程 A 執行 P 操作后信號量變為 0,表示共享資源可用,于是進程 A 就可以訪問共享內存。
- 若此時,進程 B 也想訪問共享內存,執行了 P 操作,結果信號量變為了 -1,這就意味著臨界資源已被占用,因此進程 B 被阻塞。
- 直到進程 A 訪問完共享內存,才會執行V 操作,使得信號量恢復為 0,接著就會喚醒阻塞中的線程B,使得進程 B 可以訪問共享內存,最后完成共享內存的訪問后,執行 V 操作,使信號量恢復到初始值 1。
可以發現,信號初始化為 1
,就代表著是互斥信號量,它可以保證共享內存在任何時刻只有一個進程在訪問,這就很好的保護了共享內存。
條件同步
信號量的使用2:條件同步
??在多進程里,每個進程并不一定是順序執行的,但有時候我們又希望多個進程能密切合作,以實現一個共同的任務。例如,進程 A 是負責生產數據,而進程 B 是負責讀取數據,這兩個進程是相互合作、相互依賴的,進程 A必須先生產了數據,進程 B 才能讀取到數據,所以執行是有前后順序的。
這時候,就可以用信號量來實現多進程同步的?式,我們可以初始化信號量為 0
。

?
具體過程:
- 如果進程 B 比進程 A 先執行了,那么執行到 P 操作時,由于信號量初始值為 0,故信號量會變為-1,表示進程 A 還沒生產數據,于是進程 B 就阻塞等待;
- 接著,當進程 A 生產完數據后,執行了 V 操作,就會使得信號量變為 0,于是就會喚醒阻塞在 P 操作的進程 B;
- 最后,進程 B 被喚醒后,意味著進程 A 已經生產了數據,于是進程 B 就可以正常讀取數據了。
可以發現,信號初始化為 0
,就代表著是同步信號量,它可以保證進程 A 應在進程 B 之前執行。
信號
上面說的進程間通信,都是常規狀態下的工作模式。對于異常情況下的工作模式,就需要用「信號」的方式來通知進程。
信號跟信號量雖然名字相似度 66.66%,但兩者用途完全不一樣,就好像 Java 和 JavaScript 的區別。
在 Linux 操作系統中, 為了響應各種各樣的事件,提供了幾十種信號,分別代表不同的意義。我們可以通過 kill -l
命令,查看所有的信號:

?
運行在 shell 終端的進程,我們可以通過鍵盤輸入某些組合鍵的時候,給進程發送信號。
- Ctrl+C 產生
SIGINT
信號,表示終止該進程; - Ctrl+Z 產生
SIGTSTP
信號,表示停止該進程,但還未結束;
如果進程在后臺運行,可以通過 kill 命令的方式給進程發送信號,但前提需要知道運行中的進程 PID號,例如:
kill -9 1050
表示給 PID 為 1050 的進程發送SIGKILL
信號,?來立即結束該進程;
所以,信號事件的來源主要有硬件來源(如鍵盤 Cltr+C )和軟件來源(如 kill 命令)。
信號是進程間通信機制中唯一的異步通信機制,因為可以在任何時候發送信號給某一進程,一旦有信號產生,我們就有下面這幾種,用戶進程對信號的處理方式。
1. 執行默認操作。Linux 對每種信號都規定了默認操作,例如,上面列表中的 SIGTERM 信號,就是終止進程的意思。
2. 捕捉信號。我們可以為信號定義一個信號處理函數。當信號發生時,我們就執行相應的信號處理函數。
3. 忽略信號。當我們不希望處理某些信號的時候,就可以忽略該信號,不做任何處理。有兩個信號是應用進程無法捕捉和忽略的,即 SIGKILL
和 SEGSTOP
,它們用于在任何時候中斷或結束某一進程。
五、socket
前面提到的管道、消息隊列、共享內存、信號量和信號都是在同一臺主機上進行進程間通信,那要想跨網絡絡與不同主機上的進程之間通信,就需要 Socket 通信了。
實際上,Socket 通信不僅可以跨網絡與不同主機的進程間通信,還可以在同主機上進程間通信。
創建 socket 的系統調用:
int socket(int domain, int type, int protocal)
三個參數分別代表:
- domain 參數用來指定協議族,比如 AF_INET 用于 IPV4、AF_INET6 用于 IPV6、AF_LOCAL/AF_UNIX 用于本機;
- type 參數用來指定通信特性,比如 SOCK_STREAM 表示的是字節流,對應 TCP、SOCK_DGRAM表示的是數據報,對應 UDP、SOCK_RAW 表示的是原始套接字;
- protocal 參數原本是用來指定通信協議的,但現在基本廢棄。因為協議已經通過前面兩個參數指定完成,protocol 目前一般寫成 0 即可;
根據創建 socket 類型的不同,通信的方式也就不同:
- 實現 TCP 字節流通信: socket 類型是
AF_INET
和SOCK_STREAM
; - 實現 UDP 數據報通信:socket 類型是
AF_INET
和SOCK_DGRAM
; - 實現本地進程間通信: 「本地字節流 socket 」類型是
AF_LOCAL
和SOCK_STREAM
,「本地數據報 socket 」類型是AF_LOCAL
和SOCK_DGRAM
。另外,AF_UNIX 和 AF_LOCAL 是等價的,所以AF_UNIX 也屬于本地 socket;
1. 針對 TCP 協議通信的 socket 編程模型
?
-
服務端和客戶端初始化
socket
,得到文件描述符; -
服務端調用
bind
,將綁定在 IP 地址和端口; -
服務端調用
listen
,進行監聽; -
服務端調用
accept
,等待客戶端連接; -
客戶端調用
connect
,向服務器端的地址和端口發起連接請求; -
服務端
accept
返回用 于傳輸的socket
的文件描述符; -
客戶端調用
write
寫入數據;服務端調用read
讀取數據; -
客戶端斷開連接時,會調用
close
,那么服務端read
讀取數據的時候,就會讀取到了EOF
,待處理完數據后,服務端調用close
,表示連接關閉。
??這里需要注意的是,服務端調用 accept
時,連接成功了會返回一個已完成連接的 socket的網絡描述符,后續用來傳輸數據。所以,監聽的 socket 和真正用來傳送數據的 socket,是兩個不同的socket,一個是服務器的網絡描述符,一個是客戶端的網絡描述符,其具體程序設計參考如下所示:
?
?
成功連接建立之后,雙方開始通過 read
和 write
函數來讀寫數據,就像往一個文件流里面寫東西一樣。
2. 針對 UDP 協議通信的 socket 編程模型
?
?
-
UDP 是沒有連接的,所以不需要三次握手,也就不需要像 TCP 調用 listen 和 connect,但是 UDP 的交互仍然需要 IP 地址和端口,因此也需要
bind
-
對于 UDP 來說,不需要要維護連接,那么也就沒有所謂的發送方和接收方,甚至都不存在客戶端和服務端的概念,只要有一個 socket 多臺機器就可以任意通信,因此每?個 UDP 的 socket 都需要 bind。
-
另外,每次通信時,調用
sendto
和recvfrom
,都要傳入目標主機的 IP 地址和端口。
3. 針對本地進程間通信的 socket 編程模型
本地 socket 被用于在同一臺主機上進程間通信的場景:
-
本地 socket 的編程接?和 IPv4 、IPv6 套接字編程接?是?致的,可以支持「字節流」和「數據報」兩種協議;
-
本地 socket 的實現效率大大高于 IPv4 和 IPv6 的字節流、數據報 socket 實現;
-
對于本地字節流 socket,其 socket 類型是
AF_LOCAL
和SOCK_STREAM
。 -
對于本地數據報 socket,其 socket 類型是
AF_LOCAL
和SOCK_DGRAM
。
??本地字節流 socket 和 本地數據報 socket 在 bind 的時候,不像 TCP 和 UDP 要綁定 IP 地址和端口,而是綁定一個本地文件,這也就是它們之間的最大區別。
總結
由于每個進程的用戶空間都是獨立的,不能相互訪問,這時就需要借助內核空間來實現進程間通信,原因很簡單,每個進程都是共享一個內核空間。
Linux 內核提供了不少進程間通信的方式,其中最簡單的方式就是管道,管道分為「匿名管道」和「命名管道」。
1. 匿名管道:顧名思義,它沒有名字標識。
- 匿名管道是特殊文件只存在于內存,沒有存在于文件系統中
- shell命令中的「
|
」豎線就是匿名管道,通信的數據是無格式的流并且大小受限 - 通信的方式是單向的,數據只能在一個方向上流動,如果要雙向通信,需要創建兩個管道
- 再來匿名管道是只能用于存在父子關系的進程間通信
- 匿名管道的生命周期隨著進程創建而建立,隨著進程終止而消失。
2. 命名管道 突破了匿名管道只能在親緣關系進程間的通信限制,因為使用命名管道的前提,需要在文件系統創建一個類型為 p 的設備文件,那么毫無關系的進程就可以通過這個設備文件進程通信。
- 不管是匿名管道還是命名管道,進程寫入的數據都是緩存在內核中,另一個進程讀取數據時候自然也是從內核中獲取
- 同時通信數據都遵循先進先出原則,不支持 lseek 之類的文件定位操作。
3. 消息隊列 克服了管道通信的數據是無格式的字節流的問題,消息隊列實際上是保存在內核的「消息鏈表」,消息隊列的消息體是可以用戶自定義的數據類型,發送數據時,會被分成一個一個獨立的消息體,當然接收數據時,也要與發送方發送的消息體的數據類型保持一致,這樣才能保證讀取的數據是正確的。但消息隊列通信的速度不是最及時的,并且數據塊也有一定的大小限制,畢竟每次數據的寫入和讀取都需要經過用戶態與內核態之間的拷貝過程。
?
4. 共享內存 可以解決消息隊列通信中用戶態與內核態之間數據拷貝過程帶來的開銷
- 它直接分配一個共享空間,每個進程都可以直接訪問 ,就像訪問進程自己的空間一樣快捷方便,不需要陷入內核態或者系統調用,大大提高了通信的速度,享有最快的進程間通信方式之名。
- 但是便捷高效的共享內存通信,帶來新的問題,多進程競爭同個共享資源會造成數據的錯亂。
5. 信號量 解決了共享資源造成的數據錯亂
- 做到了對共享資源的保護,以確保任何時刻只能有一個進程訪問共享資源 ,這種方式就是互斥訪問。
- 信號量不僅可以實現訪問的互斥性,還可以實現進程間的同步。
- 信號量其實是一個計數器,表示的是資源個數,其值可以通過兩個原子操作來控制,分別是 P 操作和 V 操作
6. 信號 雖然與信號量名字十分相似,但功能一點兒都不?樣。
- 信號是進程間通信機制中唯一的異步通信機制,信號可以在應用進程和內核之間直接交互,內核也可以利用信號來通知用戶空間的進程發生了哪些系統事件
- 信號事件的來源主要有硬件來源(如鍵盤 Cltr+C )和軟件來源(如 kill 命令)
- 一旦有信號生,進程有三種方式響應信號 1. 執行默認操作、2. 捕捉信號、3. 忽略信號。
- 有兩個信號是應用進程無法捕捉和忽略的,即
SIGKILL
和SEGSTOP
,這是為了方便我們能在任何時候結束或停止某個進程。
7. 前面說到的通信機制,都是工作于同一臺主機,如果要與不同主機的進程間通信,那么就需要 Socket 通信了 。Socket 實際上不僅用于不同的主機進程間通信,還可以用于本地主機進程間通信。可根據創建Socket 的類型不同,分為三種常見的通信方式,一個是基于TCP 協議的通信方式,一個是基于 UDP 協議的通信方式,一個是本地進程間通信方式。
以上,就是進程間通信的主要機制了
整理自【清華大學】操作系統 + 【小林Coding】圖解系統
參考博文:進程間的五種通信方式介紹