1. 程式人生 > >33-程序間通訊——管道

33-程序間通訊——管道

1. 什麼是管道

  管道是unix系統最古老的IPC通訊方式了,適合於有血緣關係的程序之間完成資料傳輸,比如父子程序,兄弟程序。

在這裡插入圖片描述

   管道允許一個數據流向另一個程序,管道中的資料流向是單向的。這樣程序可以通過檔案描述符1連線到管道寫入端,另一個程序通過檔案描述符0連線到管道讀取端,實際上,這兩個程序並不知道管道的存在,它們只是通過檔案描述符中讀寫資料,所以管道也稱為匿名管道。

2. 管道的通訊方式

   管道的資料通訊是單向的,採用半雙工通訊方式。

   對於半雙工通訊來說,典型的例子就是對講機,比如:當我說話的時候,你不能說話,只能聽;而當你說話的時候,我也只能聽,只有當你說完了,我才能說話,通常是一問一答的形式。也就是說,管道可以進行雙向通訊,但同一時刻資料只能進行單向通訊。

3. 建立管道

   pipe函式用於建立一個管道,本質上管道是程序核心空間建立的一個緩衝區。

<unistd.h>    
int pipe(int pipefd[2]);		

返回值說明:成功返回0;失敗則返回-1,並設定errno

pipefd引數:pipefd陣列用於儲存pipe函式呼叫成功返回讀端和寫端的兩個檔案描述符。pipefd[0]表示管道的讀端, pipefd[1]表示管道的寫端,可以通過這兩個檔案描述符向管道進行讀寫資料操作。

4. 用管道進行程序通訊

呼叫pipe建立管道成功後,呼叫pipe函式的程序會同時掌握管道的讀端和寫端,如下圖所示:

在這裡插入圖片描述

   父程序呼叫pipe函式建立管道,得到兩個檔案描述符fd[0]、fd[1]指向管道的讀端和寫端,但是系統是不允許一個程序同時讀寫的,因為管道是採用半雙工通訊,一個程序只能同一時刻讀(寫),另一個程序寫(讀),所以父程序只能通過呼叫fork來建立子程序。

在這裡插入圖片描述

   父程序呼叫fork建立子程序,那麼子程序也有兩個檔案描述符指向同一管道,這樣顯然是不合理的。父程序應該關閉管道讀端,子程序應該關閉管道寫端。然後父程序向管道中寫入資料,子程序將管道中的資料讀出,這樣就實現了程序間通訊。

程序間通訊實驗:

#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
#include <errno.h>

