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

程序通訊(1)管道

程序之間通訊的方式有很多種,主要包括

  1. 管道
  2. 命名管道
  3. 訊號
  4. 訊息佇列
  5. 共享記憶體
  6. 訊號量
  7. 套接字

其中,管道是最早的一種程序間通訊機制,主要適用於具有親緣關係之間的程序間通訊,比如,父程序與子程序之間,或者同一個父程序的兩個子程序之間。同時,管道是一中半雙工的通訊,資料只能單向流動,從一段寫入,另外一段讀出。下面通過幾個例子來看一下管道如何使用。

1. 函式原型

 #include <unistd.h>
int pipe(int fd[2]);

pipe函式建立一個管道,其宣告在unistd.h當中,傳入引數是一個int[2]陣列,返回值如果為0表示,pipe建立成功,同時fd陣列中儲存兩個檔案描述符,fd[1]指向管道的寫端,fd[0]指向管道的讀端;如果小於0,表示建立失敗。

2. 第一個例子

下面看一個最簡單的例子

#include<unistd.h>
#include<stdio.h>
int main()
{
    int n ;
    int fd[2];
    char buf[1024];
    // 建立管道
    if (pipe(fd) < 0) {
        perror("pipe error");
    }
    write(fd[1], "hello world\n", 12);
    n = read(fd[0], buf, 1024);
    printf("%s",buf);
    return 0;
}

上面的程式碼很簡單,就是建立了一個管道,然後向寫端寫入“hello world\n”字串,然後從讀端讀出,儲存到buf陣列中,最後列印到螢幕。這個例子可以用下面的示意圖來表示:
在這裡插入圖片描述

管道像一根單向的水管,資料像水一樣從一端流入,從另一端流出。管道也是有緩衝空間的,如果一直寫入不讀取,那麼緩衝空間會被佔滿,再往裡面寫資料就會失敗(就像水管的流出端被關閉,水不能再流入一樣),同樣的,如果只讀不寫,那麼資料被讀完之後,就沒有東西可讀了,再次讀取也會失敗。這個例子顯然是沒什麼用途的,但是可以幫助我們理解什麼是管道。

2. 第二個例子 程序間通訊

再來看第二個例子

#include<unistd.h>
#include<stdio.h>
int main()
{
    int n ;
    int status;
    pid_t pid;
    int fd[2];
    char buf[1024];
    // 建立管道
    if (pipe(fd) < 0) {
        perror("pipe error");
    }
    // 建立子程序
    if ((pid = fork()) < 0) {
        perror("fork error");
    } else if (pid == 0) {
        // 子程序讀取管道
        n = read(fd[0], buf, 1024);  
        printf("%s",buf);
    } else {
       // 父程序寫入管道
        write(fd[1], "hello world\n", 12);
        // 父程序等待子程序結束
        if (wait(&status) < 0) {
            perror("wait error");
        }
    }
    return 0;
}

第二個例子比第一個例子稍微複雜了一些。首先,父程序建立了一個管道,然後fork出一個子程序。在子程序中讀取管道內容,並列印內容,然後返回結束程序;在父程序中向管道寫入字串,然後等待子程序結束,最後結束程序。這個例子可以用下面的圖來表示
在這裡插入圖片描述
執行fork之後,主程序建立了一個子程序,子程序完全複製父程序的虛擬記憶體空間(這個說法其實不嚴謹,見後文),同時繼承父程序開啟的檔案等資源,所以兩個描述符也被繼承下來,兩個程序的fd陣列具有相同的值,並且指向同樣的pipe埠。因此,父子程序之前可以通過pipe實現通訊。一般地,我們會在pipe寫入端程序關閉讀端,在pipe讀入端關閉寫端。在上面的程式碼中,加入兩行

#include<unistd.h>
#include<stdio.h>
int main()
{
    int n ;
    int status;
    pid_t pid;
    int fd[2];
    char buf[1024];
    // 建立管道
    if (pipe(fd) < 0) {
        perror("pipe error");
    }
    // 建立子程序
    if ((pid = fork()) < 0) {
        perror("fork error");
    } else if (pid == 0) {
    	// 關閉寫入端檔案描述符
    	close(fd[1]);
        // 子程序讀取管道
        n = read(fd[0], buf, 1024);  
        printf("%s",buf);
    } else {
    	// 關閉寫入端檔案描述符
    	close(fd[0]);
       // 父程序寫入管道
        write(fd[1], "hello world\n", 12);
        // 父程序等待子程序結束
        if (wait(&status) < 0) {
            perror("wait error");
        }
    }
    return 0;
}

那麼程序模型變成這樣
在這裡插入圖片描述

3. 第三個例子,管道與重定向

第三個例子,我們把子程序的標準輸入重定向到管道的讀埠

#include<unistd.h>
#include<stdio.h>
int main()
{
    int n ;
    int status;
    pid_t pid;
    int fd[2];
    char buf[1024];
    if (pipe(fd) < 0) {
        perror("pipe error");
    }
    if ((pid = fork()) < 0) {
        perror("fork error");
    } else if (pid == 0) {
        close(fd[1]);
        if (fd[0] != STDIN_FILENO)  {
        	// 把stdin重定向到管道的輸入端
            if (dup2(fd[0], STDIN_FILENO) != STDIN_FILENO) {
                perror("dup2 error");
            }
            // 重定向成功的話,那麼讀端就有了兩個檔案描述符,
            //分別是STDIN_FILENO和fd[0],此時可以關閉fd[0],保留STDIN_FILENO即可
            close(fd[0]);
        }
        //此時可以通過STDIN_FILENO讀取管道內容
        n = read(STDIN_FILENO, buf, 1024);  
        printf("%s",buf);
    } else {
        close(fd[0]);
        write(fd[1], "hello world\n", 12);
        if (wait(&status) < 0) {
            perror("wait error");
        }
    }
    return 0;
}

TIPS:
主程序建立了一個子程序,子程序完全複製父程序的虛擬記憶體空間這個說法其實是不嚴謹的,父子程序的程式碼段實際上是共用的,另外完全複製父程序的虛擬記憶體空間,會造成時間和記憶體上的浪費,有的時候根本沒有必要完全複製,因此出現了cow(copy on write)技術,也就是“寫時複製”,就是當子程序對某個變數進行寫操作時,才進行復制。這個技術對使用者程式是不可見的,因此,在使用者程式層面上,認為子程序完全複製父程序的虛擬記憶體空間是完全沒有問題的。