Linux進程間的通訊方式和原理

進程的概念

  • 進程是操做系統的概念,每當咱們執行一個程序時,對於操做系統來說就建立了一個進程,在這個過程當中,伴隨着資源的分配和釋放。能夠認爲進程是一個程序的一次執行過程。

進程通訊的概念

  • 進程用戶空間是相互獨立的,通常而言是不能相互訪問的。但不少狀況下進程間須要互相通訊,來完成系統的某項功能。進程經過與內核及其它進程之間的互相通訊來協調它們的行爲。

進程通訊的應用場景

  • 數據傳輸:一個進程須要將它的數據發送給另外一個進程,發送的數據量在一個字節到幾兆字節之間。node

  • 共享數據:多個進程想要操做共享數據,一個進程對共享數據的修改,別的進程應該馬上看到。linux

  • 通知事件:一個進程須要向另外一個或一組進程發送消息,通知它(它們)發生了某種事件(如進程終止時要通知父進程)。數據結構

  • 資源共享:多個進程之間共享一樣的資源。爲了做到這一點,須要內核提供鎖和同步機制。socket

  • 進程控制:有些進程但願徹底控制另外一個進程的執行(如Debug進程),此時控制進程但願可以攔截另外一個進程的全部陷入和異常,並可以及時知道它的狀態改變。函數

進程通訊的方式

管道( pipe ):

管道包括三種:ui

  • 普通管道PIPE: 一般有兩種限制,一是單工,只能單向傳輸;二是隻能在父子或者兄弟進程間使用.
  • 流管道s_pipe: 去除了第一種限制,爲半雙工,只能在父子或兄弟進程間使用,能夠雙向傳輸.
  • 命名管道:name_pipe:去除了第二種限制,能夠在許多並不相關的進程之間進行通信.

信號量( semophore ) :

  • 信號量是一個計數器,能夠用來控制多個進程對共享資源的訪問。它常做爲一種鎖機制,防止某進程正在訪問共享資源時,其餘進程也訪問該資源。所以,主要做爲進程間以及同一進程內不一樣線程之間的同步手段。

消息隊列( message queue ) :

  • 消息隊列是由消息的鏈表,存放在內核中並由消息隊列標識符標識。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩衝區大小受限等缺點。

信號 ( sinal ) :

  • 信號是一種比較複雜的通訊方式,用於通知接收進程某個事件已經發生。

共享內存( shared memory ) :

  • 共享內存就是映射一段能被其餘進程所訪問的內存,這段共享內存由一個進程建立,但多個進程均可以訪問。共享內存是最快的 IPC 方式,它是針對其餘進程間通訊方式運行效率低而專門設計的。它每每與其餘通訊機制,如信號兩,配合使用,來實現進程間的同步和通訊。

套接字( socket ) :

  • 套解口也是一種進程間通訊機制,與其餘通訊機制不一樣的是,它可用於不一樣機器間的進程通訊。

各進程間通訊的原理及實現

管道

管道是如何通訊的

管道是由內核管理的一個緩衝區,至關於咱們放入內存中的一個紙條。管道的一端鏈接一個進程的輸出。這個進程會向管道中放入信息。管道的另外一端鏈接一個進程的輸入,這個進程取出被放入管道的信息。一個緩衝區不須要很大,它被設計成爲環形的數據結構,以便管道能夠被循環利用。當管道中沒有信息的話,從管道中讀取的進程會等待,直到另外一端的進程放入信息。當管道被放滿信息的時候,嘗試放入信息的進程會等待,直到另外一端的進程取出信息。當兩個進程都終結的時候,管道也自動消失。spa

這裏寫圖片描述

管道是如何建立的

從原理上,管道利用fork機制創建,從而讓兩個進程能夠鏈接到同一個PIPE上。最開始的時候,上面的兩個箭頭都鏈接在同一個進程Process 1上(鏈接在Process 1上的兩個箭頭)。當fork複製進程的時候,會將這兩個鏈接也複製到新的進程(Process 2)。隨後,每一個進程關閉本身不須要的一個鏈接 (兩個黑色的箭頭被關閉; Process 1關閉從PIPE來的輸入鏈接,Process 2關閉輸出到PIPE的鏈接),這樣,剩下的紅色鏈接就構成了如上圖的PIPE。操作系統

這裏寫圖片描述

  • 管道通訊的實現細節 
    在 Linux 中,管道的實現並無使用專門的數據結構,而是藉助了文件系統的file結構和VFS的索引節點inode。經過將兩個 file 結構指向同一個臨時的 VFS 索引節點,而這個 VFS 索引節點又指向一個物理頁面而實現的。以下圖

這裏寫圖片描述

