Linux進程間通信機制詳談

Linux進程間通信機制

Unix系統提供的進程間通信機制主要有:

  • 管道和FIFO(命名管道)
  • 套接字
  • 信號
  • 信號量
  • 消息隊列
  • 共享內存區

管道pipe

在這裏插入圖片描述

管道機制思想是在內存中創建一個共享文件,從而使得通信雙方利用該共享文件進行交互。需要注意的是管道數據流動是單向的,是半雙工的,數據只能向一個方向流動;需要雙方通信時,需要建立起兩個管道。管道只在具有公共祖先的兩個進程之間使用,可以將管道視爲一個獨立的文件系統,管道在管道兩側的進程看來就是一個文件,只是這個文件只存在於內存中。

一個進程寫入管道的所有數據由內核定向到另一個進程,另一個進程就可以從管道中讀取數據。當緩衝區讀空或者寫滿時,有一定的規則控制相應的讀進程或者寫進程進入等待隊列,當空的緩衝區有新數據寫入或者滿的緩衝區有數據讀出來時,就喚醒等待隊列中的進程繼續讀寫。

在Unix的shell中,通過 “|” 創建管道(樣子就像管道一樣),將前面的進程的標準輸出重定向到管道中,然後後面的進程從管道中讀取輸入。

管道的創建:應用程序調用pipe系統產生管道,該調用將返回兩個文件描述符,分別用於管道的兩側。由於兩個描述符位於同一個進程,因此進程自身通信,好像沒太大作用,但是通過fork或者clone複製進程時管道也會被複制,因此管道通信就是利用該性質,實現了具有公共祖先的進程之間的通信。

常用的操作就是創建一個連接到另一個進程的管道,然後將其輸出或向其輸入端發送數據。標準IO庫提供了兩個函數,popen 和 pclose,這兩個函數可以實現:創建一個管道,fork一個子進程,關閉未使用的管道端,執行一個shell運行命令,等待命令終止。

管道具有侷限性:只支持單向數據流,沒有名字,緩衝區有限等

命名管道FIFO

管道很簡單靈活有效,但是有一個比較大的缺點,那就是無法打開已經存在的管道,即兩個進程不能共享同一個管道,同時匿名管道,由於沒有名字,只能用於親緣關係的進程間通信。故Unix引入了命名管道,或稱爲FIFO(先進先出),因爲這是一種最先寫入文件的字節總是最先被讀出的特殊文件類型。

FIFO文件包含在系統的文件樹中,即擁有磁盤索引節點。因此哪怕沒有親緣關係的進程也可以通過該方式消息傳遞。

  • FIFO的索引節點出現在系統目錄樹上
  • FIFO是一種雙向通信管道,即一個進程可以以讀/寫模式打開一個FIFO

通過mkfifo函數創建FIFO文件,之後可以用open打開它。命名管道在打開時需要確實該管道已經存在,否則將阻塞。即以讀方式打開某管道,在此之前必須一個進程以寫方式打開管道,否則阻塞。此外,可以以讀寫(O_RDWR)模式打開有名管道,即當前進程讀,當前進程寫,不會阻塞。
在這裏插入圖片描述

消息隊列

在這裏插入圖片描述
消息傳遞機制通過消息隊列實現,發送方產生消息並將消息寫到隊列,之後一個或多個其他進程從隊列獲取消息。消息包含消息正文還有一個數,該數用來使消息隊列實現多種不同類型的消息,接收者通過該數字獲取自己想要的消息。消息讀取後將從隊列中刪除,即便是很多進程在同一個信道上監聽,每一個消息仍然只能被一個進程讀取。即進程產生的消息放入到 IPC 消息隊列,直到一個進程將其讀走然後再移除。

需要注意的是,發送者和接收者通過消息隊列傳遞信息時,不需要同時運行。發送進程打開一個隊列將消息放入後則結束工資,接收進程再發送方結束工作後仍可以訪問該消息隊列並獲取需要的消息。這期間是由內核維護的。

  • 爲了發送一條消息,進程調用msgsnd()函數,該函數的參數如下:
    • 目標消息隊列的IPC標識符
    • 消息正文的大小
    • 用戶態緩衝區的地址,緩衝區中包含有消息的類型,之後緊跟消息正文
  • 爲了獲得一條消息,進程調用msgrcv()函數,該函數參數如下:
    • IPC消息隊列資源的IPC標識符
    • 指向用戶態緩衝區的指針,消息類型和消息正文將被拷貝至該緩衝區
    • 緩衝區的大小
    • 一個值t,即應該獲取消息的特徵

消息隊列的結構定義如下:

struct msg_queue{
    struct kern_ipc_perm q_perm;
    time_t q_stime; //上一次調用msgsnd發送消息的時間
    time_t q_rtime; //上一次調用msgrcv接收消息的時間
    time_t q_ctime; //上一次修改的時間
    unsigned long q_cbytes; //隊列上當前的字節數目
    unsigned long q_qnum; //隊列中的消息數目
    unsigned long q_qbytes; //隊列上最大字節數目
    pid_t q_lspid; //上一次調用msgsnd的pid
    pid_t q_lrpid; //上一次接收消息的pid
    
    struct list_head q_messsages; //隊列中的消息鏈表
    struct list_head q_receivers;  //接收消息的進程鏈表
    struct list_head q_senders;   //發送消息的進程鏈表
};

因爲當消息隊列滿時(或到達了最大消息數,或到達了最大字節數),則試圖令新消息入列的進程將被阻塞,將其插入到發送消息鏈表。當消息隊列是空時(或者進程指定的消息類型當前隊列中沒有),接收進程將被阻塞,將其插入到接收消息鏈表。 被阻塞進入睡眠的進程,在滿足條件後將被喚醒並自動嘗試重新收發操作。

q_message各個消息都被封裝在如下數據結構中:

struct msg_msg{
    struct list_head m_list; //連接各消息的鏈表元素
    long m_type; //消息類型
    int m_ts;  //消息正文長度
    struct msg_msgseg* next; 
    /*     消息內容       */
};

結構體中並沒有指定存儲消息自身的字段,因爲每個消息至少分配一個內存頁,msg_msg保存在該頁的起始處,剩餘空間即可存儲消息正文,如果消息較長需要別的內存頁,則由next指針連接。
在這裏插入圖片描述
消息隊列的整體數據結構:(無阻塞睡眠的發送進程鏈表)
在這裏插入圖片描述
進程間通信可以通過調用原語 send() 和 receive() 來進行。實現這些原語有不同的設計方案。消息傳遞可以是阻塞或非阻塞,也稱爲同步或異步:

  • 阻塞發送:發送進程阻塞,直到消息由接收進程或郵箱所接收。
  • 非阻塞發送:發送進程發送消息,並且恢復操作。
  • 阻塞接收:接收進程阻塞,直到有消息可用。
  • 非阻塞接收:接收進程收到一個有效消息或空消息。

共享內存

採用共享內存的進程通信,就需要建立共享區域。共享內存允許兩個或多個進程通過把共享數據結構放入到共享內存區來進行訪問。如果進程要訪問共享內存區,就必須在自己的地址空間增加一個新內存區,用來映射與這個共享內存區相關的頁框。共享內存是進程間通信最快的方式,但是一定要保證數據的同步

  • 調用shmget()函數獲得共享內存區的IPC標識符,如果該共享內存區不存在則創建它
  • 調用shmat()函數把一個共享內存區附加到一個進程上
  • 調用shmdt()函數把一個共享內存區從進程的地址空間刪除
    在這裏插入圖片描述

在smd_ids全局變量的entries數組中保存了kern_ipc_perm和shmid_kernel的組合,以便管理IPC對象的訪問權限。對每個共享內存對象都創建一個僞文件,通過shm_file連接到shmid_kernel的實例。內核使用了shm_file->f_mapping指針訪問地址空間對象。完成這個過程還需要設置各進程的頁表,使每個進程都能夠訪問該IPC相關的共享內存區域。

需要注意的是,由於多個進程共享一段內存,因此需要依靠某種同步機制(如信號量)來達到進程間的同步及互斥。

IPC信號量

IPC信號量類似於內核信號量,是一個計數器,用來爲多個進程共享的數據結構提供受控訪問。

如果受保護的資源是可用的,則信號量的值就是正數;如果受保護的資源現在不可用,則信號量的值爲0。要訪問資源的進程就是試圖將對應的信號量減1,如果當前信號量值不爲正則內核將阻塞該行爲,直至該值爲正再喚醒該進程。當一個進程釋放該資源時,對應的信號量就加1。

