1. 程式人生 > >程序間通訊之管道篇

程序間通訊之管道篇

何為程序間通訊

每個程序各自有不同的使用者地址空間,任何一個程序的全域性變數在另一個程序中都是看不到的。所以程序之間如果要交換資料就必須通過核心。

在核心中開闢一塊緩衝區,程序1把資料從使用者空間拷到核心緩衝區,程序2再從核心緩衝區把資料讀走,核心提供的這種機制稱為程序間通訊(IPC,InterProcess Communication)。        

程序間通訊的本質,就是要讓不同的程序看到同一份資源。

程序間的通訊方法有很多,今天只說管道。

何為管道

管道分為兩種:匿名管道與命名管道。

何為匿名管道

匿名管道是一種最基本的IPC機制,由pipe函式建立:
         
呼叫pipe函式時在核心中開闢一塊緩衝區(稱為管道)用於通訊,它有一個讀端一個寫端,然後通過pipefd引數傳出給使用者程式兩個檔案描述符,pipefd[0]指向管道的讀端,fpipefd1]指向管道的寫端。

所以管道在使用者程式看起來就像一個開啟的檔案,通過read(filedes[0]);或者write(filedes[1]);向這個檔案讀寫資料其實是在讀寫核心緩衝區。pipe函式呼叫成功返回0,呼叫失敗返回-1。 開闢了管道之後如何實現兩個程序間的通訊呢?比如可以按下面的步驟通訊。

                   

  1. 父程序呼叫pipe開闢管道,得到兩個檔案描述符指向管道的兩端。
  2.  父程序呼叫fork建立子程序,那麼子程序也有兩個檔案描述符指向同一管道。
  3. 父程序關閉管道讀端,子程序關閉管道寫端(當然也可以反過來)。父程序可以往管道里寫,子程序可以從管道里讀,管道是用環形佇列實現的,資料從寫端流入,從讀端流出,這樣就實現了程序間通訊。

因為管道是單向通訊的,即單工,所以父子程序必須關閉它們各自不需要的端。其次,匿名管道是通過子程序繼承父程序的檔案描述符表才得以實現父子程序共同看到一份資源,所以匿名管道也就只能在有親緣關係的程序間實現通訊。

程式碼實現:

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

int main()
{
	int fd[2];
	if (pipe(fd))
	{
		perror("pipe");
		return 1;
	}

	// 實現父程序寫,子程序讀
	pid_t id = fork();
	if (id < 0)
	{
		perror("fork");
		return 2;
	}
	else if (id == 0) // child
	{
		close(fd[1]);
		
		char buf[128];
		int cnt = 0;
		while (cnt++ < 5)
		{
			ssize_t _s = read(fd[0], buf, sizeof(buf));
			if (_s > 0)
			{
				buf[_s] = '\0';;
				printf("father say to child: %s\n", buf);
			}
			else if (_s == 0)
			{
				printf("father close write");
				break;
			}
			else
			{
				perror("read");
				break;
			}
		}

		close(fd[0]);
	}
	else		  // father
	{
		close(fd[0]);
		
		char * msg = "hello world";
		int cnt = 0;
		while (cnt++ < 5)
		{
			write(fd[1], msg, strlen(msg));
			sleep(1);
		}
	
		close(fd[1]);
	}
	
	return 0;
}
執行結果:

         

匿名管道的特點:

  1. 單向通訊
  2. 具有親緣關係的程序間通訊
  3. 管道生命週期隨程序(管道檔案描述符在程序結束後被關閉)
  4. 面向位元組流的服務
  5. 底層實現的同步機制,無需使用者在考慮(為空不允許讀,為滿不允許寫(阻塞))

幾種特殊情況:

  1. 如果所有指向管道寫端的檔案描述符都關閉了(管道寫端的引用計數等於0),而仍然有程序從管道的讀端讀資料,那麼管道中剩餘的資料都被讀取後,再次read會返回0,就像讀到檔案末尾一樣。
  2. 如果有指向管道寫端的檔案描述符沒關閉(管道寫端的引用計數大於0),而持有管道寫端的程序也沒有向管道中寫資料,這時有程序從管道讀端讀資料,那麼管道中剩餘的資料都被讀取後,再次read會阻塞,直到管道中有資料可讀了才讀取資料並返回。
  3. 如果所有指向管道讀端的檔案描述符都關閉了(管道讀端的引用用計數等於0),這時有程序向管道的寫端write,那麼該程序會收到訊號SIGPIPE,通常會導致程序異常終止。
  4. 如果有指向管道讀端的檔案描述符沒關閉(管道讀端的引用計數大於0),而持有管道讀端的程序也沒有從管道中讀資料,這時有程序向管道寫端寫資料,那麼在管道被寫滿時再次write會阻塞,直到管道中有空位置了才寫入資料並返回。

何為命名管道

匿名管道的缺點就是隻能在有親緣關係的程序間進行通訊,針對這個缺陷,又提出來了命名管道(FIFO)的概念。FIFO不同於管道之處在於它提供一個路徑名與之關聯,以FIFO的檔案形式儲存於檔案系統中。命名管道是一個裝置檔案,因此,即使程序與建立FIFO的程序不存在親緣關係,只要可以訪問該路徑,就能夠通過FIFO
相互通訊。值得注意的是,FIFO(first input first output)總是按照先進先出的原則工作,第一個被寫入的資料將首先從管道中讀出。

建立命名管道的方式無非也就是那兩種:命令和函式。而且命令和函式對應的名字是一樣的,mkfifo(mknod)命令/函式。

示例程式碼:

server:

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

int main()
{
	// 建立管道時需要在mode引數位置傳S_IFIFO,表明建立的是命名管道
	int ret = mkfifo("./.fifo", S_IFIFO | 0644);	
	if (ret < 0)
	{
		perror("mkfifo");
		return 1;
	}

	int fd = open("./.fifo", O_WRONLY);
	if (fd < 0)
	{
		perror("open");
		return 2;
	}

	int cnt = 0;
	char *msg = "hello world";
	while (cnt++ < 5)
	{
		write(fd, msg, strlen(msg));
		sleep(1);
	}

	close(fd);
	return 0;
}

client:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
	int fd = open("./.fifo", O_RDONLY);
	if (fd < 0)
	{
		perror("open");
		return 2;
	}

	int cnt = 0;
	char buf[128];
	while (cnt++ < 5)
	{
		ssize_t _s = read(fd, buf, sizeof(buf) - 1);
		if (_s > 0)
		{
			buf[_s] = '\0';;
			printf("server say to client: %s\n", buf);
		}
		else if (_s == 0)
		{
			printf("server close write\n");
			break;
		}
		else
		{
			perror("read");
		}
		sleep(1);
	}

	close(fd);
	return 0;
}

執行結果:

         

命名管道建立後就可以使用了,命名管道和管道的使用方法基本是相同的。只是使用命名管道時,必須先呼叫open()將其開啟。因為命名管道是一個存在於硬碟上的檔案,而管道是存在於記憶體中的特殊檔案。需要注意的是,呼叫open()開啟命名管道的程序可能會被阻塞。但如果同時用讀寫方式(O_RDWR)開啟,則一定不會導致阻塞;如果以只讀方式(O_RDONLY)開啟,則呼叫open()函式的程序將會被阻塞直到有寫方開啟管道;同樣以寫方式(O_WRONLY)開啟也會阻塞直到有讀方式開啟道。


命名管道與匿名管道不同的地方在於即使沒有親緣關係,也可以通過FIFO來通訊,且管道的生命週期不再是隨程序,因為即使命名管道檔案描述符被關閉,FIFO依然存在於磁碟上,是一個檔案。