1. 程式人生 > >Linux進程間通信之管道(pipe)、命名管道(FIFO)與信號(Signal)

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

阻塞 node include 系統 types.h 標準 div [0 pipe

整理自網絡

Unix IPC包括:管道(pipe)、命名管道(FIFO)與信號(Signal)

管道(pipe)

管道可用於具有親緣關系進程間的通信,有名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關系進程間的通信;

實現機制:

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

技術分享

從原理上,管道利用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);
}
技術分享

命名管道(named PIPE)

由於基於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 );

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

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