天天看點

Linux程序通信之管道和FIFO

Linux程序間的通信可以簡稱為IPC(Interprocess Communication),前面說過的 Linux的同步工具也是屬于IPC的一部分,這裡我想說的是通常意義的程序間的實際資料通。

1管道

管道是最早的UNIX IPC,所有的UNIX系統都支援這個IPC通信機制。我們最常見到使用它的位置就是shell中使用的管道指令。管道IPC有兩個特性:

  • 管道僅提供半雙工的資料通信,即隻支援單向的資料流。
  • 管道隻能在有親緣關系的程序間使用。這是由于管道沒有名字的原因,是以不能跨程序的位址空間進行使用。這裡這句話不是絕對的,因為從技術上可以在程序間傳遞管道的描述符,是以是可以通過管道實作無親緣程序間的通信的。但盡管如此,管道還是通常用于具有共同祖先的程序間的通信。

管道的接口定義如下:

#include <unistd.h>
int pipe(int filedes[2]);
                       //成功傳回0,失敗傳回-1
           

pipe函數用來建立一個管道,fd是傳出參數,用于儲存傳回的兩個檔案描述符,該檔案描述符用于辨別管道的兩端,fd[0]隻能由于讀,fd[1]隻能用于寫。

那麼如果我們往fd[0]端寫資料會是什麼樣的結果呢?下面是測試代碼:

#include <iostream>
#include <cstring>

#include <unistd.h>
#include <errno.h>

using namespace std;

int main()
{
    int fd[2];

    if (pipe(fd) < 0)
    {
        cout<<"create pipe failed..."<<endl;
        return -1;
    }

    char *temp = "yuki";

    if (write(fd[0], temp, strlen(temp) + 1) < 0)
    {
        cout<<"write pipe failed:"<<strerror(errno)<<endl;
    }

    return 0;
}
           

代碼的執行結果如下:

write pipe failed:Bad file descriptor
           

從這個結果可以看出,核心對于管道的fd[0]描述符打開的方式是以隻讀方式打開的,那麼同理fd[1]是以隻寫方式打開的,是以管道隻能保證單向的資料通信。

下圖1顯示的是一個程序内的管道的模樣:

Linux程式通信之管道和FIFO

圖1單個程序内管道的模樣

從上圖我們可以看到位于核心中的管道,程序通過兩個檔案描述符進行資料的傳輸,當然單個程序内的管道是沒有必要的,上面隻是為了更形象的表明管道的工作方式,一般管道的使用方式都是:父程序建立一個管道,然後fork産生一個子程序,由于子程序繼承父程序打開的檔案描述符,是以父子程序可以通過管道程序通信。這種使用方式如下圖2所示:

Linux程式通信之管道和FIFO

圖2父子程序間的管道

如上圖所示,當父程序通過fork建立子程序後,父子程序都擁有對管道操作的檔案描述符,此時父子程序關閉對應的讀寫端,使父子程序間形成單向的管道。關閉哪個端要根據具體的資料流向決定。

1.1父子程序間的單向通信

上面說了父程序通過fork建立子程序後,父子程序間可以通過管道通信,資料流的方向根據具體的應用決定。我們都知道在shell中,管道的資料流向都是從父程序流向子程序,即父程序關閉讀端,子程序關閉寫端。如下圖3所示:

Linux程式通信之管道和FIFO

圖3 父子程序間的單向管道

上圖的測試代碼如下:

#include <iostream>

#include <unistd.h>

using namespace std;

int main()
{
    int fd[2];

    if (pipe(fd) < 0)
    {
        cout<<"create pipe failed..."<<endl;
        return -1;
    }

    char buf[256];

    if (fork() == 0)
    {
        close(fd[1]);

        read(fd[0], buf, sizeof(buf));
        cout<<"receive message from pipe:"<<buf<<endl;

        exit(0);
    }

    close(fd[0]);

    char *temp = "I have liked yuki...";
    write(fd[1], temp, strlen(temp) + 1);

    return 0;
}
           

代碼的執行結果如下:

receive message from pipe:I have liked yuki...
           

其中代碼流程是,子程序等待父程序通過管道發送過來的資料,然後輸出接收到的資料,代碼中的read會阻塞到管道中有資料為止,具體管道的read和write的規則将會在後面介紹。

1.2父子程序間的雙向通信

由上我們知道,一個管道隻能支援親緣程序間的單向通信即半雙工通信。如果要想通過管道來支援雙向通信呢,那這裡就需要建立兩個管道,fd1,fd2;父程序中關閉fd1[0],fd2[1],子程序中關閉fd1[1],fd2[0]。這種通信模式如下圖所示:

Linux程式通信之管道和FIFO

圖 4 父子程序間的雙向通信

下面是雙向通信的測試代碼:

#include <iostream>

#include <unistd.h>

using namespace std;

int main()
{
    int fd1[2], fd2[2];

    if (pipe(fd1) < 0 || pipe(fd2) < 0)
    {
        cout<<"create pipe failed..."<<endl;
        return -1;
    }

    char buf[256];
    char *temp = "I have liked yuki...";

    if (fork() == 0)
    {
        close(fd1[1]);
        close(fd2[0]);

        read(fd1[0], buf, sizeof(buf));
        cout<<"child:receive message from pipe 1:"<<buf<<endl;

        write(fd2[1], temp, strlen(temp) + 1);
        exit(0);
    }

    close(fd1[0]);
    close(fd2[1]);

    write(fd1[1], temp, strlen(temp) + 1);
    read(fd2[0], buf, sizeof(buf));
    cout<<"parent:receive message from pipe 2:"<<buf<<endl;

    return 0;
}
           

代碼的執行結果如下:

child:receive message from pipe 1:I have liked yuki...
parent:receive message from pipe 2:I have liked yuki...
           

其中代碼的流程是父程序建立了兩個管道,我們可以用fd1,fd2表示,管道fd1負責父程序向子程序發送資料,fd2負責子程序想父程序發送資料。程序啟動後,子程序等待父程序通過管道fd1發送資料,當子程序收到父程序的資料後,輸出消息,并通過管道fd2回複父程序,然後子程序退出,父程序收到子程序的響應後,輸出消息并退出。

前面已經說了對管道的read會阻塞到管道中有資料為止,具體管道的read和write的規則将會在後面介紹。

1.3 popen和pclose函數

作為關于管道的一個執行個體,就是标準I/O函數庫提供的popen函數,該函數建立一個管道,并fork一個子程序,該子程序根據popen傳入的參數,關閉管道的對應端,然後執行傳入的shell指令,然後等待終止。

調用程序和fork的子程序之間形成一個管道。調用程序和執行shell指令的子程序之間的管道通信是通過popen傳回的FILE*來間接的實作的,調用程序通過标準檔案I/O來寫入或讀取管道。

下面是這兩個函數的聲明。

#include <stdio.h>

FILE *popen(const char *command, const char *type);
                          //成功傳回标準檔案I/O指針,失敗傳回NULL

int pclose(FILE *stream);
                          //成功傳回shell的終止狀态,失敗傳回-1
           

command:該傳入參數是一個shell指令行,這個指令是通過shell處理的。

type:該參數決定調用程序對要執行的command的處理,type有如下兩種情況:

  • type = “r”,調用程序将讀取command執行後的标準輸出,該标準輸出通過傳回的FILE*來操作;
  • type = “w”,調用程序将寫command執行過程中的标準輸入;

pclose函數會關閉由popen建立的标準I/O流,等待其中的指令終止,然後傳回shell的執行狀态。

下面是關于popen的測試代碼:

#include <iostream>
#include <cstdio>

#include <unistd.h>

using namespace std;

int main()
{
    char *cmd = "ls /usr/local/bin ";

    FILE *p = popen(cmd, "r");
    char buf[256];

    while (fgets(buf, 256, p) != NULL)
    {
        cout<<buf;
    }  

    pclose(p);

    return 0;
}
           

程式的執行結果如下所示:

ccmake
cmake
cpack
CSGMP_CG_Server
CSGMP_Start.sh
ctest
...
           

程式的執行流程如下:調用程序執行popen時,會建立一個管道,然後fork生成一個子程序,子程序執行popen傳入的"ls /usr/local/bin" shell指令,子程序将執行結果通過管道傳遞給調用程序,調用程序通過标準檔案I/O來讀取管道中的資料,并輸出顯示。

2 FIFO

POSIX标準中的FIFO又名有名管道或命名管道。我們知道前面講述的POSIX标準中管道是沒有名稱的,是以它的最大劣勢是隻能用于具有親緣關系的程序間的通信。FIFO最大的特性就是每個FIFO都有一個路徑名與之相關聯,進而允許無親緣關系的任意兩個程序間通過FIFO進行通信。

是以,FIFO的兩個特性:

  • 和管道一樣,FIFO僅提供半雙工的資料通信,即隻支援單向的資料流。
  • 和管道不同的是,FIFO可以支援任意兩個程序間的通信。

下面是FIFO的接口定義:

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
                         //成功則傳回0,失敗傳回-1
           

pathname:一個Linux路徑名,它是FIFO的名字。即每個FIFO與一個路徑名相對應。

