面試題:管道、消息隊列、共享內存哪個開銷最小?

面試遇到這樣的問題,Linux包含多種進程間通信(interprocess communication,或者IPC),我們或多或少在面試中都被問及IPC的相關知識,所以這篇文章記錄總結一下自己對於IPC的認識和理解吧!

概述

進程間的通信手段大體可以分爲兩類

  • 通信類
    在這裏插入圖片描述

  • 同步類
    在這裏插入圖片描述

關於POSIX IPC與System V IPC的區別:

  • POSIX(Portable Operating System Interface for Computing Systems)是由IEEE 和ISO/IEC 開發的一簇UNIX標準。
  • System V, 曾經也被稱爲 AT&T System V,是Unix操作系統衆多版本中的一支。由AT&T的貝爾實驗室開發。

區別:
1、System V IPC方法早於POSIX IPC,幾乎所有的Unix平臺都支持System V IPC,其可移植性較好,但是在使用過程中也暴露出一些弱點。
2、POSIX IPC提供了和System V IPC相對應的工具(它也包括消息隊列、信號量和共享內存)。3、從設計的角度上講,POSIX IPC是優於System V IPC的,接口簡單,易於使用。但是POSIXIPC的可移植性並不如System V IPC。

IPC包含以下幾種方式

  • 管道
  • 消息隊列
  • 信號量
  • 共享內存

管道

管道是最早出現的進程間通信方式,在shell中執行命令,經常會將上一個命令的輸出作爲下一個命令的輸入,由多個命令配合完成一件事,這就是通過管道來完成的。

在這裏插入圖片描述

進程who的標準輸出,通過管道傳遞給下游的wc進程作爲標準輸入,從而通過相互配合完成了一件任務。

無名管道

無名管道的通信是作用在親緣進程之間的,所謂親緣進程是指有同一個公共祖先的進程組,所以管道不止可以用在父子進程,還可以用在兄弟進程、祖孫進程、叔侄進程

管道實質是一個字節流,並非前面提到的消息,沒有消息的邊界。如果多個進程發送的字節流混在一起,則無法辨認出各自的內容。所以一般是兩個有親緣關係的進程用管道來通信。從程序設計的角度來講,當進程調用pipe函數時,哪兩個有親緣關係的進程使用該管道來通信應是事先約定好的,其他有親緣關係的進程不應該進來攪局。

管道中的內容是閱後即焚的,這個特性指的是讀取管道內容是消耗型的行爲,即一個進程讀取了管道內的一些內容之後,這些內容就不會繼續在管道之中了。一般來講管道是單向的。如果兩個進程之間想雙向通信怎麼辦?可以建立兩個管道,如圖:
在這裏插入圖片描述
管道是一種文件,可以調用read、write和close等操作文件的接口來操作管道。另一方面管道又不是一種普通的文件,它屬於一種獨特的文件系統:pipefs。管道的本質是內核維護了一塊緩衝區與管道文件相關聯,對管道文件的操作,被內核轉換成對這塊緩衝區內存的操作。

在Linux下,可以使用如下接口創建管道:

#include <unistd.h>
int pipe(int pipefd[2]);

如果成功,則返回值是0,如果失敗,則返回值是-1,並且設置errno。
成功調用pipe函數之後,會返回兩個打開的文件描述符,一個是管道的讀取端描述符pipefd[0],另一個是管道的寫入端描述符pipefd[1]

調用pipe函數的進程隨後調用fork函數:
在這裏插入圖片描述
用管道通信的兩個進程,各持有一個管道文件描述符,不相干的進程應自覺關閉掉這些文件描述符。這麼做不僅僅是爲了讓數據的流向更加清晰,也不僅僅是爲了節省文件描述符,更重要的原因是:關閉未使用的管道文件描述符對管道的正確使用影響重大

管道有如下三條性質

  • 只有當所有的寫入端描述符都已關閉,且管道中的數據都被讀出,對讀取端描述符調用read函數纔會返回0(即讀到EOF標誌)。
  • 如果所有讀取端描述符都已關閉,此時進程再次往管道里面寫入數據,寫操作會失敗,errno被設置爲EPIPE,同時內核會向寫入進程發送一個SIGPIPE的信號。
  • 當所有的讀取端和寫入端都關閉後,管道才能被銷燬。

管道內存大小

本質來講管道也是一片內存區域,那它的大小是多少呢?

  • 自Linux 2.6.11版本起,管道的默認大小是65536字節.

可以調用fcntl來獲取和修改這個值的大小,代碼如下:

pipe_capacity = fcntl(fd, ?F_GETPIPE_SZ); //獲取管道大小
ret = fcntl(fd, ?F_SETPIPE_SZ, size); //設置管道大小

其上限記錄在/proc/sys/fs/pipe-max-size裏,對於特權用戶,還可以修改該上限值。
在這裏插入圖片描述