有兩個 file 數據結構,但它們定義文件操做例程地址是不一樣的,其中一個是向管道中寫入數據的例程地址,而另外一個是從管道中讀出數據的例程地址。這樣,用戶程序的系統調用仍然是一般的文件操做,而內核卻利用這種抽象機制實現了管道這一特殊操做。線程

關於管道的讀寫

管道實現的源代碼在fs/pipe.c中,在pipe.c中有不少函數,其中有兩個函數比較重要,即管道讀函數pipe_read()和管道寫函數pipe_wrtie()。管道寫函數經過將字節複製到 VFS 索引節點指向的物理內存而寫入數據,而管道讀函數則經過複製物理內存中的字節而讀出數據。固然,內核必須利用必定的機制同步對管道的訪問,爲此,內核使用了鎖、等待隊列和信號。設計

當寫進程向管道中寫入時,它利用標準的庫函數write(),系統根據庫函數傳遞的文件描述符,可找到該文件的 file 結構。file 結構中指定了用來進行寫操做的函數(即寫入函數)地址,因而,內核調用該函數完成寫操做。寫入函數在向內存中寫入數據以前,必須首先檢查 VFS 索引節點中的信息,同時知足以下條件時,才能進行實際的內存複製工做:

  • 內存中有足夠的空間可容納全部要寫入的數據;
  • 內存沒有被讀程序鎖定。

若是同時知足上述條件,寫入函數首先鎖定內存,而後從寫進程的地址空間中複製數據到內存。不然,寫入進程就休眠在 VFS 索引節點的等待隊列中,接下來,內核將調用調度程序,而調度程序會選擇其餘進程運行。寫入進程實際處於可中斷的等待狀態,當內存中有足夠的空間能夠容納寫入數據,或內存被解鎖時,讀取進程會喚醒寫入進程,這時,寫入進程將接收到信號。當數據寫入內存以後,內存被解鎖,而全部休眠在索引節點的讀取進程會被喚醒。

管道的讀取過程和寫入過程相似。可是,進程能夠在沒有數據或內存被鎖定時當即返回錯誤信息,而不是阻塞該進程,這依賴於文件或管道的打開模式。反之,進程能夠休眠在索引節點的等待隊列中等待寫入進程寫入數據。當全部的進程完成了管道操做以後,管道的索引節點被丟棄,而共享數據頁也被釋放。

Linux函數原型

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

filedes[0]用於讀出數據,讀取時必須關閉寫入端,即close(filedes[1]);

filedes[1]用於寫入數據,寫入時必須關閉讀取端,即close(filedes[0])。

程序實例:

int main(void) { int n; int fd[2]; pid_t pid; char line[MAXLINE]; if(pipe(fd) 0){ /* 先創建管道獲得一對文件描述符 */ exit(0); } if((pid = fork()) 0) /* 父進程把文件描述符複製給子進程 */ exit(1); else if(pid > 0){ /* 父進程寫 */ close(fd[0]); /* 關閉讀描述符 */ write(fd[1], "\nhello world\n", 14); } else{ /* 子進程讀 */ close(fd[1]); /* 關閉寫端 */ n = read(fd[0], line, MAXLINE); write(STDOUT_FILENO, line, n); } exit(0); }

命名管道

因爲基於fork機制,因此管道只能用於父進程和子進程之間,或者擁有相同祖先的兩個子進程之間 (有親緣關係的進程之間)。爲了解決這一問題,Linux提供了FIFO方式鏈接進程。FIFO又叫作命名管道(named PIPE)。

實現原理

FIFO (First in, First out)爲一種特殊的文件類型,它在文件系統中有對應的路徑。當一個進程以讀(r)的方式打開該文件,而另外一個進程以寫(w)的方式打開該文件,那麼內核就會在這兩個進程之間創建管道,因此FIFO實際上也由內核管理,不與硬盤打交道。之因此叫FIFO,是由於管道本質上是一個先進先出的隊列數據結構,最先放入的數據被最早讀出來,從而保證信息交流的順序。FIFO只是借用了文件系統(file system,命名管道是一種特殊類型的文��,由於Linux中全部事物都是文件,它在文件系統中以文件名的形式存在。)來爲管道命名。寫模式的進程向FIFO文件中寫入,而讀模式的進程從FIFO文件中讀出。當刪除FIFO文件時,管道鏈接也隨之消失。FIFO的好處在於咱們能夠經過文件的路徑來識別管道,從而讓沒有親緣關係的進程之間創建鏈接

函數原型:

#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *filename, mode_t mode); int mknode(const char *filename, mode_t mode | S_IFIFO, (dev_t) 0 );

