1. 程式人生 > >程序間通訊之Linux C管道程式設計

程序間通訊之Linux C管道程式設計

管道簡述

管道(pipe)是Unix/Linux中最常見的程序間通訊方式之一,它在兩個程序之間實現一個數據流通的通道,資料以一種資料流的方式在程序間流動。在系統中,管道相當於檔案系統上的一個檔案,用於快取所要傳輸的資料。在某些特性上又不同於檔案,例如當資料讀出後,管道中就沒有資料了,但檔案沒有這個特性。但管道具有兩個缺點:

·部分系統下的管道是半雙工的,資料在同一時間只能向一個方向流動。從實現的角度看,Linux核心採用環形緩衝區實現管道,如果允許同時讀寫,可能會導致資料衝突,Linux採用鎖機制來防止同時讀寫。

·管道通常來說只能在具有親屬關係(父子程序、兄弟程序)的程序間使用。

管道(也稱為匿名管道)是Linux中最古老的程序通訊機制,其應用非常廣泛,也為使用者在shell中提供了相應的管道操作符“|”。操作符“|”將其前後兩個命令連線到一起,前一個命令的輸出成為後一個命令的輸入,且可以支援使用多個“|”連線多個命令。

管道特點

·管道沒有名字,所以也稱為匿名管道

·管道是半雙工的,資料只能向一個方向流動,需要雙向通訊時,需要建立起兩個管道。

·只能用於父子程序或兄弟程序之間。

·管道對於其兩端的程序而言只是一個檔案,但它不是普通的檔案,它不屬於作業系統的某種檔案系統,而是單獨構成一種檔案系統,並且只存在於記憶體中。

·管道傳送的是無格式位元組流,這就要求使用管道的通訊雙方必須事先約定好資料的格式。

管道實現

/**
 *    struct pipe_buffer - a linux kernel pipe buffer
 *    @page: the page containing the data for the pipe buffer
 *    @offset: offset of data inside the @page
 *    @len: length of data inside the @page
 *    @ops: operations associated with this buffer. See @pipe_buf_operations.
 *    @flags: pipe buffer flags. See above.
 *    @private: private data owned by the ops.
 **/
struct pipe_buffer {
    struct page *page;
    unsigned int offset, len;
    const struct pipe_buf_operations *ops;
    unsigned int flags;
    unsigned long _private;
};


/**
 *    struct pipe_inode_info - a linux kernel pipe
 *    @mutex: mutex protecting the whole thing
 *    @wait: reader/writer wait point in case of empty/full pipe
 *    @nrbufs: the number of non-empty pipe buffers in this pipe
 *    @buffers: total number of buffers (should be a power of 2)
 *    @curbuf: the current pipe buffer entry
 *    @tmp_page: cached released page
 *    @readers: number of current readers of this pipe
 *    @writers: number of current writers of this pipe
 *    @files: number of struct file referring this pipe (protected by ->i_lock)
 *    @waiting_writers: number of writers blocked waiting for room
 *    @r_counter: reader counter
 *    @w_counter: writer counter
 *    @fasync_readers: reader side fasync
 *    @fasync_writers: writer side fasync
 *    @bufs: the circular array of pipe buffers
 *    @user: the user who created this pipe
 **/
struct pipe_inode_info {
    struct mutex mutex;
    wait_queue_head_t wait;
    unsigned int nrbufs, curbuf, buffers;
    unsigned int readers;
    unsigned int writers;
    unsigned int files;
    unsigned int waiting_writers;
    unsigned int r_counter;
    unsigned int w_counter;
    struct page *tmp_page;
    struct fasync_struct *fasync_readers;
    struct fasync_struct *fasync_writers;
    struct pipe_buffer *bufs;
    struct user_struct *user;
};

當程序建立一個匿名管道時,Linux核心為匿名管道準備了兩個檔案描述符:一個用於管道的輸入,即在管道中寫入資料;另一個用於管道的輸出,也就是從管道中讀出資料。抽象圖如下: 

匿名管道

如果一個管道只與一個程序相聯絡,只實現程序自身的內部通訊,則這個管道是毫無意義的。通常情況下,一個建立管道的程序接著就會建立其子程序,由於父子程序可以共享開啟檔案,子程序將從父程序那裡繼承到讀寫管道的檔案描述符,這樣便建立了父子程序之間的通訊管道。