int main(void) {
        int
fd[2]; int ret; pid_t pid; char buf[1024]; //呼叫pipe函式,建立管道 ret = pipe(fd); if (ret == -1){ perror("pipe error:"); } //建立子程序 pid = fork(); if (pid == -1){ perror("fork error"); } //父程序 else if (pid > 0) { //關閉父程序的讀端 close(fd[0]); read(STDIN_FILENO , buf , sizeof(buf)); //往管道寫資料 write(fd[1], buf, strlen(buf)); //父程序阻塞等待回收子程序 wait(NULL); //關閉寫端 close(fd[1]); } else if (pid == 0) { //關閉子程序寫端 close(fd[1]); //從管道讀資料 ret = read(fd[0], buf, sizeof(buf)); printf("------------\n"); write(STDOUT_FILENO, buf, ret); //關閉讀端 close(fd[0]); } return 0; }

程式執行結果: 在這裡插入圖片描述

5. 為何要關閉未使用的管道檔案描述符

其實,關閉未使用的管道檔案描述符主要有兩個原因:

  1. 防止程序的檔案描述符耗盡
  2. 保證正確的使用管道進行程序間通訊

   通常,從管道中讀取資料的程序會關閉其持有的管道寫端檔案描述符,而往管道寫資料的程序會關閉其持有的讀端檔案描述符。

   這樣的話,那麼當所有指向管道寫端的檔案描述符全部都被關閉時,然後程序從管道讀完剩餘的所有資料後,read將會返回0,表示讀到檔案末尾。

   假設該程序在建立管道後,沒有關閉寫端檔案描述符,那麼即便程序從管道中讀完剩餘的所有資料後,read依然不會返回0(不會讀到檔案末尾),而是阻塞等待資料到來,這是因為核心發現管道的寫端檔案描述符並未全部被關閉,認為將來會有資料寫入管道。

   如果所有指向管道讀端的檔案描述符全部都關閉了,此時仍然有程序向管道寫入資料的話,那麼該程序會收到訊號SIGPIPE(管道已損壞),通常會導致程序異常終止,write返回返回EPIPE錯誤。

   下面通過兩個實驗來驗證這兩個問題。

5.1 未關閉未使用的管道檔案描述符實驗一

一個程序建立管道後,沒有關閉未使用的管道檔案描述符,然後對管道進行讀寫。

#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
#include <errno.h>

int main(void) {
        int fd[2]; 
        int ret; 
        char buf1[64]; 
        char buf2[64];

        //呼叫pipe函式,建立管道
        ret = pipe(fd);
        if (ret == -1){
                //建立失敗
                perror("pipe error:");
        }
        read(STDIN_FILENO , buf1 , sizeof(buf1));
        //小寫轉大寫
        int i;
        for(i = 0; i < strlen(buf1); i++){
                buf1[i] = toupper(buf1[i]);
        }
        //往管道寫資料
        write(fd[1], buf1, strlen(buf1));
        //從管道讀資料
        read(fd[0] , buf2 , sizeof(buf2));

        //寫到標準輸出
        write(STDOUT_FILENO , buf2 , strlen(buf2));
        //再次從管道中讀取資料,此時會阻塞
        read(fd[0] , buf2 , sizeof(buf2));
        return 0;
}

程式執行結果: 在這裡插入圖片描述

從程式的執行結果來看,此時程序已經阻塞在read處,正等待管道中有資料到來。

5.2 未關閉未使用的管道檔案描述符實驗二

#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
#include <errno.h>
#include <signal.h>

//SIGPIPE訊號處理函式
void sig_handler(int sig){
        if(sig == SIGPIPE){
                puts("catch SIGPIPE");
        }
}

int main(void) {
        int fd[2]; 
        int ret; 
        pid_t pid; 
        char buf1[64];
        char buf2[64];

        //呼叫pipe函式,建立管道
        ret = pipe(fd);
        if (ret == -1){
                //建立失敗
                perror("pipe error:");
        }

        //註冊SIGPIPE訊號
        signal(SIGPIPE , sig_handler);
        //fork子程序
        pid = fork();
        if (pid == -1){
                //fork失敗
                perror("fork error");
        }

        //父程序
        else if (pid > 0) {
                //關閉父程序的讀端
                close(fd[0]);
                
                read(STDIN_FILENO , buf1 , sizeof(buf1));
                //往管道寫資料
                write(fd[1], buf1, strlen(buf1));
                //保證子程序搶到cpu
                sleep(1);

                //第二次往管道寫資料
                ret = write(fd[1] , buf1 , strlen(buf1));
                if(ret < 0){
                        //判斷對方讀端是否以關閉,導致EPIPE錯誤
                        if(errno == EPIPE){
                                puts("peer is close");
                        }
                }

                //父程序阻塞等待回收子程序
                wait(NULL);
                //關閉寫端
                close(fd[1]);
                
        } else if (pid == 0) {
                //關閉子程序寫端
                close(fd[1]);
                //從管道讀資料
                ret = read(fd[0], buf2, sizeof(buf2)); 
				//小寫轉大寫
                int i;
                for(i = 0; i < ret; i++){
                        buf2[i] = toupper(buf2[i]);
                }
                printf("------------\n");
                write(STDOUT_FILENO, buf2, ret);
                //模擬讀端關閉
                close(fd[0]);
                //休眠2秒
                sleep(2);
        }
        return 0;
}

程式執行結果: 在這裡插入圖片描述

   從程式的執行結果來看,如果管道的讀端已經關閉,那麼寫端繼續寫入的話就會收到SIGPIPE訊號,導致程序終止。

   通過這兩個實驗相信你已經明白了為什麼要關閉未使用的管道讀端檔案描述符的原因,因為有時候我們需要這種錯誤來判斷管道的狀態,如果未關閉這些未使用的管道檔案描述符的話,這可能會導致程序間無法正確使用管道進行通訊。

6. 管道中的資料流

   管道中的資料基本都是位元組流形式的,這意味著管道中沒有訊息或訊息邊界的概念,程序從管道中可以讀取任意大小的資料,同理,往管道中寫入資料也可以寫入任意大小的資料。

   另外管道中的資料傳輸是順序的,從管道中讀取資料必須按照一定的順序讀取資料,資料一旦被讀走就不存在了,往管道寫資料同理,每次新寫入的資料都會加入到管道末尾。換句話說,使用管道通訊的話不能呼叫lseek函式來隨機讀寫資料,因為管道本質上是核心的一塊記憶體,管道中的資料都是以一定順序讀寫的

   舉個例子:比如往管道中寫入hello world這樣的字串,寫入資料的順序是先寫入hello,再寫入world,然後從管道中讀取資料的順序是先讀取hello,再讀world。也就是說,讀和寫的順序是一樣的(類似於網路程式設計中的socket通訊)。

7. 總結

優點:    跟其他IPC通訊方式相比,管道使用相對簡單

缺點:

  1. 管道只能單向的通訊
  2. 只能用父子程序間通訊
  3. 管道中的資料一旦被讀走就不存在了,不可反覆讀取