1. 程式人生 > >程序通訊之管道(PIPE)

程序通訊之管道(PIPE)

在前面程序通訊概念和程序通訊方式,我們瞭解了程序通訊的簡單概念以及4種程序通訊的方式,今天我們將要通過具體例項來學習,理解程序通訊方式中的管道(PIPE)

本文所有程式碼都在Ubuntu16.04測試。

我們在前面已經瞭解了常用的程序間通訊方式,它們大致可以以如下方式分類:

A. 傳統的程序間通訊方式
無名管道(pipe)、有名管道(fifo)和訊號(signal)

B. System v IPC物件
共享記憶體(share memory)、訊息佇列(message queue)和訊號量(semaphore)

C. BSD
套接字(socket)

擷取網上的一張圖:

這裡寫圖片描述

今天,我們主要聊聊無名管道,以及有名管道。

定義和特性

首先什麼叫管道?

在聊管道之前,我們先看看管道是幹嘛的?管道是用於程序通訊的一種方式,說白了就是有兩個或者多個程序要相互發送,接收資訊,那麼問題來了,我們知道程序的地址空間是各自獨立的,(資料段寫時拷貝,程式碼段共享),如果想對兩程序AB裡面資料共享操作,那麼必須要有一種機制來保證,而管道就是這樣的一種機制。

管道就是作業系統在核心中開闢的一段緩衝區,程序1可以將需要互動的資料拷貝到這段緩衝區,程序2就可以讀取了,類似於下面這張圖。

這裡寫圖片描述

說白了,管道就有點像檔案,將需要的資料放在這樣一個檔案裡面,程序1向檔案中寫,程序2從檔案中讀。

管道有兩種:

  1. 匿名管道
  2. 命名管道

說實在點,這兩者區別不是很大,如果把管道理解為檔案,命名管道就是知道名字的檔案,匿名就是不知道名字的檔案,當然了,它倆還是有區別的,最大的區別就是命名管道支援不相關程序通訊,而匿名管道只支援有血緣關係的程序通訊,比如父子程序,兄弟程序等等。。。由於區別不大,我們先講述匿名管道,命名管道基本類似。

匿名管道有如下特性:

1、半雙工通訊(單向通訊),資料在同一時刻只能在一個方向上流動。資料只能從管道的一端寫入,從另一端讀出。從管道讀資料是一次性操作,資料一旦被讀走,它就從管道中被拋棄,釋放空間以便寫更多的資料。
2、寫入管道中的資料遵循先入先出(FIFO)的規則。
3、面向位元組流。管道所傳送的資料是無格式的,這要求管道的讀出方與寫入方必須事先約定好資料的格式,如多少位元組算一個訊息等。


4、依賴於檔案系統,生命週期隨程序結束而結束
5、匿名管道沒有名字,只能在具有公共祖先的程序(父程序與子程序,或者兩個兄弟程序,具有親緣關係)之間使用。(常用於父子程序通訊)
6、同步機制

在Linux下,管道是由pipe這個函式建立:

int pipe(int filedes[2]);
功能: 建立無名管道。
引數: filedes為 int 型陣列的首地址,其存放了管道的檔案描述符 filedes[0]、filedes[1]。
當一個管道建立時,它會建立兩個檔案描述符 fd[0] 和 fd[1]。其中 fd[0] 固定用於讀管道,而 fd[1] 固定用於寫管道。
返回值:成功:0 ; 失敗:-1

1 . 當我們在父程序中建立了一個管道,它大概就是這樣的:

這裡寫圖片描述

當一個管道建立時,它會建立兩個檔案描述符 fd[0] 和 fd[1]。其中 fd[0] 固定用於讀管道,而 fd[1] 固定用於寫管道。

2 . 我們fork出子程序後,子程序對它進行了一次拷貝。

這裡寫圖片描述

3 . 由於管道是單向通訊,如果是子程序寫,父程序讀取的話,需要關閉子程序的讀端和父程序的寫端。資料從寫端流入從讀端流出,這樣就實現了程序間通訊。

下面我們看一個例子:子程序想管道寫10次i am child,父程序從中讀取。

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>

#define TIMES 10


int main()
{
    //create a pipe
    int pe[2] = {0};
    if (pipe(pe) == -1)
    {
        perror("pipe failure\n");    
        exit(1);
    }

    int pid = fork();

    if (pid < 0)
    {
        perror("fork failure\n");
        exit(2);
    }
    else if (pid == 0) //child
    {
        close(pe[0]); //close reading
        int i = 0;

        const char* message = "i am child\n";

        for (; i < TIMES; ++i)
        {
            write(pe[1], message, strlen(message));
            sleep(1);
        }

    }
    else // father
    {
        close(pe[1]); //close writing

        char buffer[100] = {'\0'};
        int i = 0;
        for (; i < TIMES; ++i)
        {
            read(pe[0], buffer, sizeof(buffer)); 
            printf("#child : %s", buffer);
        }

    }

    return 0;
}

結果如下:

這裡寫圖片描述

注意到,我在這裡讓子程序一秒一寫,而父程序會等待子程序,直到管道中有資料才讀取,這樣子保證同步機制。借別人圖一用~如下:

這裡寫圖片描述

對於讀程序
當read一個寫端已經關閉的管道時,在所有的資料都被讀取之後,read返回0標識管道讀取完畢。從技術上來講,如果管道的寫端還有程序存在,就不會產生檔案的結束。如果寫端存在,但是管道中沒有資料,此時讀端會被阻塞。
如果讀取一個寫端還存在的程序,如果一次讀取的資料位元組數大於管道能夠容納的最大位元組數(PIPE _ BUF表示管道能夠容納的最大位元組數,可以通過pathconf和fpathconf函式獲取該值),則返回管道中現有的位元組數;如果請求的資料量不大於PIPE_BUF,則返回請求讀取的位元組數(請求的位元組數小於管道現有的位元組數)或者管道中現有的位元組數(管道中現有的位元組數小於請求的位元組數)。