由於管道是半雙工,因此在程式設計時使用管道,需要確定資料的傳輸方向,父子程序分別關閉與之無關的描述符。例如資料從子程序傳送到父程序,則子程序關閉讀管道的描述符。

管道的讀寫規則

由於管道是半雙工通訊,因此在進行相應操作時需要注意以下幾點:

·如果從一個寫描述符關閉的管道中讀資料,當讀完所有的資料後,read函式返回0,表明已到達檔案末尾。嚴格來說,只有當沒有資料繼續寫入後,才可以說達到了檔案末尾。所以應該分清到底是暫時沒有資料輸入還是已經到達檔案末尾。如果是前者,該程序應該等待。若為多程序寫、單程序讀的情況就更為複雜。

·如果向一個讀描述符關閉的管道中寫資料,就會產生SIGPIPE訊號,不管是忽略這個訊號還是處理它,write函式都返回-1。

·常數PIPE_BUF規定了核心中管道緩衝區的大小,因為管道被設計為環形緩衝區,所以在寫管道時要注意寫入的資料大小。如果超過規定大小就會產生交錯現象,從而導致資料丟失。

Linux的管道操作

建立

Linux核心提供了函式pipe用於建立一個管道,它接收一個長度為2的int型別陣列,用於核心寫入檔案描述符。fd[0]為讀出端的描述符,fd[1]為寫入端的描述符。以下示例程式碼在同一個程序中讀寫同一個管道,這並無意義,僅作為示例。

#include <iostream>
#include <cstdlib>
#include <unistd.h>

int main()
{
    int fd[2]; //建立檔案描述符陣列
    char writebuf[20] = {"Hello World!"}; //寫緩衝區
    char readbuf[20]; //讀緩衝區

    if (pipe(fd) < 0)
    {
        std::cerr << "建立管道失敗\n";
        exit(0);
    }
    write(fd[1], writebuf, sizeof(writebuf)); //向管道寫入端寫入資料
    write(fd[0], readbuf, sizeof(readbuf)); //向管道讀出端讀出資料
    std::cout << readbuf << "\n";
    std::cout << "管道的讀fd是" << fd[0] << "\n寫fd是" << fd[1] << std::endl;
    return 0;
}

管道通訊

管道的用途是為了在不同的程序中進行資料互動,但僅限於在有親屬關係的程序間使用,即父子程序、兄弟程序等。以下程式碼以父子程序間通訊為例。

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    int fd[2];
    pid_t pid;
    char buf[20];

    if (pipe(fd) < 0)
    {
        std::cerr << "建立管道失敗\n";
        exit(0);
    }

    if ((pid = fork()) < 0)
    {
        std::cerr << "建立子程序失敗\n";
        exit(0);
    }
    else if (pid > 0) //父程序
    {
        close(fd[0]); //關閉讀出端
        write(fd[1], "I'm your father.\n", 17);
    }
    else //子程序
    {
        close(fd[1]); //關閉寫入端
        read(fd[0], buf, sizeof(buf));
        std::cout << buf;
        exit(0);
    }

    if (waitpid(pid, NULL, 0) != pid)
    {
        cerr << "銷燬程序失敗\n";
    }
    exit(0);
}

管道高階應用

從上一段程式碼示例可看出,管道建立函式pipe通常是和程序建立函式fork或fork配合使用。Linux核心提供了將兩者“合二為一”的函式popen和pclose。

FILE *popen(const char *cmdstring, const char *type);
int pclose(FILE *stream);

與fopen和fclose函式一樣,popen和pclose必須配合使用。

函式popen用於建立管道,內部呼叫fork和exec函式執行第一個引數中指出的命令,返回一個FILE結構的指標,即用於訪問管道的指標。第二個引數則用於指出管道的型別,如果管道是以型別“r”開啟的,那麼這個管道的輸入端將連線到命令列cmdstring的標準輸出端,此時命令列的輸出可以從管道中讀入。反之,如果是以型別“w”開啟的,那麼這個管道的輸出端將連線到命令列的標準輸入端,此時向管道中寫入的資料就成為命令列的輸入資料。