天天看點

Linux程序間通信(四):命名管道 mkfifo()、open()、read()、close()

在前一篇文章—— ​​Linux程序間通信 -- 使用匿名管道​​ 中,我們看到了如何使用匿名管道來在程序之間傳遞資料,同時也看到了這個方式的一個缺陷,就是這些程序都由一個共同的祖先程序啟動,這給我們在不相關的的程序之間交換資料帶來了不友善。這裡将會介紹程序的另一種通信方式——命名管道,來解決不相關程序間的通信問題。

一、什麼是命名管道

命名管道也被稱為FIFO檔案,它是一種特殊類型的檔案,它在檔案系統中以檔案名的形式存在,但是它的行為卻和之前所講的沒有名字的管道(匿名管道)類似。

由于Linux中所有的事物都可被視為檔案,是以對命名管道的使用也就變得與檔案操作非常的統一,也使它的使用非常友善,同時我們也可以像平常的檔案名一樣在指令中使用。

二、建立命名管道

我們可以使用兩下函數之一來建立一個命名管道,他們的原型如下:

#include <sys/types.h>

#include <sys/stat.h>

int mkfifo(const char *filename, mode_t mode);

int mknod(const char *filename, mode_t mode | S_IFIFO, (dev_t)0);

這兩個函數都能建立一個FIFO檔案,注意是建立一個真實存在于檔案系統中的檔案,filename指定了檔案名,而mode則指定了檔案的讀寫權限。

mknod是比較老的函數,而使用mkfifo函數更加簡單和規範,是以建議在可能的情況下,盡量使用mkfifo而不是mknod。

三、通路命名管道

1、打開FIFO檔案

與打開其他檔案一樣,FIFO檔案也可以使用open調用來打開。注意,mkfifo函數隻是建立一個FIFO檔案,要使用命名管道還是将其打開。

但是有兩點要注意:

1、就是程式不能以O_RDWR模式打開FIFO檔案進行讀寫操作,而其行為也未明确定義,因為如一個管道以讀/寫方式打開,程序就會讀回自己的輸出,同時我們通常使用FIFO隻是為了單向的資料傳遞。

2、就是傳遞給open調用的是FIFO的路徑名,而不是正常的檔案。

打開FIFO檔案通常有四種方式,

open(const char *path, O_RDONLY); // 1

open(const char *path, O_RDONLY | O_NONBLOCK); // 2

open(const char *path, O_WRONLY); // 3

open(const char *path, O_WRONLY | O_NONBLOCK); // 4

在open函數的調用的第二個參數中,你看到一個陌生的選項 O_NONBLOCK,選項 O_NONBLOCK 表示非阻塞,加上這個選項後,表示open調用是非阻塞的,如果沒有這個選項,則表示open調用是阻塞的。

open調用的阻塞是什麼一回事呢?很簡單,對于以隻讀方式(O_RDONLY)打開的FIFO檔案,如果open調用是阻塞的(即第二個參數為O_RDONLY),除非有一個程序以寫方式打開同一個FIFO,否則它不會傳回;如果open調用是非阻塞的的(即第二個參數為O_RDONLY | O_NONBLOCK),則即使沒有其他程序以寫方式打開同一個FIFO檔案,open調用将成功并立即傳回。

對于以隻寫方式(O_WRONLY)打開的FIFO檔案,如果open調用是阻塞的(即第二個參數為O_WRONLY),open調用将被阻塞,直到有一個程序以隻讀方式打開同一個FIFO檔案為止;如果open調用是非阻塞的(即第二個參數為O_WRONLY | O_NONBLOCK),open總會立即傳回,但如果沒有其他程序以隻讀方式打開同一個FIFO檔案,open調用将傳回-1,并且FIFO也不會被打開。

四、使用FIFO實作程序間的通信

說了這麼多,下面就用一個例子程式來說明一下,兩個程序如何通過FIFO實作通信吧。這裡有兩個源檔案,一個fifowrite.c,它在需要時建立管道,然後向管道寫入資料,資料由檔案Data.txt提供,大小為10M,内容全是字元‘0’。另一個源檔案為fiforead.c,它從FIFO中讀取資料,并把讀到的資料儲存到另一個檔案DataFormFIFO.txt中。為了讓程式更加簡潔,忽略了有些函數調用是否成功的檢查。

fifowrite.c的源代碼如下:

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

int main()
{
  const char *fifo_name = "/tmp/my_fifo";
  int pipe_fd = -1;
  int data_fd = -1;
  int res = 0;
  const int open_mode = O_WRONLY;
  int bytes_sent = 0;
  char buffer[PIPE_BUF + 1];

  if (access(fifo_name, F_OK) == -1)
  {
    // 管道檔案不存在
    // 建立命名管道
    res = mkfifo(fifo_name, 0777);
    if (res != 0)
    {
      fprintf(stderr, "Could not create fifo %s\n", fifo_name);
      exit(EXIT_FAILURE);
    }
  }

  printf("Process %d opening FIFO O_WRONLY\n", getpid());

  // 以隻寫阻塞方式打開FIFO檔案,以隻讀方式打開資料檔案
  pipe_fd = open(fifo_name, open_mode);
  data_fd = open("Data.txt", O_RDONLY);
  printf("Process %d result %d\n", getpid(), pipe_fd);

  if (pipe_fd != -1)
  {
    int bytes_read = 0;

    // 向資料檔案讀取資料
    bytes_read = read(data_fd, buffer, PIPE_BUF);
    buffer[bytes_read] = '\0';
    while (bytes_read > 0)
    {
      // 向FIFO檔案寫資料
      res = write(pipe_fd, buffer, bytes_read);
      if (res == -1)
      {
        fprintf(stderr, "Write error on pipe\n");
        exit(EXIT_FAILURE);
      }

      // 累加寫的位元組數,并繼續讀取資料
      bytes_sent += res;
      bytes_read = read(data_fd, buffer, PIPE_BUF);
      buffer[bytes_read] = '\0';
    }
    close(pipe_fd);
    close(data_fd);
  }
  else
  {
    exit(EXIT_FAILURE);
  }

  printf("Process %d finished\n", getpid());
  exit(EXIT_SUCCESS);
}      

