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

在前一篇文章—— Linux進程間通訊 -- 使用匿名管道 中,咱們看到了如何使用匿名管道來在進程之間傳遞數據,同時也看到了這個方式的一個缺陷,就是這些進程都由一個共同的祖先進程啓動,這給咱們在不相關的的進程之間交換數據帶來了不方便。這裏將會介紹進程的另外一種通訊方式——命名管道,來解決不相關進程間的通訊問題。php

1、什麼是命名管道

命名管道也被稱爲FIFO文件,它是一種特殊類型的文件,它在文件系統中以文件名的形式存在,可是它的行爲卻和以前所講的沒有名字的管道(匿名管道)相似。html

因爲Linux中全部的事物均可被視爲文件,因此對命名管道的使用也就變得與文件操做很是的統一,也使它的使用很是方便,同時咱們也能夠像日常的文件名同樣在命令中使用。shell

2、建立命名管道

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

#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。服務器

3、訪問命名管道

一、打開FIFO文件函數

與打開其餘文件同樣,FIFO文件也可使用open調用來打開。注意,mkfifo函數只是建立一個FIFO文件,要使用命名管道仍是將其打開。性能

可是有兩點要注意ui

一、就是程序不能以O_RDWR模式打開FIFO文件進行讀寫操做,而其行爲也未明肯定義,由於如一個管道以讀/寫方式打開,進程就會讀回本身的輸出,同時咱們一般使用FIFO只是爲了單向的數據傳遞。

二、就是傳遞給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也不會被打開。

4、使用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',表示該文件是一個管道。

5、命名管道的安全問題

前面的例子是兩個進程之間的通訊問題,也就是說,一個進程向FIFO文件寫數據,而另外一個進程則在FIFO文件中讀取數據。試想這樣一個問題,只使用一個FIFO文件,若是有多個進程同時向同一個FIFO文件寫數據,而只有一個讀FIFO進程在同一個FIFO文件中讀取數據時,會發生怎麼樣的狀況呢,會發生數據塊的相互交錯是很正常的?並且我的認爲多個不一樣進程向一個FIFO讀進程發送數據是很普通的狀況。

爲了解決這一問題,就是讓寫操做的原子化。怎樣才能使寫操做原子化呢?答案很簡單,系統規定:在一個以O_WRONLY(即阻塞方式)打開的FIFO中, 若是寫入的數據長度小於等待PIPE_BUF,那麼或者寫入所有字節,或者一個字節都不寫入。若是全部的寫請求都是發往一個阻塞的FIFO的,而且每一個寫記請求的數據長度小於等於PIPE_BUF字節,系統就能夠確保數據決不會交錯在一塊兒。

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

使用匿名管道,則通訊的進程之間須要一個父子關係,通訊的兩個進程必定是由一個共同的祖先進程啓動。可是匿名管道沒有上面說到的數據交叉的問題。

與使用匿名管道相比,咱們能夠看到fifowrite.exe和fiforead.exe這兩個進程是沒有什麼必然的聯繫的,若是硬要說他們具備某種聯繫,就只能說是它們都訪問同一個FIFO文件。它解決了以前在匿名管道中出現的通訊的兩個進程必定是由一個共同的祖先進程啓動的問題。可是爲了數據的安全,咱們不少時候要採用阻塞的FIFO,讓寫操做變成原子操做。

 

 

參考:

http://blog.csdn.net/ljianhui/article/details/10202699

《Linux 高性能服務器編程》

相關文章
相關標籤/搜索