Linux進程間通信總結


Linux進程間通信總結

1. 管道

管道是Linux支持的最初Unix IPC形式之一,具有以下特點:

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

(2)只能用於父子進程或者兄弟進程之間(具有親緣關系的進程);

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

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

 

管道的創建

 

    #include <unistd.h>

    int pipe(int fd[2])

返回的fd[0]用於讀,fd[1]用於寫。因此,一個進程在由pipe()創建管道后,一般再fork一個子進程,然后通過管道實現父子進程間的通信(因此也不難推出,只要兩個進程中存在親緣關系,這里的親緣關系指的是具有共同的祖先,都可以采用管道方式來進行通信)。

 

管道的應用:

* 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;

};

 

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

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)消息隊列

 

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

 

(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寫入一個字節

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM