Linux程序设计——进程间通信:管道

使用信号在进程间通信,传送的信息只限于一个信号值。更多的数据交换需要使用一种新的机制——管道。管道(pipe)把一个进程的输出连接到另一个进程的输入。

对shell命令的连接就是通过管道实现的,使用管道符号"|"连接。

1、进程管道

使用popen和pclose创建和关闭管道。popen允许一个程序将另一个程序作为新进程启动,并可以传递数据给它或者通过它接收数据。

#include 
FILE *popen(const char *command, const char *open_mode);
int pclose(FILE *stream_to_close);
popen函数的open_mode必须是"r"或者"w",而且调用popen是必须指定。"r"表示被调用程序的输出可以作为调用程序的输入。"w"表示调用程序可以用fwrite调用向被调用程序发送数据,而被调用程序可以在自己的标准输入上读取这些数据。

pclose调用关闭popen调用创建的管道。

下面的程序使用带参数"w"的popen启动od -c命令,并向该命令发送数据。该命令接收数据并处理,最后打印在标准输出上。

#include 
#include 
#include 
#include 

int main()
{
	FILE *write_fp;
	char buffer[BUFSIZ + 1];
	
	sprintf(buffer, "Once upon a time, there was...\n");
	write_fp = popen("od -c", "w"); // 八进制输出数据
	if(write_fp != NULL)
	{
		fwrite(buffer, sizeof(char), strlen(buffer), write_fp);
		pclose(write_fp);
		exit(EXIT_SUCCESS);
	}
	exit(EXIT_FAILURE);
}

请求popen调用运行一个程序时,它首先启动shell,然后再将command命令字符串作为参数传递给它。好处:Linux系统中所有的参数扩展都是由shell完成的。所以,在启动程序之前先启动shell来分析命令字符串,使各种shell扩展在程序启动之前就全部完成。不足:针对每个popen调用不仅要启动一个被请求的程序,还要启动一个shell,即每个popen调用将多启动两个进程,造成调用成本过高,而且对目标命令的调用比正常方式要慢。


2、pipe调用

pipe调用在两个程序之间传递数据不需要启动一个shell来解释请求的命令。同时提供了对读写数据的更多控制。

#include 
int pipe(int file_descriptor[2]);
pipe调用成功时返回0,失败返回-1并设置errno表明失败的原因。

EMFILE:进程使用的文件描述符过多

ENFILE:系统的文件表已满

EFAULT:文件描述符无效

两个返回的文件描述符通过file_descriptor连接起来。写到file_descriptor[1]中的所有数据都可以从file_descriptor[0]读出。数据基于先进先出原则,保证数据输入输出的顺序一致。管道的真正优势在于两个进程之间的数据传递。程序在原先的进程中创建管道,再调用fork创建新的进程,父进程向管道写入数据,子进程读取管道中的数据并输出,实现通过管道在两个进程之间传递数据。

#include 
#include 
#include 
#include 

int main()
{
	int data_processed;
	int file_pipes[2];
	const char some_data[] = "abc";
	char buffer[BUFSIZ + 1];
	pid_t fork_result;

	memset(buffer, '\0', sizeof(buffer));
	if(pipe(file_pipes) == 0)
	{
		fork_result = fork();
		if(fork_result == -1)
		{
			fprintf(stderr, "Fork failure");
			exit(EXIT_FAILURE);
		}
		//父进程写数据子进程读数据
		if(fork_result == 0)  //子进程
		{
			data_processed = read(file_pipes[0], buffer, BUFSIZ);
			printf("Read %d bytes: %s\n", data_processed, buffer);
			exit(EXIT_SUCCESS);
		}
		else //父进程
		{
			data_processed = write(file_pipes[1], some_data, strlen(some_data));
			printf("Wrote %d bytes\n", data_processed);
		}
	}
	exit(EXIT_SUCCESS);
}

3、父进程和子进程

前面关于管道的描述中,父进程和子进程运行的是同一个程序。通过pipe调用创建管道后,exec调用可以实现在子进程中运行一个与其父进程完全不同的另外一个程序。但是通过exec调用的乾需要知道应该访问哪个文件描述符,为了解决这个问题可以将文件描述符作为参数传递给用exec启动的程序。

