Linux下進程間通信:管道-pipe函數

摘要:在本系列序中作者概述了 linux 進程間通信的幾種主要手段。其中管道和有名管道是最早的進程間通信機制之一,管道可用於具有親緣關係進程間的通信,有名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關係進程間的通信。 認清管道和有名管道的讀寫規則是在程序中應用它們的關鍵,本文在詳細討論了管道和有名管道的通信機制的基礎上,用實例對其讀寫規則進行了程序驗證,這樣做有利於增強讀者對讀寫規則的感性認識,同時也提供了應用範例。本文來自網絡,但原文出處難以查證。

管道相關的關鍵概念

管道是Linux支持的最初Unix IPC形式之一,具有以下特點:

  • 管道是半雙工的,數據只能向一個方向流動;需要雙方通信時,需要建立起兩個管道;
  • 只能用於父子進程或者兄弟進程之間(具有親緣關係的進程);
  • 單獨構成一種獨立的文件系統:管道對於管道兩端的進程而言,就是一個文件,但它不是普通的文件,它不屬於某種文件系統,而是自立門戶,單獨構成一種文件系統,並且只存在與內存中。
  • 數據的讀出和寫入:一個進程向管道中寫的內容被管道另一端的進程讀出。寫入的內容每次都添加在管道緩衝區的末尾,並且每次都是從緩衝區的頭部讀出數據。

管道的創建

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

返回:成功,0;失敗,-1

該函數創建的管道的兩端處於一個進程中間,在實際應用中沒有太大意義,因此,一個進程在由pipe()創建管道後,一般再fork一個子進程,然後通過管道實現父子進程間的通信(因此也不難推出,只要兩個進程中存在親緣關係,這裏的親緣關係指的是具有共同的祖先,都可以採用管道方式來進行通信)。

管道的讀寫規則

管道兩端可分別用描述字fd[0]以及fd[1]來描述,需要注意的是,管道的兩端是固定了任務的。即一端只能用於讀,由描述字fd[0]表示,稱其爲管道讀端;另一端則只能用於寫,由描述字fd[1]來表示,稱其爲管道寫端。如果試圖從管道寫端讀取數據,或者向管道讀端寫入數據都將導致錯誤發生。一般文件的I/O函數都可以用於管道,如close、read、write等等。

從管道中讀取數據:

  • 如果管道的寫端不存在,則認爲已經讀到了數據的末尾,讀函數返回的讀出字節數爲0;
  • 當管道的寫端存在時,如果請求的字節數目大於PIPE_BUF,則返回管道中現有的數據字節數,如果請求的字節數目不大於 PIPE_BUF,則返回管道中現有數據字節數(此時,管道中數據量小於請求的數據量);或者返回請求的字節數(此時,管道中數據量不小於請求的數據量)。注:(PIPE_BUF在include/linux/limits.h中定義,不同的內核版本可能會有所不同。Posix.1要求 PIPE_BUF至少爲512字節,red hat 7.2中爲4096)。

關於管道的讀規則驗證:

/************** * readtest.c * **************/
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
main()
{
        int pipe_fd[2];
        pid_t pid;
        char r_buf[100];
        char w_buf[4];
        char *p_wbuf;
        int r_num;
        int cmd;
        memset(r_buf, 0, sizeof(r_buf));
        memset(w_buf, 0, sizeof(r_buf));
        p_wbuf = w_buf;
        if(pipe(pipe_fd) < 0)
        {
                printf("pipe create error\n");
                return -1;
        }
        if((pid = fork()) == 0)
        {
                printf("\n");
                close(pipe_fd[1]);
                sleep(3);       //確保父進程關閉寫端
                r_num = read(pipe_fd[0], r_buf, 100);
                printf("read num is %d the data read from the pipe is %d\n", r_num, atoi(r_buf));
                close(pipe_fd[0]);
                exit();
        }
        else if(pid > 0)
        {
                close(pipe_fd[0]);//read
                strcpy(w_buf,"111");
                if(write(pipe_fd[1], w_buf, 4) != -1)
                        printf("parent write over\n");
                close(pipe_fd[1]);//write
                printf("parent close fd[1] over\n");
                sleep(10);
        }
}