對於寫程序
向管道中寫資料的時候,PIPE_ BUF規定了核心中管道緩衝區的大小。如果對管道呼叫write且所寫的位元組數小於等於PIPE _ BUF,則此操作不會與其他程序對同一管道的的寫操作交叉進行。但是如果有多個程序對管道同時寫一個管道,且所寫的位元組數大於PIPE_BUF,那麼縮寫的資料就會與其他的程序交叉。如果管道已經滿了,且沒有讀程序讀取管道中的資料,此時寫操作將會被阻塞。

我們測試看一下匿名管道有多大??

#include<stdio.h>  
#include<sys/types.h>  
#include<unistd.h>  
#include<fcntl.h>  
#include<errno.h>  
#include<stdlib.h>  
#include<string.h>  
int main()  
{  
    int _pipe[2];  
    if(pipe(_pipe)==-1)  
    {  
        printf("pipe error\n");  
        return 1;  
    }  
    int ret;  
    int count=0;  
    int flag=fcntl(_pipe[1],F_GETFL);  
    fcntl(_pipe[1],F_SETFL,flag|O_NONBLOCK);  
    while(1)  
    {  
        ret=write(_pipe[1],"A",1);  
        if(ret==-1)  
        {  
            printf("error %s\n",strerror(errno));  
            break;  
        }  
        count++;  
    }  
    printf("count=%d\n",count);  
    return 0;  
}  

結果如下:

error Resource temporarily unavailable
count=65536

可見pipe_buff有64k大小。這篇文章對於管道大小有著說明。

另一個需要注意的問題就是,只有在讀端存在的情況下,向管道中寫資料才有意義。如果讀端不存在了(壓根就沒有或者已經被關閉),那麼會產生訊號SIGPIPE,應用程式可以處理該訊號或者忽略(預設動作是使得應用程式終止)。

在我們之前,例子中,我們是一秒一寫,假設寫10次也就是至少10秒才能寫完,我們在10s之前將讀端關閉驗證我們所說的。

程式碼我們只在父程序中修改:

   if (pid < 0)
    {
        perror("fork failure\n");
        exit(2);
    }
    else if (pid == 0) //child
    {
        close(pe[0]); //close reading
        int i = 0;

        const char* message = "i am child\n";

        for (; i < TIMES; ++i)
        {
            write(pe[1], message, strlen(message));
            sleep(1);
        }

    }
    else // father
    {
        close(pe[1]); //close writing

        char buffer[100] = {'\0'};
        int i = 0;
        for (; i < TIMES / 3; ++i) //只讀取3次
        {
            read(pe[0], buffer, sizeof(buffer)); 
            printf("#child : %s", buffer);
        }
        close(pe[0]);//關閉父程序的讀端

        int status = 0;
        int ret = waitpid(pid, &status, 0);
        if (ret > 0)
        {
            if (WIFSIGNALED(status))
            {
                printf("signal is %d\n", WTERMSIG(status));
            }
            if (WIFEXITED(status))
            {
                printf("exitCode is %d\n", WEXITSTATUS(status));
            }
        }

    }

這裡寫圖片描述

匿名管道

我們在前面講過,匿名管道有一點不足就是隻能用於血緣關係的程序通訊,在命名管道(named pipe或FIFO)提出後,該限制得到了克服。FIFO不同於管道之處在於它提供⼀個路徑名與之關聯,以FIFO的檔案形式儲存於⽂檔案系統中。

命名管道是一個裝置檔案,因此,即使程序與建立FIFO的程序不存在親緣關係,只要可以訪問該路徑,就能夠通過FIFO相互通訊。值得注意的是,FIFO(first input first output)總是按照先進先出的原則工作,第⼀個被寫入的資料將首先從管道中讀出。

命名管道也被稱為FIFO檔案,它是一種特殊型別的檔案,它在檔案系統中以檔名的形式存在,但是它的行為卻和之前所講的沒有名字的管道(匿名管道)類似。

由於Linux中所有的事物都可被視為檔案,所以對命名管道的使⽤用也就變得與檔案操作非常的統一,也使它的使用非常方便,同時我們也可以像平常的檔名一樣在命令中使用。

Linux下有兩種方式建立命名管道。一是在Shell下互動地建⽴立⼀一個命名管道,二是在程式中使用系統函式建立命名管道。我們主要介紹第二種,關於系統程式設計方面在程式中使用。

這裡寫圖片描述

通過mkfifo建立命名管道,path為建立的命名管道的全路徑名:mod為建立的命名管道的模式,指明其存取許可權;建立成功返回0,否則發揮-1

#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>

int main()
{    
    umask(0);
    int ret = mkfifo("named_pipe", 0666 | S_IFIFO);                                                                                                                               
    if (ret == -1)
    {
        printf("mkfifo error\n");
        exit(-1);
    }

    return 0;
}    

在上面程式碼中,我們建立了命名管道,而它的使用方法和匿名管道基本類似。只是使用命名管道時,必須先呼叫open()將其開啟。因為命名管道是一個存在於硬碟上的檔案,而管道是存在於記憶體中的特殊檔案。

需要注意的是:呼叫open()開啟命名管道的程序可能會被阻塞。

但如果同時用讀寫方式(O_ RDWR)開啟,則一定不會導致阻塞;
如果以只讀方式(O_ RDONLY)開啟,則呼叫open()函式的程序將會被阻塞直到有寫方開啟管道;同樣以寫方式(O_WRONLY)開啟也會阻塞直到有讀方式開啟管道。