Linux管道FIFO

嚇人的猿發表於2018-02-27

管道FIFO

2.1 管道基本概念

​ 管道是針對於本地計算機的兩個程式之間的通訊而設計的通訊方法,管道建立後,實際上是獲得兩個檔案描述符:一個用與讀取而另一個用於寫入。任何從管道寫入端寫入的資料,可以從管道讀取端讀出。

​ 管道通訊具有以下特點:

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

  • 管道存放在記憶體中,是一種獨立的檔案系統。

2.2 無名管道的建立與讀寫

​ 系統呼叫pipe()用於建立一個管道,其函式原型如下:

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

​ pipe()將建立一對檔案描述符,放到引數pipefd中。pipefd[0]檔案描述符用來從管道中讀取資料,pipefd[1]用於寫入資料到管道。

​ 單個程式中的管道幾乎沒有任何意義,通常,程式會先呼叫pipe,接著呼叫fork,從而建立從父程式到子程式的通道。

​ fork出子程式後,父程式的檔案描述表也複製到子程式,於是如下圖:

​ 關閉掉父程式的fd[0], 以及子程式的fd[1],則父程式可以傳送訊息給子程式,反之亦然。

原始碼:pipe_test.c

#include <stdio.h>
#include <unistd.h>

void child_process(int pipefd[])
{
        int i = 0;
        char writebuf[128] = {0};
  
        /*子程式關閉讀檔案描述符*/
        close(pipefd[0]); 
        while(1)
        {
                sprintf(writebuf, "write pipe : %d", i);
          
                /*子程式往管道寫入資料*/
                write(pipefd[1], writebuf, strlen(writebuf));
                printf("write pipe: %s\n", writebuf);
                i = (i + 1) % 10;
                sleep(1);
        }
}

void father_process(int pipefd[])
{
        char readbuf[128] = {0};
  
         /*父程式關閉寫檔案描述符*/
        close(pipefd[1]);
        while(1)
        {
                /*父程式從管道中讀取資料*/
                read(pipefd[0], readbuf, sizeof(readbuf));
                printf("read pipe: %s\n", readbuf);
        }
}

int main(int argc, char** argv)
{
        int pipefd[2];
        pid_t pid;

        if( pipe(pipefd) == -1)
        {
                perror("pipe:");
                return -1;
        }

        pid = fork();
        if(pid == 0)
        {
                child_process(pipefd);
        }
        else if(pid != -1)
        {
                father_process(pipefd);
        }
        else
        {
                perror("fork:");
        }
        return 0;
}

執行結果:子程式寫資料,父程式讀出資料並且列印。

where@ubuntu:~$ ./pipe_test 
write pipe: write pipe : 0
read pipe: write pipe : 0
write pipe: write pipe : 1
read pipe: write pipe : 1
write pipe: write pipe : 2
read pipe: write pipe : 2
write pipe: write pipe : 3

​ 注意:匿名管道只能用於父子程式或者兄弟程式之間(具有親緣關係的程式)。

​ 如果管道讀端被關閉,那麼這個時候如果繼續write()管道,會發出訊號SIGPIPE。如果管道寫端被關閉,

那麼這個時候如果繼續read()管道直接返回0。

2.3 命名管道FIFO

​ FIFO是first in first out(先進先出)的縮寫,FIFO也稱為“命名管道”。FIFO是一種特殊型別的管道,它在檔案系統中有一個相應的檔案,稱為管道檔案。

​ FIFO檔案可以通過mkfifo()函式建立。在FIFO檔案建立之後,任何一個具有適當許可權的程式都可以開啟FIFO檔案。

  • mkfifo()函式原型為:

#include <unistd.h>
int mkfifo(const char* pathname, mode_t mode);

引數:

pathname一個FIFO檔案的路徑名。

mode與普通檔案creat()函式中的mode引數相同。

返回值:

​ 如果要建立的檔案已經存在,返回-1, errnoEEXIST錯誤, 成功返回0。

​ 一般檔案的I/O函式都可以用於FIFO,如open()、read()、write()、close()等等。

  • 原始碼fifo_write_test.c

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
void handle_sig(int sig)
{
    printf("signal pipe\n");
    exit(-1);
}