源檔案fiforead.c的代碼如下:

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

int main()
{
  const char *fifo_name = "/tmp/my_fifo";
  int pipe_fd = -1;
  int data_fd = -1;
  int res = 0;
  int open_mode = O_RDONLY;
  char buffer[PIPE_BUF + 1];
  int bytes_read = 0;
  int bytes_write = 0;

  // 清空緩沖數組
  memset(buffer, '\0', sizeof(buffer));

  printf("Process %d opening FIFO O_RDONLY\n", getpid());

  // 以隻讀阻塞方式打開管道檔案,注意與fifowrite.c檔案中的FIFO同名
  pipe_fd = open(fifo_name, open_mode);

  // 以隻寫方式建立儲存資料的檔案
  data_fd = open("DataFormFIFO.txt", O_WRONLY | O_CREAT, 0644);
  printf("Process %d result %d\n", getpid(), pipe_fd);

  if (pipe_fd != -1)
  {
    do
    {
      // 讀取FIFO中的資料,并把它儲存在檔案DataFormFIFO.txt檔案中
      res = read(pipe_fd, buffer, PIPE_BUF);
      bytes_write = write(data_fd, buffer, res);
      bytes_read += res;
    }
    while (res > 0);
    close(pipe_fd);
    close(data_fd);
  }
  else
  {
    exit(EXIT_FAILURE);
  }

  printf("Process %d finished, %d bytes read\n", getpid(), bytes_read);
  exit(EXIT_SUCCESS);
}      

運作結果如下:

Linux程式間通信(四):命名管道 mkfifo()、open()、read()、close()

分析:兩個程式都使用阻塞模式的FIFO,為了讓大家更清楚地看清楚阻塞究竟是怎麼一回事,首先我們運作fifowrite.exe,并把它放到背景去運作。這時調用jobs指令,可以看到它确實在背景運作着,過了5秒後,再調用jobs指令,可以看到程序fifowrite.exe還沒有結束,它還在繼續運作。因為fifowrite.exe程序的open調用是阻塞的,在fiforead.exe還沒有運作時,也就沒有其他的程序以讀方式打開同一個FIFO,是以它就一直在等待,open被阻塞,沒有傳回。然後,當我們程序fiforead.exe運作時(為了檢視性能,在time指令中運作),fifowrite.exe中的open調用傳回,程序開始繼續工作,然後結束程序。而fiforead.exe的open調用雖然也是阻塞模式,但是fifowrite.exe早已運作,即早有另一個程序以寫方式打開同一個FIFO,是以open調用立即傳回。

從time中的輸出來看,管道的傳遞效率是非常高的,因為fiforead.exe既要讀取資料,還要寫資料到檔案DataFormFIFO.txt中,10M的資料隻用了0.1秒多一點。

此外,如果此時,你在shell中輸入如下指令,ls -l /tmp/my_fifo,可以看到如下結果:

Linux程式間通信(四):命名管道 mkfifo()、open()、read()、close()

證明FIFO檔案确實是存在于檔案系統中的檔案,檔案屬性的第一個字元為‘p',表示該檔案是一個管道。

五、命名管道的安全問題

前面的例子是兩個程序之間的通信問題,也就是說,一個程序向FIFO檔案寫資料,而另一個程序則在FIFO檔案中讀取資料。試想這樣一個問題,隻使用一個FIFO檔案,如果有多個程序同時向同一個FIFO檔案寫資料,而隻有一個讀FIFO程序在同一個FIFO檔案中讀取資料時,會發生怎麼樣的情況呢,會發生資料塊的互相交錯是很正常的?而且個人認為多個不同程序向一個FIFO讀程序發送資料是很普通的情況。

為了解決這一問題,就是讓寫操作的原子化。怎樣才能使寫操作原子化呢?答案很簡單,系統規定:在一個以O_WRONLY(即阻塞方式)打開的FIFO中, 如果寫入的資料長度小于等待PIPE_BUF,那麼或者寫入全部位元組,或者一個位元組都不寫入。如果所有的寫請求都是發往一個阻塞的FIFO的,并且每個寫記請求的資料長度小于等于PIPE_BUF位元組,系統就可以確定資料決不會交錯在一起。

六、命名管道與匿名管道的對比

使用匿名管道,則通信的程序之間需要一個父子關系,通信的兩個程序一定是由一個共同的祖先程序啟動。但是匿名管道沒有上面說到的資料交叉的問題。

與使用匿名管道相比,我們可以看到fifowrite.exe和fiforead.exe這兩個程序是沒有什麼必然的聯系的,如果硬要說他們具有某種聯系,就隻能說是它們都通路同一個FIFO檔案。它解決了之前在匿名管道中出現的通信的兩個程序一定是由一個共同的祖先程序啟動的問題。但是為了資料的安全,我們很多時候要采用阻塞的FIFO,讓寫操作變成原子操作。