天天看點

程序間通信——匿名管道原理及詳解(附有案例代碼)

1、定義

       管道也叫無名(匿名)管道,它是是UNIX系統IPC(程序間通信)的最古老形式,所有的UNIX系統都支援這種通信機制。

       統計一個目錄中檔案的數目指令: ls | wc -l,為了執行該指令,shell 建立了兩個程序來分别執行ls 和wc;通常情況下,程序 ls 的輸出直接通過 stdout 輸出到控制台,但是為了兩個程序能夠進行通信,系統會建立一個管道,然後把程序 ls 發的内容輸出到管道,程序 wc 從管道中讀取程序 ls 輸出的内容,進而實作程序間的通信。

程式間通信——匿名管道原理及詳解(附有案例代碼)

2、特點

A.管道其實是一個在核心記憶體中維護的緩沖器,這個緩沖器的存儲能力是有限的,不同的作業系統大小不一定相同。

程式間通信——匿名管道原理及詳解(附有案例代碼)

B.管道擁有檔案的特質:讀操作、寫操作,匿名管道沒有檔案實體,有名管道有檔案實體,但不存儲資料。可以按照操作檔案的方式對管道進行操作。

C.一個管道是一個位元組流,使用管道時不存在消息或者消息邊界的概念,從管道讀資料的程序可以讀取任意大小的資料塊,而不管寫入程序寫入管道的資料塊的大小是多少。通過管道傳遞的資料是順序的,從管道中讀取出來的位元組的順序和它們被寫入管道的順序是完全一樣的。

D.在管道中資料的傳遞方向是單向的,一端用于寫入,一端用于讀取,管道是半雙工的(半雙工可以了解為獨木橋,可以雙向傳遞但是同一時刻隻能單向傳遞)。從管道讀資料是一次性操作,資料一旦被讀走,它就從管道中被抛棄,釋放空間以便寫更多的資料,在管道中無法随機的通路資料。

E.匿名管道隻能在具有公共祖先的程序(父程序與子程序,或者兩個兄弟程序,具有親緣關系)之間使用。原因:fork出來的子程序中檔案描述符和父程序中的檔案描述符一樣,即讀寫描述符一一對應。

程式間通信——匿名管道原理及詳解(附有案例代碼)

 F.管道資料秉承先進先出原則,可以了解為一個環形隊列,之是以是環形,因為隊頭資料讀出後,還可以寫入新資料,不至于記憶體浪費。 

程式間通信——匿名管道原理及詳解(附有案例代碼)

3、匿名管道的使用 

建立匿名管道
#include <unistd.h>
int pipe(int pipefd[2]);

功能:建立一個匿名管道,用來程序間通信。
參數:int pipefd[2] 這個數組是一個傳出參數。
     pipefd[0] 對應的是管道的讀端
     pipefd[1] 對應的是管道的寫端
傳回值:
     成功 0
     失敗 -1

管道預設是阻塞的:如果管道中沒有資料,read阻塞,如果管道滿了,write阻塞

注意:匿名管道隻能用于具有關系的程序之間的通信(父子程序,兄弟程序)

檢視管道緩沖大小指令
ulimit -a

檢視管道緩沖大小函數
#include <unistd.h>
long fpathconf(int fd, int name);
           

4、匿名管道通信案例 

說明:由于匿名管道是半雙工的,是以同一時刻讀端和寫端隻能有一個開啟,另一個需要手動關閉; 

// 子程序發送資料給父程序,父程序讀取到資料輸出
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    // 在fork之前建立管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if(ret == -1) {
        perror("pipe");
        exit(0);
    }

    // 建立子程序
    pid_t pid = fork();

    if(pid > 0) {
        // 父程序
        printf("i am parent process, pid : %d\n", getpid());

        // 關閉寫端,同一時刻寫端、讀端隻能有一個開啟
        close(pipefd[1]);        
        
        // 從管道的讀取端讀取資料
        char buf[1024] = {0};

        while(1) {
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("parent recv : %s, pid : %d\n", buf, getpid());
            
            // 向管道中寫入資料
            //char * str = "hello,i am parent";
            //write(pipefd[1], str, strlen(str));
            //sleep(1);
        }

    } else if(pid == 0){
        // 子程序
        printf("i am child process, pid : %d\n", getpid());

        // 關閉讀端,同一時刻寫端、讀端隻能有一個開啟
        close(pipefd[0]);
        char buf[1024] = {0};

        while(1) {
            // 向管道中寫入資料
            char * str = "hello,i am child";
            write(pipefd[1], str, strlen(str));

            // int len = read(pipefd[0], buf, sizeof(buf));
            // printf("child recv : %s, pid : %d\n", buf, getpid());
            // bzero(buf, 1024);
        }
    }
    return 0;
}
           

