Linux進程間通訊總結

Linux進程間通訊總結

1. 管道

管道是Linux支持的最初Unix IPC形式之一,具備如下特色:linux

(1)管道是半雙工的,數據只能向一個方向流動;須要雙方通訊時,須要創建起兩個管道;shell

(2)只能用於父子進程或者兄弟進程之間(具備親緣關係的進程);網絡

(3)單獨構成一種獨立的文件系統:管道對於管道兩端的進程而言,就是一個文件,但它不是普通的文件,它不屬於某種文件系統,而是自立門戶,單獨構成一種文件系統,而且只存在與內存中。框架

(4)數據的讀出和寫入:一個進程向管道中寫的內容被管道另外一端的進程讀出。寫入的內容每次都添加在管道緩衝區的末尾,而且每次都是從緩衝區的頭部讀出數據。dom

 

管道的建立異步

 

    #include <unistd.h>socket

    int pipe(int fd[2])函數

返回的fd[0]用於讀,fd[1]用於寫。所以,一個進程在由pipe()建立管道後,通常再fork一個子進程,而後經過管道實現父子進程間的通訊(所以也不難推出,只要兩個進程中存在親緣關係,這裏的親緣關係指的是具備共同的祖先,均可以採用管道方式來進行通訊)。oop

 

管道的應用:設計

* shell:

管道可用於輸入輸出重定向,它將一個命令的輸出直接定向到另外一個命令的輸入。好比,當在某個shell程序鍵入who│wc -l後,相應shell程序將建立who以及wc兩個進程和這兩個進程間的管道

* 用於具備親緣關係的進程間通訊

 

管道的侷限:

* 只支持單向數據流;

* 只能用於具備親緣關係的進程之間;

* 沒有名字;

* 管道的緩衝區是有限的(管道制存在於內存中,在管道建立時,爲緩衝區分配一個頁面大小);

* 管道所傳送的是無格式字節流,這就要求管道的讀出方和寫入方必須事先約定好數據的格式。

 

2. 有名管道

FIFO不一樣於管道之處在於它提供一個路徑名與之關聯,以FIFO的文件形式存在於文件系統中。這樣,即便與FIFO的建立進程不存在親緣關係的進程,只要能夠訪問該路徑,就可以彼此經過FIFO相互通訊(可以訪問該路徑的進程以及FIFO的建立進程之間),所以,經過FIFO不相關的進程也能交換數據。

 

有名管道建立:

 

    int mkfifo(const char * pathname, mode_t mode)

   

3. 信號

信號是在軟件層次上對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一箇中斷請求能夠說是同樣的。信號是異步的,一個進程沒必要經過任何操做來等待信號的到達,事實上,進程也不知道信號到底何時到達。信號事件的發生有兩個來源:硬件來源(好比咱們按下了鍵盤或者其它硬件故障);軟件來源,最經常使用發送信號的系統函數是kill, raise,alarm和setitimer以及sigqueue函數,軟件來源還包括一些非法運算等操做。

 

(1)信號的種類

可靠信號與不可靠信號, 實時信號與非實時信號

可靠信號就是實時信號, 那些從UNIX系統繼承過來的信號都是非可靠信號, 表如今信號

不支持排隊,信號可能會丟失, 好比發送屢次相同的信號, 進程只能收到一次. 信號值小於SIGRTMIN的都是非可靠信號.

非可靠信號就是非實時信號, 後來, Linux改進了信號機制, 增長了32種新的信號, 這些信

號都是可靠信號, 表如今信號支持排隊, 不會丟失, 發多少次, 就能夠收到多少次. 信號值

位於 [SIGRTMIN, SIGRTMAX] 區間的都是可靠信號.

(2)信號的安裝

早期的Linux使用系統調用 signal 來安裝信號

#include <signal.h>

void (*signal(int signum, void (*handler))(int)))(int); 

該函數有兩個參數, signum指定要安裝的信號, handler指定信號的處理函數.

該函數的返回值是一個函數指針, 指向上次安裝的handler

 

經典安裝方式:

if (signal(SIGINT, SIG_IGN) != SIG_IGN) {

    signal(SIGINT, sig_handler);

}

先得到上次的handler, 若是不是忽略信號, 就安裝此信號的handler

 

因爲信號被交付後, 系統自動的重置handler爲默認動做, 爲了使信號在handler處理期間, 仍能對後繼信號作出反應, 每每在handler的第一條語句再次調用 signal

sig_handler(ing signum)

{

    /* 從新安裝信號 */

    signal(signum, sig_handler);

    ......

}

咱們知道在程序的任意執行點上, 信號隨時可能發生, 若是信號在sig_handler從新安裝

信號以前產生, 此次信號就會執行默認動做, 而不是sig_handler. 這種問題是不可預料的.

 

使用庫函數 sigaction  來安裝信號