程序輸出結果:
parent write over
parent close fd[1] over
read num is 4 the data read from the pipe is 111
附加結論:管道寫端關閉後,寫入的數據將一直存在,直到讀出爲止。

向管道中寫入數據:

  • 向管道中寫入數據時,linux將不保證寫入的原子性,管道緩衝區一有空閒區域,寫進程就會試圖向管道寫入數據。如果讀進程不讀走管道緩衝區中的數據,那麼寫操作將一直阻塞。
  • 只有在管道的讀端存在時,向管道中寫入數據纔有意義。否則,向管道中寫入數據的進程將收到內核傳來的 SIFPIPE信號,應用程序可以處理該信號,也可以忽略(默認動作則是應用程序終止)。

管道的寫規則的驗證一:

//寫端對讀端存在的依賴性

#include <unistd.h>
#include <sys/types.h>
main()
{
        int pipe_fd[2];
        pid_t pid;
        char r_buf[4];
        char *w_buf;
        int writenum;
        int cmd;
        memset(r_buf, 0, sizeof(r_buf));
        if(pipe(pipe_fd) < 0)
        {
                printf("pipe create error\n");
                return -1;
        }

        if((pid = fork()) == 0)
        {//子進程
                close(pipe_fd[0]);//讀端關閉
                close(pipe_fd[1]);//寫端關閉
                sleep(10);
                exit();
        } else if(pid > 0) {
                //父進程
                sleep(1);
                close(pipe_fd[0]);
                w_buf="111";
                if((writenum = write(pipe_fd[1], w_buf, 4)) == -1)
                        printf("write to pipe error\n");
                else
                        printf("the bytes write to pipe is %d \n", writenum);
                close(pipe_fd[1]);
        }
}

則輸出結果爲: Broken pipe,原因就是該管道以及它的所有fork()產物的讀端都已經被關閉。如果在父進程中保留讀端,即在寫完pipe後,再關閉父進程的讀端,也會正常寫入pipe,讀者可自己驗證一下該結論。因此,在向管道寫入數據時,至少應該存在某一個進程,其中管道讀端沒有被關閉,否則就會出現上述錯誤(管道斷裂,進程收到了SIGPIPE信號,默認動作是進程終止)

對管道的寫規則的驗證二:

//linux不保證寫管道的原子性驗證

#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
main(int argc, char **argv)
{
        int pipe_fd[2];
        pid_t pid;
        char r_buf[4096];
        char w_buf[4096 * 2];
        int writenum;
        int rnum;
        memset(r_buf, 0, sizeof(r_buf));
        if(pipe(pipe_fd) < 0)
        {
                printf("pipe create error\n");
                return -1;
        }
        if((pid = fork()) == 0)
        {
                close(pipe_fd[1]);
                while(1)
                {
                        sleep(1);
                        rnum = read(pipe_fd[0], r_buf, 1000);
                        printf("child: readnum is %d\n", rnum);
                }
                close(pipe_fd[0]);
                exit();
        }
        else if(pid > 0)
        {
                close(pipe_fd[0]);      //write
                memset(r_buf, 0, sizeof(r_buf));
                if((writenum = write(pipe_fd[1], w_buf, 1024)) == -1)
                        printf("write to pipe error\n");
                else
                        printf("the bytes write to pipe is %d \n", writenum);
                writenum = write(pipe_fd[1], w_buf, 4096);
                close(pipe_fd[1]);
        }
}

輸出結果: the bytes write to pipe 1000 the bytes write to pipe 1000 ,此行輸出說明了寫入的非原子性
the bytes write to pipe 1000 the bytes write to pipe 1000 the bytes write to pipe 1000 the bytes write to pipe 120 ,此行輸出說明了寫入的非原子性
the bytes write to pipe 0 the bytes write to pipe 0 ......