5、注意事項

1.所有的指向管道寫端的檔案描述符都關閉了(管道寫端引用計數為0),有程序從管道的讀端

讀資料,那麼管道中剩餘的資料被讀取以後,再次read會傳回0,就像讀到檔案末尾一樣。

2.如果有指向管道寫端的檔案描述符沒有關閉(管道的寫端引用計數大于0),而持有管道寫端的程序也沒有往管道中寫資料,這個時候有程序從管道中讀取資料,那麼管道中剩餘的資料被讀取後,再次read會阻塞,直到管道中有資料可以讀了才讀取資料并傳回。

3.如果所有指向管道讀端的檔案描述符都關閉了(管道的讀端引用計數為0),這個時候有程序向管道中寫資料,那麼該程序會收到一個信号SIGPIPE, 通常會導緻程序異常終止。

4.如果有指向管道讀端的檔案描述符沒有關閉(管道的讀端引用計數大于0),而持有管道讀端的程序也沒有從管道中讀資料,這時有程序向管道中寫資料,那麼在管道被寫滿的時候再次write會阻塞,直到管道中有空位置才能再次寫入資料并傳回。

6、總結

    讀管道:

        管道中有資料,read傳回實際讀到的位元組數。

        管道中無資料:

            寫端被全部關閉,read傳回0(相當于讀到檔案的末尾)

            寫端沒有完全關閉,read阻塞等待

    寫管道:

        管道讀端全部被關閉,程序異常終止(程序收到SIGPIPE信号)

        管道讀端沒有全部關閉:

            管道已滿,write阻塞

            管道沒有滿,write将資料寫入,并傳回實際寫入的位元組數

認為設定管道非阻塞

int flags = fcntl(fd[0], F_GETFL);  // 擷取原來的flag
flags |= O_NONBLOCK;            // 修改flag的值
fcntl(fd[0], F_SETFL, flags);   // 設定新的flag
           

設定非阻塞案例:

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
/*
    設定管道非阻塞
    int flags = fcntl(fd[0], F_GETFL);  // 擷取原來的flag
    flags |= O_NONBLOCK;            // 修改flag的值
    fcntl(fd[0], F_SETFL, flags);   // 設定新的flag
*/
int main() {

    // 在fork之前建立管道
    int pipefd[2];
    int ret = pipe(pipefd);
    if(ret == -1) {
        perror("pipe");
        exit(0);
    }

    // 建立子程序
    pid_t pid = fork();
    if(pid > 0) {
        // 父程序
        printf("i am parent process, pid : %d\n", getpid());

        // 關閉寫端
        close(pipefd[1]);
        
        // 從管道的讀取端讀取資料
        char buf[1024] = {0};

        int flags = fcntl(pipefd[0], F_GETFL);  // 擷取原來的flag
        flags |= O_NONBLOCK;            // 修改flag的值
        fcntl(pipefd[0], F_SETFL, flags);   // 設定新的flag

        while(1) {
            int len = read(pipefd[0], buf, sizeof(buf));
            printf("len : %d\n", len);
            printf("parent recv : %s, pid : %d\n", buf, getpid());
            memset(buf, 0, 1024);
            sleep(1);
        }

    } else if(pid == 0){
        // 子程序
        printf("i am child process, pid : %d\n", getpid());
        // 關閉讀端
        close(pipefd[0]);
        char buf[1024] = {0};
        while(1) {
            // 向管道中寫入資料
            char * str = "hello,i am child";
            write(pipefd[1], str, strlen(str));
            sleep(5);
        }
        
    }
    return 0;
}
           

繼續閱讀