爲了獲取該共享資源,進程的操作如下:

  1. 測試控制該資源的信號量
  2. 如果信號量爲正,則使用該資源,信號量對應減1
  3. 否則,內核阻塞該過程,進程進入休眠狀態。直至被喚醒後將進入步驟1
  4. 當進程不再使用該資源時將釋放資源,對應信號量加1,如果有進程正在休眠等該信號量,則將其喚醒

爲了正確完成操作,信號量的測試以及加1減1操作必須是原子操作

IPC信號量相較於內核信號量更加複雜,主要是每個IPC信號量可能是多個信號量值的集合,即一個IPC資源保護多個獨立共享的資源,並且System V IPC信號量提供了失效安全機制,當進程死亡時可以令對應的IPC信號量恢復。

當進程想訪問IPC信號量所保護的一個或多個資源時,步驟如下:

  • 調用semget()封裝函數獲得IPC信號量標識符
  • 調用semop()封裝函數測試並遞減所有原始信號量的值,如果不能訪問這些資源則將進程掛起
  • 當放棄受保護的資源時,調用semop()函數原子增加所有相關的信號量
  • semct()封裝函數可以用於刪除IPC信號量

信號

信號是Linux系統中用於進程間互相通信的一種機制,信號可以在任何時候發給某一進程,而無需知道該進程的狀態;如果該進程當前並未處於執行狀態,則該信號由內核保存起來,直到該進程恢復執行並接收該信號爲止;如果一個信號被進程設置爲阻塞,則該信號的傳遞將被延遲,直到其阻塞被取消是才被傳遞給進程。

使用信號的主要目的:

  • 讓進程知道某一特定事件已經發生
  • 強迫進程執行它自己代碼中的信號處理程序

信號是軟中斷,是一種異步通信方式,信號可以在用戶空間進程和內核之間直接交互,內核可以利用信號來通知用戶空間的進程發生了哪些系統事件。

kill命令通過PID向進程發送信號,可以通過kill -l 查看支持哪些信號量

在這裏插入圖片描述

一些信號的含義:

以下列出幾個常用的信號:

信號名稱 描述
SIGHUP 用戶從終端註銷,所有由該終端開啓的進程都將收到該進程。系統缺省狀態下對該信號的處理是終止進程
SIGINT 程序終止信號。程序運行過程中,按Ctrl+C鍵將產生該信號
SIGQUIT 程序退出信號,程序運行過程中,按Ctrl+\\鍵將產生該信號
SIGKILL 用戶終止進程執行信號。shell下執行kill -9發送該信號。該信號不能被阻塞、處理和忽略
SIGTERM 程序結束(terminate)信號, 與SIGKILL不同的是該信號可以被阻塞和處理。通常用來要求程序自己正常退出。
SIGSTOP 暫停(stopped)進程的執行. 該進程還未結束, 只是暫停執行. 本信號不能被阻塞, 處理或忽略.

信號傳遞的流程:

  • 一個進程產生信號,並設置要發送的進程PID,然後傳遞給內核
  • 內核根據對應的進程的設置決定是否發送給接收進程,如果接收者阻塞該信號,則暫時保留該信號,直到該進程解除了對此信號的阻塞(如果對應進程已經退出,則丟棄此信號),如果對應進程沒有阻塞,操作系統將傳遞此信號
  • 接收進程接收到此信號後,將根據當前進程對此信號設置的預處理方式,暫時終止當前代碼的執行,保護上下文(即保存當前的狀態),然後開始執行中斷服務程序,執行完成後恢復上下文,繼續執行中斷前的程序

套接字

進程通過套接字網絡接口可以實現通信,既可以和本機內的進程通信,也可以和本機外的進程通信。

網絡套接字需要底層協議的支持,例如 TCP(傳輸控制協議)或 UDP(用戶數據報協議)。

IPC 套接字依賴於本地系統內核的支持來進行通信;特別的,IPC 通信使用一個本地的文件作爲套接字地址
在這裏插入圖片描述

也就是通信雙方基於套接字編程,基於提供的庫函數進行網絡通信。

這一部分具體應該詳看網絡的相關書籍瞭解。

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