爲了克服非可靠信號並同一SVR4和BSD之間的差別, 產生了 POSIX 信號安裝方式, 使用sigaction安裝信號的動做後, 該動做就一直保持, 直到另外一次調用 sigaction創建另外一個動做爲止. 這就克服了古老的 signal 調用存在的問題

 

#include <signal.h> 
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

 

/* 設置SIGINT */

action.sa_handler = sig_handler;

sigemptyset(&action.sa_mask);

sigaddset(&action.sa_mask, SIGTERM);

action.sa_flags = 0;

 

/* 獲取上次的handler, 若是不是忽略動做, 則安裝信號 */

sigaction(SIGINT, NULL, &old_action);

if (old_action.sa_handler != SIG_IGN) {

    sigaction(SIGINT, &action, NULL);

}

 

基於 sigaction 實現的庫函數: signal

sigaction 天然強大, 但安裝信號很繁瑣, 目前linux中的signal()是經過sigation()函數實現的,所以,即便經過signal()安裝的信號,在信號處理函數的結尾也沒必要再調用一次信號安裝函數。

 

3)如何屏蔽信號

所謂屏蔽, 並非禁止遞送信號, 而是暫時阻塞信號的遞送, 

解除屏蔽後, 信號將被遞送, 不會丟失. 相關API爲

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum);

int sigdelset(sigset_t *set, int signum);

int sigismember(const sigset_t *set, int signum);

int sigsuspend(const sigset_t *mask);

int sigpending(sigset_t *set);

-----------------------------------------------------------------

int  sigprocmask(int  how,  const  sigset_t *set, sigset_t *oldset));

sigprocmask()函數可以根據參數how來實現對信號集的操做,操做主要有三種:

* SIG_BLOCK  在進程當前阻塞信號集中添加set指向信號集中的信號

* SIG_UNBLOCK   若是進程阻塞信號集中包含set指向信號集中的信號,則解除

   對該信號的阻塞

* SIG_SETMASK    更新進程阻塞信號集爲set指向的信號集

屏蔽整個進程的信號:

4)信號的生命週期

從信號發送到信號處理函數的執行完畢

對於一個完整的信號生命週期(從信號發送到相應的處理函數執行完畢)來講,

能夠分爲三個重要的階段,這三個階段由四個重要事件來刻畫:

信號誕生;信號在進程中註冊完畢;信號在進程中的註銷完畢;信號處理函數執行完畢。

 

下面闡述四個事件的實際意義:

信號"誕生"。信號的誕生指的是觸發信號的事件發生(如檢測到硬件異常、定時器超時以及調用信號發送函kill()或sigqueue()等)。

信號在目標進程中"註冊";

進程的task_struct結構中有關於本進程中未決信號的數據成員:

struct sigpending pending:

struct sigpending{

      struct sigqueue *head, **tail;

      sigset_t signal;

};

 

第三個成員是進程中全部未決信號集,第1、第二個成員分別指向一個

sigqueue類型的結構鏈(稱之爲"未決信號鏈表")的首尾,鏈表中

的每一個sigqueue結構刻畫一個特定信號所攜帶的信息,並指向下一個

sigqueue結構:

struct sigqueue{

      struct sigqueue *next;

      siginfo_t info;

}

信號的註冊

信號在進程中註冊指的就是信號值加入到進程的未決信號集中

(sigpending結構的第二個成員sigset_t signal),

而且加入未決信號鏈表的末尾。 只要信號在進程的未決信號集中,

代表進程已經知道這些信號的存在,但還沒來得及處理,或者該信號被進程阻塞。

當一個實時信號發送給一個進程時,無論該信號是否已經在進程中註冊,

都會被再註冊一次,所以,信號不會丟失,所以,實時信號又叫作"可靠信號"。

這意味着同一個實時信號能夠在同一個進程的未決信號鏈表中添加屢次. 

當一個非實時信號發送給一個進程時,若是該信號已經在進程中註冊,

則該信號將被丟棄,形成信號丟失。所以,非實時信號又叫作"不可靠信號"。

這意味着同一個非實時信號在進程的未決信號鏈表中,至多佔有一個sigqueue結構.

一個非實時信號誕生後,

(1)、若是發現相同的信號已經在目標結構中註冊,則再也不註冊,對於進程來講,

至關於不知道本次信號發生,信號丟失.

(2)、若是進程的未決信號中沒有相同信號,則在進程中註冊本身。

 

信號的註銷。

在進程執行過程當中,會檢測是否有信號等待處理

(每次從系統空間返回到用戶空間時都作這樣的檢查)。若是存在未決

信號等待處理且該信號沒有被進程阻塞,則在運行相應的信號處理函數前,

進程會把信號在未決信號鏈中佔有的結構卸掉。是否將信號從進程未決信號集

中刪除對於實時與非實時信號是不一樣的。對於非實時信號來講,因爲在未決信

號信息鏈中最多隻佔用一個sigqueue結構,所以該結構被釋放後,應該把信

號在進程未決信號集中刪除(信號註銷完畢);而對於實時信號來講,可能在