mode:指定的檔案權限位,類似于open函數的第三個參數。即建立該FIFO時,指定使用者的通路權限,有以下值:S_IRUSR,S_IWUSR,S_IRGRP,S_IWGRP,S_IROTH,S_IWOTH。

mkfifo函數預設指定O_CREAT | O_EXECL方式建立FIFO,如果建立成功,直接傳回0。如果FIFO已經存在,則建立失敗,會傳回-1并且errno置為EEXIST。對于其他錯誤,則置響應的errno值;

當建立一個FIFO後,它必須以隻讀方式打開或者隻寫方式打開,是以可以用open函數,當然也可以使用标準的檔案I/O打開函數,例如fopen來打開。由于FIFO是半雙工的,是以不能夠同時打開來讀和寫。

其實一般的檔案I/O函數,如read,write,close,unlink都可用于FIFO。對于管道和FIFO的write操作總是會向末尾添加資料,而對他們的read則總是會從開頭資料,是以不能對管道和FIFO中間的資料進行操作,是以對管道和FIFO使用lseek函數,是錯誤的,會傳回ESPIPE錯誤。

mkfifo的一般使用方式是:通過mkfifo建立FIFO,然後調用open,以讀或者寫的方式之一打開FIFO,然後進行資料通信。

下面是FIFO的一個簡單的測試代碼:

#include <iostream>

#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

#include <sys/stat.h>
#include <sys/types.h>

using namespace std;

#define FIFO_PATH "/home/anonym/fifo"

int main()
{
    if (mkfifo(FIFO_PATH, 0666) < 0 && errno != EEXIST)
    {
        cout<<"create fifo failed..."<<endl;
        return -1;
    }

    if (fork() == 0)
    {

        int readfd = open(FIFO_PATH, O_RDONLY);
        cout<<"child open fifo success..."<<endl;

        char buf[256];

        read(readfd, buf, sizeof(buf));
        cout<<"receive message from pipe:"<<buf<<endl;

        close(readfd);

        exit(0);
    }

    sleep(3);
    int writefd = open(FIFO_PATH, O_WRONLY);
    cout<<"parent open fifo success..."<<endl;

    char *temp = "i love you";
    write(writefd, temp, strlen(temp) + 1);

    close(writefd);
}
           

程式的執行結果如下:

parent open fifo success...
child open fifo success...
receive message from pipe:i love you
           

由上面的運作結果可以看到,子程序以讀方式open的操作會阻塞到父程序以寫方式open;關于這一點以及read和write的操作會在後面管道和FIFO的屬性部分進行介紹;

POSIX标準不僅規定了對mkfifo IPC的支援,還包括了對mkfifo shell指令的支援,是以符合POSIX标準的UNIX中都含有mkfifo指令來建立有名管道,例如下面是在Linux 2.6.18的測試:

[[email protected] program]# mkfifo skywalker
[[email protected] program]# echo "I have liked yuki..." >skywalker &
[1] 28839
[[email protected] program]# cat < skywalker
I have liked yuki...
[1]+  Done                    echo "I have liked yuki..." > skywalker
           

這裡在第二行最後加上‘&’使程序轉到背景運作,是因為FIFO以隻寫方式打開需要阻塞到FIFO以隻讀方式打開為止,是以必須要作為背景程式運作,否則程序會阻塞在前端,無法再進行相關輸入;

1.3管道和FIFO的屬性

由于在POSIX标準中,管道和FIFO都是通過檔案描述符來進行操作的,預設的情況下,對他們的操作都是阻塞的,當然也可以通過設定來使對他們的操作變成非阻塞的。我們都知道可以有兩種方式來設定一個檔案描述符為O_NONBLOCK非阻塞的:

  • 調用open時,指定O_NONBLOCK标志。例如:
int fd = open(FILE_NAME, O_RDONLY | O_NONBLOCK);
           
  •  通過fcntl檔案描述符控制操作函數,對一個已經打開的描述符啟用O_NONBLOCK标志。其中對于管道必須使用這種方式。示例如下:
int flag;
flag = fcntl(fd, F_GETFL, 0);

flag |= O_NONBLOCK;
fcntl(fd, F_SETFL, flag);
           

下圖主要說明了對管道和FIFO的各種操作在阻塞和非阻塞狀态下的不同,這張圖對對于了解和使用管道和FIFO是非常重要的。

Linux程式通信之管道和FIFO

圖 5管道和FIFO的各種操作