其中filename是被建立的文件名稱,mode表示將在該文件上設置的權限位和將被建立的文件類型(在此狀況下爲S_IFIFO),dev是當建立設備特殊文件時使用的一個值。所以,對於先進先出文件它的值爲0。

程序實例:

#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> int main() { int res = mkfifo("/tmp/my_fifo", 0777); if (res == 0) { printf("FIFO created/n"); } exit(EXIT_SUCCESS); }

參考文獻

Linux進程間通訊之管道(pipe)、命名管道(FIFO)與信號(Signal)

信號量

什麼是信號量

爲了防止出現因多個程序同時訪問一個共享資源而引起的一系列問題,咱們須要一種方法。好比在任一時刻只能有一個執行線程訪問代碼的臨界區域。臨界區域是指執行數據更新的代碼須要獨佔式地執行。而信號量就能夠提供這樣的一種訪問機制,讓一個臨界區同一時間只有一個線程在訪問它,也就是說信號量是用來調協進程對共享資源的訪問的。

信號量是一個特殊的變量,程序對其訪問都是原子操做,且只容許對它進行等待(即P(信號變量))和發送(即V(信號變量))信息操做。最簡單的信號量是隻能取0和1的變量,這也是信號量最多見的一種形式,叫作二進制信號量。而能夠取多個正整數的信號量被稱爲通用信號量。

信號量的工做原理

因爲信號量只能進行兩種操做等待和發送信號,即P(sv)和V(sv),他們的行爲是這樣的:

  • P(sv):若是sv的值大於零,就給它減1;若是它的值爲零,就掛起該進程的執行
  • V(sv):若是有其餘進程因等待sv而被掛起,就讓它恢復運行,若是沒有進程因等待sv而掛起,就給它加1.

舉個例子,就是兩個進程共享信號量sv,一旦其中一個進程執行了P(sv)操做,它將獲得信號量,並能夠進入臨界區,使sv減1。而第二個進程將被阻止進入臨界區,由於當它試圖執行P(sv)時,sv爲0,它會被掛起以等待第一個進程離開臨界區域並執行V(sv)釋放信號量,這時第二個進程就能夠恢復執行。

Linux的信號量機制

Linux提供了一組精心設計的信號量接口來對信號進行操做,它們不僅是針對二進制信號量,下面將會對這些函數進行介紹,但請注意,這些函數都是用來對成組的信號量值進行操做的。它們聲明在頭文件sys/sem.h中。

semget函數

它的做用是建立一個新信號量或取得一個已有信號量,原型爲:

int semget(key_t key, int num_sems, int sem_flags); 
  • 第一個參數key是整數值(惟一非零),不相關的進程能夠經過它訪問一個信號量,它表明程序可能要使用的某個資源,程序對全部信號量的訪問都是間接的,程序先經過調用semget函數並提供一個鍵,再由系統生成一個相應的信號標識符(semget函數的返回值),只有semget函數才直接使用信號量鍵,全部其餘的信號量函數使用由semget函數返回的信號量標識符。若是多個程序使用相同的key值,key將負責協調工做。

  • 第二個參數num_sems指定須要的信號量數目,它的值幾乎老是1。

  • 第三個參數sem_flags是一組標誌,當想要當信號量不存在時建立一個新的信號量,能夠和值IPC_CREAT作按位或操做。設置了IPC_CREAT標誌後,即便給出的鍵是一個已有信號量的鍵,也不會產生錯誤。而IPC_CREAT | IPC_EXCL則能夠建立一個新的,惟一的信號量,若是信號量已存在,返回一個錯誤。

semget函數成功返回一個相應信號標識符(非零),失敗返回-1.

semop函數

它的做用是改變信號量的值,原型爲:

int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops); 

sem_id是由semget返回的信號量標識符,sembuf結構的定義以下:

struct sembuf{ short sem_num;//除非使用一組信號量,不然它爲0 short sem_op;//信號量在一次操做中須要改變的數據,一般是兩個數,一個是-1,即P(等待)操做, //一個是+1,即V(發送信號)操做。 short sem_flg;//一般爲SEM_UNDO,使操做系統跟蹤信號, //並在進程沒有釋放該信號量而終止時,操做系統釋放信號量 }; 

semctl函數

int semctl(int sem_id, int sem_num, int command, ...); 

若是有第四個參數,它一般是一個union semum結構,定義以下:

union semun{ int val; struct semid_ds *buf; unsigned short *arry; }; 

前兩個參數與前面一個函數中的同樣,command一般是下面兩個值中的其中一個 SETVAL:用來把信號量初始化爲一個已知的值。p 這個值經過union semun中的val成員設置,其做用是在信號量第一次使用前對它進行設置。 IPC_RMID:用於刪除一個已經無需繼續使用的信號量標識符。

相關文章
相關標籤/搜索