4、管道关闭后的读操作

大多数从标准输入读取数据的程序并不知道有多少数据需要读取,所以采取的是读取数据——处理数据——读取数据的循环方法,直到没有数据可读为止。当没有数据可读时,read调用通常会阻塞,即它将暂停进程来等待直到有数据到达为止。如果管道的另一端已被关闭,也就是说没有进程打开这个管道并向它写入数据,这时read调用就会阻塞。但是这样的阻塞不是很有用,因此对一个已关闭写数据的通道做read调用时将返回0而不是阻塞。

如果跨越fork调用使用管道,就会有两个不同的文件描述符可以用于向管道写数据,一个在父进程中,另外一个在子进程中。只有把父子进程中的针对管道的写文件描述符都关闭,管道才会被认为是关闭的,对管道的read调用才会失败。

5、管道用作标准输入和输出

建立管道时,将其中一个管道文件描述符设置为一个已知值,一般是标准输入0或标准输出1。现调用dup函数可以实现将管道作为标准输入输出。

#include 
int dup(int file_descriptor);
int dup2(int file_descriptor_one, int file_descriptor_two);
》》》dup实现进程之间传递数据

标准输入的文件描述符总是0,而dup调用返回的新的文件描述符又总是使用最小可用的数字。因此,如果首先关闭文件描述符0再调用dup,那么新的文件描述符就将是数字0。因为新的文件描述符是复制一个已有的文件描述符,所以标准输入就会改为指向一个传递给dup函数的文件描述符所对应的文件或管道。我们创建了两个文件描述符,指向同一个文件或管道,而且其中之一是标准输入。

下面的示例程序将stdin文件描述符替换为自己创建的管道的读取端。而且对文件描述符进行清理,使得程序可以正确检测到管道中数据的结束。

#include 
#include 
#include 
#include 

int main()
{
	int data_processed;
	int file_pipes[2];
	const char some_data[] = "abc";
	pid_t fork_result;

	if(pipe(file_pipes) == 0)
	{
		fork_result = fork();
		if(fork_result == (pid_t)-1)
		{
			fprintf(stderr, "Fork failure");
			exit(EXIT_FAILURE);
		}
		if(fork_result == (pid_t)0)  //子进程
		{
			close(0); //子进程关闭其标准输入
			dup(file_pipes[0]); //将管道读取端设置为标准输入
			close(file_pipes[0]); // 关闭原先用来读数据的文件描述符
			close(file_pipes[1]); // 关闭子进程写数据文件描述符
			execlp("od", "od", "-c", (char *)0);
			exit(EXIT_FAILURE);
		}
		else //父进程
		{
			close(file_pipes[0]); // 父进程关闭读数据文件描述符
			data_processed = write(file_pipes[1], some_data, strlen(some_data));
			close(file_pipes[1]);
			printf("%d - wrote %d bytes\n", getpid(), data_processed);
		}
	}
	exit(EXIT_SUCCESS);
}
6、命名管道:FIFO

以上我们实现了在程序之间传递数据,这些相关程序都是由一个共同的祖先进程启动的。如果想在不相关的进程之间交换数据,我们需要通过命名管道来实现。

FIFO文件通常也称为命名管道(named pipe),是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但它的行为却和没有名字的管道类似。在命令行上创建命名管理使用命令

$ mknod filename p ——不在X/Open规范的命令列表中

$ mkfifo filename      ——通用命令

在程序中,可以使用以下两个函数调用。

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

下述程序调用mkfifo创建名为fifo的管道。再通过shell命令ls -lF /tmp/fifo查看管道文件的信息,可以看到输出信息为:prwxrwxr-x 1 user user 0 May  8 16:32 /tmp/fifo|,第一字符p代表这是一个命名管道。

#include 
#include 
#include 
#include 
#include 

int main()
{
	int res = mkfifo("/tmp/fifo", 0777);
	if(res == 0) printf("FIFO created\n");
	exit(EXIT_SUCCESS);
}
7、访问命名管道

由于命名管道出现在文件系统中,因此可以像使用平常文件一样在命令中使用管道。

》》》使用open打开FIFO文件

open不能以O_RDWR模式打开FIFO文件进行读写操作。如果一个管道以读写方式打开,进程就会从这个管道读回它自己的输入。如果需要在程序之间双向传递数据,最好使用一对管道,一个方向使用一个,或者采用先关闭再重新打开FIFO的方法明确地改变数据流的方向。打开FIFO文件和打开普通文件的另一个区别是是对open_flag的O_NONBLOCK选项的用法。open_flag有三种标志O_RDONLY、O_WRONLY和O_NONBLOCK,共有四种组合。

open(cosnt char *path, O_RDONLY);
这种情况,open调用将阻塞,除非有一个进程以写方式打开同一个FIFO,否则它不会返回。

open(const char *path, O_WRONLY);
这种情况,open调用将阻塞,直到有一个进程以读方式打开同一个FIFO为止。

open(const char *path, O_RDONLY | O_NONBLOCK);
这种情况,即使没有其他进程以写方式打开FIFO,这个open调用也将成功并立即返回。

open(const char *path, O_WRONLY | O_NONBLOCK);
这种情况,函数调用总是立即返回,但是如果没有进程以读方式打开FIFO文件,open调用将返回一个错误-1并且FIFO也不会被打开。如果有进程以读方式打开FIFO文件,就可以通过它返回的文件描述符对这个FIFO进行写操作。

下述程序通过使用带O_NONBLOCK标志的调用来同步两个进程,读进程和写进程在open调用处取得同步。

#include 
#include 
#include 
#include 
#include 
#include 

#define FIFO_NAME "/tmp/fifo"

int main(int argc, char *argv[])
{
	int i, res;
	int open_mode = 0;

	if(argc < 2)
	{
		fprintf(stderr, "Usage: %s \n", *argv);
		exit(EXIT_FAILURE);
	}
	for(i = 1; i < argc; i++) //传递参数
	{
		if(strncmp(*++argv, "O_RDONLY", 8) == 0)
			open_mode |= O_RDONLY;
		if(strncmp(*argv, "O_WRONLY", 8) == 0)
			open_mode |= O_WRONLY;
		if(strncmp(*argv, "O_NONBLOCK", 10) == 0)
			open_mode |= O_NONBLOCK;
	}
	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\n", getpid());
	res = open(FIFO_NAME, open_mode); //打开管道
	printf("Process %d result %d\n", getpid(), res);
	sleep(5);
	if(res != -1)
		(void) close(res); //关闭管道
	printf("Process %d finished\n", getpid());
	exit(EXIT_SUCCESS);
}
当一个Linux进程被阻塞时,它并不消耗CPU资源,因此采用这种方式进行进程同步是非常有效的。

》》》对FIFO进程读写操作

对一个空的、阻塞的FIFO的read调用将等待,直到有数据可读时才继续执行。对一个空的、非阻塞的FIFO的read调用将立刻返回0。对一个完全阻塞FIFO的write调用将等待,直到数据可以被写入时才继续执行。如果FIFO不能接收所有数据,它将按下面的规则执行:

1)如果请求写入的数据的长度小于等于PIPE_BUF字节,调用失败,数据不能写入。

2)如果请求写入的数据的长度大于PIPE_BU字节,将写入部分数据,返回实际写入的字节数,返回值了可能是0。

如果几个不同的程序尝试同时向FIFO写数据,保证来自不同程序的数据块不相互交错是非常重要的。Linux规定在一个以O_WRONLY方式打开的FIFO中,如果写入的数据长度小于等于PIPE_BUF,那么或者写入全部数据,或者一个字节都不写入。如果能保证所有的写请求是发往一个阻塞的FIFO的,并且每个写请求的数据长度小于等于PIPE_BUF字节,系统就可以确保数据不会交错在一起。因此通常将每次通过FIFO传递的数据长度限制为PIPE_BUF字节,除非只使用一个写进程和一个读进程。


8、总结

本文介绍了如何使用管道在进程间传递数据,包括通过popen或pipe调用创建未命名管道、如何使用管道和dup调用把数据从一个程序传递到另一个程序。最后主要介绍命名管道及如何在不相关的程序之间传递数据,并通过相关实例说明实现原理。



你可能感兴趣的:(Linux程序设计,Linux程序设计)