int main(int argc, char** argv)
{
    int fd;
    int ret;
    char buf[128];
    int i =  0;

    /*處理管道訊號*/
    signal(SIGPIPE, handle_sig);
    if(mkfifo("./fifo", 0640) == -1)
    {
        if(errno !=  EEXIST)
        {
            perror("mkfifo");
            return -1;
        }
    }
    /*只寫方式開啟管道*/
    fd = open("./fifo", O_WRONLY);  
    if(fd == -1)
    {
        perror("open");
        return -1;
    }

    while(1)    
    {
        sprintf(buf, "data %d", i++);
        /*往管道寫資料*/
        ret = write(fd, buf, strlen(buf));

        printf("write fifo [%d] %s\n", ret, buf);
      
        sleep(1);
    }
    return 0;
}

​ 原始碼中,通過mkfifo來建立FIFO檔案,並且以只寫 的方式開啟,只有當兩邊的管道都開啟的時候才能寫進去,否則阻塞在write()函式上,如果管道另一端開啟後被關閉,那麼這個時候如果繼續write()FIFO管道,會發出訊號SIGPIPE.

  • 原始碼fifo_read_test.c

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>

int main(int argc, char** argv)
{
    int fd;
    int ret;
    char buf[128];
    if(mkfifo("./fifo", 0640) == -1)
    {
        if(errno !=  EEXIST) /*如果錯誤型別是fifo檔案已經存在,則繼續執行*/
        {
            perror("mkfifo");
            return -1;
        }
    }

    /*以只讀方式開啟管道*/
    fd = open("./fifo", O_RDONLY);  
    if(fd == -1)
    {
        perror("open");
        return -1;
    }

    while(1)    
    {
        memset(buf, 0, sizeof(buf));
        /*讀管道*/
        ret = read(fd, buf, sizeof(buf) - 1);
    
        printf("read fifo [%d] : %s\n", ret, buf);
        
        sleep(1);
    }
    return 0;
}

​ 原始碼中,通過mkfifo()函式建立FIFO管道,如果已經存在那麼就直接只讀方式開啟,如果另一端沒有被開啟,則阻塞在read()函式上,如果另一端開啟後關閉,則read()一直讀到EOF也就是0個位元組。

2.4 管道容量

​ 管道有它的容量大小,通過man 7 pipe來檢視。預設情況下為64k位元組,也可以通過fcntl函式來檢視:

#define _GNU_SOURCE //在任何標頭檔案包含之前新增這個巨集定義
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
/*
cmd:F_GETPIPE_SZ 獲取管道容量大小,設定管道容量大小F_SETPIPE_SZ
*/

​ 例如:

#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
    if(mkfifo("./fifo", 0640) == -1)
    {
        if(errno !=  EEXIST)
        {
            perror("mkfifo");
            return -1;
        }
    }
    /*只寫方式開啟管道*/
    int fd = open("./fifo", O_WRONLY | O_NONBLOCK);  
    if(fd == -1)
    {
        perror("open");
        return -1;
    }

    printf("pipe size %d\n", fcntl(fd, F_GETPIPE_SZ));
    fcntl(fd, F_SETPIPE_SZ, 4096 * 3); /*必須填寫4096的整數倍*/
    printf("pipe size %d\n", fcntl(fd, F_GETPIPE_SZ));
    return 0;
}
    

2.5 注意事項

​ 使用管道需要注意以下4種特殊情況(假設都是阻塞I/O操作,沒有設定O_NONBLOCK標誌):

1.如果所有指向管道寫端的檔案描述符都關閉了(管道寫端的引用計數等於0),而仍然有程式從管道的讀端讀資料,那麼管道中剩餘的資料都被讀取後,再次read會返回0,就像讀到檔案末尾一樣。

2.如果有指向管道寫端的檔案描述符沒關閉(管道寫端的引用計數大於0),而持有管道寫端的程式也沒有向管道中寫資料,這時有程式從管道讀端讀資料,那麼管道中剩餘的資料都被讀取後,再次read會阻塞,直到管道中有資料可讀了才讀取資料並返回。

3.如果所有指向管道讀端的檔案描述符都關閉了(管道讀端的引用計數等於0),這時有程式向管道的寫端write,那麼該程式會收到訊號SIGPIPE,通常會導致程式異常終止。講訊號時會講到怎樣使SIGPIPE訊號不終止程式。

4.如果有指向管道讀端的檔案描述符沒關閉(管道讀端的引用計數大於0),而持有管道讀端的程式也沒有從管道中讀資料,這時有程式向管道寫端寫資料,那麼在管道被寫滿時再次write會阻塞,直到管道中有空位置了才寫入資料並返回。

相關文章