從上圖我們看到關于管道和FIFO的讀出和寫入的若幹規則,主要需要注意的有以下幾點:

  • 以隻讀方式open FIFO時,如果FIFO還沒有以隻寫方式open,那麼在阻塞模式下,該操作會阻塞到FIFO以隻寫方式open為止。
  • 以隻寫方式open FIFO時,如果FIFO還沒有以隻讀方式open,那麼在阻塞模式下,該操作會阻塞到FIFO以隻讀方式open為止。
  • 從空管道或空FIFO中read,如果管道和FIFO已打開來寫,在阻塞模式下,那麼該操作會阻塞到管道或FIFO有資料為止,或管道或FIFO不再以寫方式打開。如果管道和FIFO沒有打開來寫,那麼該操作會傳回0;
  • 向管道或FIFO中write,如果管道或FIFO沒有打開來讀,那麼核心會産生SIGPIPE信号,預設情況下,該信号會終止該程序。

另外對于管道和FIFO還需要說明的若幹規則如下:

  • 如果請求write的資料的位元組數小于等于PIPE_BUF(POSIX關于管道和FIFO大小的限制值),那麼write操作可以保證是原子的,如果大于PIPE_BUF,那麼就不能保證了。

那麼由此可知write的原子性是由寫入資料的位元組數是否小于等于PIPE_BUF決定的,和是不是O_NONBLOCK沒有關系。下面是在阻塞和非阻塞情況下,write不同大小的資料的操作結果:

在阻塞的情況下:

  • 如果write的位元組數小于等于PIPE_BUF,那麼write會阻塞到寫入所有資料,并且寫入操作是原子的。
  •  如果write的位元組數大于PIPE_BUF,那麼write會阻塞到寫入所有資料,但寫入操作不是原子的,即write會根據目前緩沖區剩餘的大小,寫入相應的位元組數,然後等待下一次有空餘的緩沖區,這中間可能會有其他程序進行write操作。

在非阻塞的情況下:

  • 如果write的位元組數小于等于PIPE_BUF,且管道或FIFO有足以存放要寫入資料大小的空間,那麼就寫入所有資料;
  •  如果write的位元組數小于等于PIPE_BUF,且管道或FIFO沒有足夠存放要寫入資料大小的空間,那麼就會立即傳回EAGAIN錯誤。
  • 如果write的位元組數大于PIPE_BUF,且管道或FIFO有至少1B的空間,那麼就核心就會寫入相應的位元組數,然後傳回已寫入的位元組數;
  • 如果write的位元組數大于PIPE_BUF,且管道或FIFO無任何的空間,那麼就會立即傳回EAGAIN錯誤。

1.4管道和FIFO的限制

系統核心對于管道和FIFO的唯一限制為:OPEN_MAX和PIPE_BUF;

OPEN_MAX:一個程序在任意時刻可以打開的最大描述符數。PIPE_BUF辨別一個管道可以原子寫入管道和FIFO的最大位元組數,并不是管道或FIFO的容量。

關于這兩個系統限制,POSIX标準中都有定義的不變最小值:_POSIX_OPEN_MAX和_POSIX_PIPE_BUF,這兩個宏是POSXI标準定義的編譯時确定的值,他們是标準定義的且不會改變的,POSIX标準關于這兩個值的限制為:

cout<<_POSIX_OPEN_MAX<<endl;
cout<<_POSIX_PIPE_BUF<<endl;

//運作結果為:
20
512
           

我們都知道,關于POSIX的每個不變最小值都有一個具體的系統的實作值,這些是實作值由具體的系統決定,通過調用以下函數在運作時确定這個實作值:

#include <unistd.h>

long sysconf(int name);
long fpathconf(int filedes, int name);
long pathconf(char *path, int name);

                           //成功傳回具體的值,失敗傳回-1
           

其中sysconf是用于傳回系統限制值,這些值是以_SC_開頭的常量,pathconf和fpathconf是用于傳回與檔案和目錄相關的運作時的限制值,這些值都是以_PC_開頭的常量;下面是在Linux 2.6.18下的測試代碼:

cout<<sysconf(_SC_OPEN_MAX)<<endl;
cout<<pathconf(FIFO_PATH, _PC_PATH_MAX)<<endl;

//運作結果為:
1024
4096
           

當然上面兩個系統限制值的具體實作值也可以通過ulimit指令來檢視,下面是在Linux 2.6.18下檢視的結果:

[[email protected] program]# ulimit -a
...
open files                    (-n) 1024
pipe size            (512 bytes, -p) 8
...
           

這兩個值在Linux 2.6.18下都是不允許修改的,也是沒有必要修改的;

Jul 20, 2013 16:52 @library 

機會永遠都是留給有準備的人。。。

繼續閱讀