1. 程式人生 > >Linux程序間通訊——使用命名管道

Linux程序間通訊——使用命名管道

在前一篇文章——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);
}
執行結果如下:
分析:兩個程式都使用阻塞模式的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,可以看到如下結果:
證明FIFO檔案確實是存在於檔案系統中的檔案,檔案屬性的第一個字元為‘p',表示該檔案是一個管道。 五、命名管道的安全問題 前面的例子是兩個程序之間的通訊問題,也就是說,一個程序向FIFO檔案寫資料,而另一個程序則在FIFO檔案中讀取資料。試想這樣一個問題,只使用一個FIFO檔案,如果有多個程序同時向同一個FIFO檔案寫資料,而只有一個讀FIFO程序在同一個FIFO檔案中讀取資料時,會發生怎麼樣的情況呢,會發生資料塊的相互交錯是很正常的?而且個人認為多個不同程序向一個FIFO讀程序傳送資料是很普通的情況。 為了解決這一問題,就是讓寫操作的原子化。怎樣才能使寫操作原子化呢?答案很簡單,系統規定:在一個以O_WRONLY(即阻塞方式)開啟的FIFO中, 如果寫入的資料長度小於等待PIPE_BUF,那麼或者寫入全部位元組,或者一個位元組都不寫入。如果所有的寫請求都是發往一個阻塞的FIFO的,並且每個寫記請求的資料長度小於等於PIPE_BUF位元組,系統就可以確保資料決不會交錯在一起。 六、命名管道與匿名管道的對比 使用匿名管道,則通訊的程序之間需要一個父子關係,通訊的兩個程序一定是由一個共同的祖先程序啟動。但是匿名管道沒有上面說到的資料交叉的問題。 與使用匿名管道相比,我們可以看到fifowrite.exe和fiforead.exe這兩個程序是沒有什麼必然的聯絡的,如果硬要說他們具有某種聯絡,就只能說是它們都訪問同一個FIFO檔案。它解決了之前在匿名管道中出現的通訊的兩個程序一定是由一個共同的祖先程序啟動的問題。但是為了資料的安全,我們很多時候要採用阻塞的FIFO,讓寫操作變成原子操作。