結論: 寫入數目小於4096時寫入是非原子的!

如果把父進程中的兩次寫入字節數都改爲5000,則很容易得出下面結論:寫入管道的數據量大於4096字節時,緩衝區的空閒空間將被寫入數據(補齊),直到寫完所有數據爲止,如果沒有進程讀數據,則一直阻塞。

管道應用實例

用於shell

管道可用於輸入輸出重定向,它將一個命令的輸出直接定向到另一個命令的輸入。比如,當在某個shell程序(Bourne shell或C shell等)鍵入who│wc -l後,相應shell程序將創建who以及wc兩個進程和這兩個進程間的管道。考慮下面的命令行:

$kill -| grep SIGRTMIN
運行結果如下:
30) SIGPWR
31) SIGSYS
32) SIGRTMIN
33) SIGRTMIN+1
34) SIGRTMIN+2
35) SIGRTMIN+3
36) SIGRTMIN+4
37) SIGRTMIN+5
38) SIGRTMIN+6
39) SIGRTMIN+7
40) SIGRTMIN+8
41) SIGRTMIN+9
42) SIGRTMIN+10
43) SIGRTMIN+11
44) SIGRTMIN+12
45) SIGRTMIN+13
46) SIGRTMIN+14
47) SIGRTMIN+15
48) SIGRTMAX-15
49) SIGRTMAX-14

用於具有親緣關係的進程間通信

下面例子給出了管道的具體應用,父進程通過管道發送一些命令給子進程,子進程解析命令,並根據命令作相應處理。

#include <unistd.h>
#include <sys/types.h>
main()
{
        int pipe_fd[2];
        pid_t pid;
        char r_buf[4];
        char **w_buf[256];
        int childexit = 0;
        int i;
        int cmd;
        memset(r_buf, 0, sizeof(r_buf));
        if(pipe(pipe_fd) < 0)
        {
                printf("pipe create error\n");
                return -1;
        }
        if((pid=fork())==0)
        { //子進程:解析從管道中獲取的命令,並作相應的處理
                printf("\n");
                close(pipe_fd[1]);
                sleep(2);
                while(!childexit)
                {
                        read(pipe_fd[0], r_buf, 4);
                        cmd = atoi(r_buf);
                        if(cmd == 0)
                        {
                                printf("child: receive command from parent over\n now child process exit\n");
                                childexit = 1;
                        }
                        else if(handle_cmd(cmd) != 0)
                                return;
                        sleep(1);
                }
                close(pipe_fd[0]);
                exit();
        }
        else if(pid > 0)
        { //父進程:發送命令給子進程
                close(pipe_fd[0]);
                w_buf[0] = "003";
                w_buf[1] = "005";
                w_buf[2] = "777";
                w_buf[3] = "000";
                for(= 0; i < 4; i++)
                        write(pipe_fd[1], w_buf[i], 4);
                close(pipe_fd[1]);
        }
}

//下面是子進程的命令處理函數(特定於應用)
int handle_cmd(int cmd)
{
        if((cmd < 0) || (cmd > 256))//suppose child only support 256 commands
        {
                printf("child: invalid command \n");
                return -1;
        }
        printf("child: the cmd from parent is %d\n", cmd);
        return 0;
}

管道的侷限性

管道的主要侷限性正體現在它的特點上:

  • 只支持單向數據流;
  • 只能用於具有親緣關係的進程之間;
  • 沒有名字;
  • 管道的緩衝區是有限的(管道制存在於內存中,在管道創建時,爲緩衝區分配一個頁面大小);
  • 管道所傳送的是無格式字節流,這就要求管道的讀出方和寫入方必須事先約定好數據的格式,比如多少字節算作一個消息(或命令、或記錄)等等;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章