命名管道FIFO

無名管道因爲沒有實體文件與之關聯,靠的是世代相傳的文件描述符,所以只能應用在有共同祖先的各個進程之間。對於沒有親緣關係的任意兩個進程之間,無名管道就愛莫能助了。

命名管道就是爲了解決無名管道的這個問題而引入的。FIFO與管道類似,最大的差別就是有實體文件與之關聯。由於存在實體文件,不相關的沒有親緣關係的進程也可以通過使用FIFO來實現進程之間的通信。

與無名管道相比,命名管道僅僅是披了一件馬甲,其核心與無名管道是一模一樣的。

創建命名管道的接口定義方式:

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);//mode指FIFO文件的讀寫權限,與open函數類似

消息隊列

相比於管道來講,消息隊列機制中,雙方是通過消息來通信的,無需花費精力從字節流中解析出完整的消息。

System V消息隊列

消息隊列中每條消息都有type字段,消息的讀取進程可以通過type字段來選擇自己感興趣的消息,也可以根據type字段來實現按消息的優先級進行讀取,而不一定要按照消息生成的順序來依次讀取。

內核爲每一個System V消息隊列分配了一個msg_queue類型的結構體,它內部是維護一個優先級消息鏈表。

POSIX消息隊列

POSIX消息隊列與System V消息隊列有一定的相似之處,信息交換的基本單位是消息,但也有顯著的區別。

最大的區別當屬在Linux實現裏POSIX消息隊列的句柄本質是文件描述符。這個性質給POSIX消息隊列帶來了巨大的優勢。因爲是文件描述符,所以可以使用I/O多路複用系統調用(select、poll或epoll等)來監控這個文件描述符。

其次,POSIX消息隊列提供了通知功能,當消息隊列中有消息可用時,就會通知到進程。而System V消息隊列沒有通知功能,所以消息隊列上何時有消息進程無從得知,只能阻塞(msgrcv)或輪詢(帶IPC_NOWAIT標誌位的msgrcv)。

最後,System V消息隊列的消息提取要比POSIX消息隊列靈活。POSIX消息隊列本質是個優先級隊列。而System V消息中存在類型字段,可以提取類型等於某值的消息,這點POSIX消息隊列是做不到的。

信號量

消息隊列的作用是進程之間傳遞消息。而信號量的作用是爲了同步多個進程的操作。

一般來說,信號量是和某種預先定義的資源相關聯的。信號量元素的值,表示與之關聯的資源的個數。內核會負責維護信號量的值,並確保其值不小於0。信號量上支持的操作有:

  • 將信號量的值設置成一個絕對值。
  • 在信號量當前值的基礎上加上一個數量。
  • 在信號量當前值的基礎上減去一個數量。
  • 等待信號量的值等於0。
創建打開信號量

創建或打開信號量的函數爲semget,其接口如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);//nsems表示信號量的個數

共享內存

共享內存是所有IPC手段中最快的一種。它之所以快是因爲共享內存一旦映射到進程的地址空間,進程之間數據的傳遞就不須要涉及內核了。

前面已經討論過的管道、FIFO和消息隊列,任意兩個進程之間想要交換信息,都必須通過內核,內核在其中發揮了中轉站的作用:

  • 發送信息的一方,通過系統調用(write或msgsnd)將信息從用戶層拷貝到內核層,由內核暫存這部分信息。
  • 提取信息的一方,通過系統調用(read或msgrcv)將信息從內核層提取到應用層。
    在這裏插入圖片描述

一個通信週期內,上述過程至少牽扯到兩次內存拷貝(從用戶拷貝到內核空間和從內核空間拷貝到用戶空間)和兩次系統調用,這其中的開銷不容小覷。用戶層的體驗固然不佳,所以產生了共享內存的通信方式,共享內存是在用戶空間不經過內核:
在這裏插入圖片描述
共享內存,這種思路可以通俗地概括爲內核搭臺,進程唱戲。簡單地說,內核負責構建出一片內存區域,兩個或多個進程可以將這塊內存區域映射到自己的虛擬地址空間,從此之後內核不再參與雙方通信。

注意 建立共享內存之後,內核完全不參與進程間的通信,這種說法嚴格來講並不是正確的。因爲當進程使用共享內存時,可能會發生缺頁,引發缺頁中斷,這種情況下,內核還是會參與進來的。

總結

在Unix發展過程中出現了各類的進程通信方式,目前比較主流的通信方式主要包含管道、信號量、消息隊列、共享內存等,從比較它們的底層實現可以看出來共享內存是開銷最小的通信方式,源於它和內核的交流比較少,從而降低了開銷。

歡迎讀者一起交流學習,如本文章有錯誤之處還望不吝指出!謝謝!
個人github賬號:https://github.com/SpecialAll

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章