未決信號信息鏈中佔用多個sigqueue結構,所以應該針對佔用sigqueue結構

的數目區別對待:若是隻佔用一個sigqueue結構(進程只收到該信號一次),

則應該把信號在進程的未決信號集中刪除(信號註銷完畢)。不然,不該該在進程

的未決信號集中刪除該信號(信號註銷完畢)。 

進程在執行信號相應處理函數以前,首先要把信號在進程中註銷。

 

信號生命終止。

進程註銷信號後,當即執行相應的信號處理函數,執行完畢後,

信號的本次發送對進程的影響完全結束。

 

4. system V 提供的進程間通訊的三種方式

(1)消息隊列

 

與命名管道相比,消息隊列的優點在於,一、消息隊列也能夠獨立於發送和接收進程而存在,從而消除了在同步命名管道的打開和關閉時可能產生的困難。二、同時經過發送消息還能夠避免命名管道的同步和阻塞問題,不須要由進程本身來提供同步方法。三、接收程序能夠經過消息類型有選擇地接收數據,而不是像命名管道中那樣,只能默認地接收。

 

(2)信號量

 

信號量與其餘進程間通訊方式不大相同,它主要提供對進程間共享資源訪問控制機制。至關於內存中的標誌,進程能夠根據它斷定是否可以訪問某些共享資源,同時,進程也能夠修改該標誌。除了用於訪問控制外,還可用於進程同步。信號量有如下兩種類型:

二值信號量:最簡單的信號量形式,信號燈的值只能取0或1,相似於互斥鎖。

計算信號量:信號量的值能夠取任意非負值(固然受內核自己的約束)。

 

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

P(sv):若是sv的值大於零,就給它減1;若是它的值爲零,就掛起該進程的執行

V(sv):若是有其餘進程因等待sv而被掛起,就讓它恢復運行,若是沒有進程因等待sv而掛起,就給它加1.

 

(3)共享內存

 

速度最快,效率最高的進程間通訊方式,進程之間直接訪問內存,而不是經過傳送數據。可是使用共享內存須要本身提供同步機制。

 

5. 套接字(unix域協議)

socket API本來是爲網絡通信設計的,但後來在socket的框架上發展出一種IPC機制,就是UNIX Domain Socket。雖然網絡socket也可用於同一臺主機的進程間通信(經過loopback地址127.0.0.1),可是UNIX Domain Socket用於IPC更有效率:不須要通過網絡協議棧,不須要打包拆包、計算校驗和、維護序號和應答等,只是將應用層數據從一個進程拷貝到另外一個進程。UNIX域套接字與TCP套接字相比較,在同一臺傳輸主機的速度前者是後者的兩倍。這是由於,IPC機制本質上是可靠的通信,而網絡協議是爲不可靠的通信設計的。UNIX Domain Socket也提供面向流和麪向數據包兩種API接口,相似於TCP和UDP,可是面向消息的UNIX Domain Socket也是可靠的,消息既不會丟失也不會順序錯亂。

 值得注意的是,Unix域協議表示協議地址的是路徑名,而不是Internet域的IP地址和端口號。

 

    #define UNIX_PATH_MAX    108

    struct sockaddr_un 

    { 

    sa_family_t sun_family;               /* AF_UNIX */

    char        sun_path[UNIX_PATH_MAX];  /* pathname */

    };

   

socketpair 函數:建立一個全雙工的流管道

   

    int socketpair(int domain, int type, int protocol, int sv[2]);

 

使用unix域協議的例子

* libevent網絡庫對信號的封裝:libevent實現了對於socket網絡套接口,定時器事件,信號事件的統一監聽, 即統一事件源。簡單地說,就是把信號也轉換成IO事件,集成到Libevent中。網絡套接口實際爲文件描述符fd,能夠在epoll中直接監聽,定時器事件能夠設置epoll的超時時間進行監聽,信號的產生是隨機的,libevent網絡庫是如何進行處理使得能用epoll來實現信號的監聽呢?

 

假如用戶要監聽SIGINT這個信號,那麼在實現的內部就對SIGINT這個信號設置捕抓函數。此外,在實現的內部還要創建一條管道(pipe),並把這個管道加入到多路IO複用函數中。當SIGINT這個信號發生後,捕抓函數將會被調用。而這個捕抓函數的工做就是往管道寫入一個字符(這個字符每每等於所捕抓到信號的信號值)。此時,這個管道就變成是可讀的了,多路IO複用函數能檢測到這個管道變成可讀的了。換句話說就是多路IO複用函數檢測到這個SIGINT信號發生了,這也就完成了對信號的監聽工做。

 

  1. 建立一個管道(Libevent實際上使用的是socketpair)
  2. 爲這個socketpair的一個讀端建立一個event,並將之加入到多路IO複用函數的監聽之中
  3. 設置信號捕抓函數
  4. 有信號發生,就往socketpair寫入一個字節
相關文章
相關標籤/搜索