Linux进程间通信 -- 使用匿名管道

在前面,介绍了一种进程间的通信方式:使用信号,我们创建通知事件,并通过它引起响应,但传递的信息只是一个信号值。这里将介绍另一种进程间通信的方式——匿名管道,通过它进程间可以交换更多有用的数据。

一、什么是管道

如果你使用过Linux的命令,那么对于管道这个名词你一定不会感觉到陌生,因为我们通常通过符号“|"来使用管道,但是管理的真正定义是什么呢?管道是一个进程连接数据流到另一个进程的通道,它通常是用作把一个进程的输出通过管道连接到另一个进程的输入。

举个例子,在shell中输入命令:ls -l | grep string,我们知道ls命令(其实也是一个进程)会把当前目录中的文件都列出来,但是它不会直接输出,而是把本来要输出到屏幕上的数据通过管道输出到grep这个进程中,作为grep这个进程的输入,然后这个进程对输入的信息进行筛选,把存在string的信息的字符串(以行为单位)打印在屏幕上。

二、使用popen()函数

1、popen()函数和pclose()函数介绍

有静就有动,有开就有关,与此相同,与popen()函数相对应的函数是pclose()函数,它们的原型如下:

#include <stdio.h>
FILE* popen(const char *command, const char *open_mode);
int pclose(FILE *stream_to_close);

poen()函数允许一个程序将另一个程序作为新进程来启动,并可以传递数据给它或者通过它接收数据。command是要运行的程序名和相应的参数。open_mode只能是"r(只读)"和"w(只写)"的其中之一。注意,popen()函数的返回值是一个FILE类型的指针,而Linux把一切都视为文件,也就是说我们可以使用stdio I/O库中的文件处理函数来对其进行操作。

如果open_mode是"r",主调用程序就可以使用被调用程序的输出,通过函数返回的FILE指针,就可以能过stdio函数(如fread)来读取程序的输出;如果open_mode是"w",主调用程序就可以向被调用程序发送数据,即通过stdio函数(如fwrite)向被调用程序写数据,而被调用程序就可以在自己的标准输入中读取这些数据。

pclose()函数用于关闭由popen创建出的关联文件流。pclose()只在popen启动的进程结束后才返回,如果调用pclose()时被调用进程仍在运行,pclose()调用将等待该进程结束。它返回关闭的文件流所在进程的退出码。

2、例子

很多时候,我们根本就不知道输出数据的长度,为了避免定义一个非常大的数组作为缓冲区,我们可以以块的方式来发送数据,一次读取一个块的数据并发送一个块的数据,直到把所有的数据都发送完。下面的例子就是采用这种方式的数据读取和发送方式。源文件名为popen.c,代码如下:

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

int main()
{
	FILE *read_fp = NULL;
	FILE *write_fp = NULL;
	char buffer[BUFSIZ + 1];
	int chars_read = 0;

	// 初始化缓冲区
	memset(buffer, ‘\0‘, sizeof(buffer));

	// 打开ls和grep进程
	read_fp = popen("ls -l", "r");
	write_fp = popen("grep rwxrwxr-x", "w");

	// 两个进程都打开成功
	if (read_fp && write_fp)
	{
		// 读取一个数据块
		chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
		while (chars_read > 0)
		{
			buffer[chars_read] = ‘\0‘;

			// 把数据写入grep进程
			fwrite(buffer, sizeof(char), chars_read, write_fp);

			// 还有数据可读,循环读取数据,直到读完所有数据
			chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
		}

		// 关闭文件流
		pclose(read_fp);
		pclose(write_fp);

		exit(EXIT_SUCCESS);
	}

	exit(EXIT_FAILURE);
}

运行结果如下:

从运行结果来看,达到了信息筛选的目的。程序在进程ls中读取数据,再把数据发送到进程grep中进行筛选处理,相当于在shell中直接输入命令:ls -l | grep rwxrwxr-x。

3、popen()的实现方式及优缺点

当请求popen()调用运行一个程序时,它首先启动shell,即系统中的sh命令,然后将command字符串作为一个参数传递给它。

这样就带来了一个优点和一个缺点。优点是:在Linux中所有的参数扩展都是由shell来完成的。所以在启动程序(command中的命令程序)之前先启动shell来分析命令字符串,也就可以使各种shell扩展(如通配符)在程序启动之前就全部完成,这样我们就可以通过popen()启动非常复杂的shell命令。

而它的缺点就是:对于每个popen()调用,不仅要启动一个被请求的程序,还要启动一个shell,即每一个popen()调用将启动两个进程,从效率和资源的角度看,popen()函数的调用比正常方式要慢一些。

三、pipe()调用

如果说popen()是一个高级的函数,pipe()则是一个底层的调用。与popen()函数不同的是,它在两个进程之间传递数据不需要启动一个shell来解释请求命令,同时它还提供对读写数据的更多的控制。

pipe()函数的原型如下:

#include <unistd.h>
int pipe(int file_descriptor[2]);

我们可以看到pipe()函数的定义非常特别,该函数在数组中墙上两个新的文件描述符后返回0,如果返回返回-1,并设置errno()来说明失败原因。

数组中的两个文件描述符以一种特殊的方式连接起来,数据基于先进先出的原则,写到file_descriptor[1]的所有数据都可以从file_descriptor[0]读回来。由于数据基于先进先出的原则,所以读取的数据和写入的数据是一致的。

特别提醒:

1、从函数的原型我们可以看到,它跟popen函数的一个重大区别是,popen()函数是基于文件流(FILE)工作的,而pipe是基于文件描述符工作的,所以在使用pipe后,数据必须要用底层的read()和write()调用来读取和发送。

2、不要用file_descriptor[0]写数据,也不要用file_descriptor[1]读数据,其行为未定义的,但在有些系统上可能会返回-1表示调用失败。数据只能从file_descriptor[0]中读取,数据也只能写入到file_descriptor[1],不能倒过来。

例子:

首先,我们在原先的进程中创建一个管道,然后再调用fork()创建一个新的进程,最后通过管道在两个进程之间传递数据。源文件名为pipe.c,代码如下:

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

int main(int argc, char **argv)
{
	ssize_t data_processed = 0;
	int filedes[2];
	const char data[] = "Hello pipe!\n";
	char buffer[BUFSIZ + 1];
	pid_t pid;

	// 清空缓冲区
	memset(buffer, ‘\0‘, sizeof(buffer));

	if (pipe(filedes) == 0)
	{
		// 创建管道成功
		// 调用fork 创建子进程
		pid = fork();
		if (pid == -1)
		{
			fprintf(stderr, "Fork failure\n");
			exit(EXIT_FAILURE);
		}

		if (pid == 0)
		{
			// 子进程
			// 读取数据
			data_processed = read(filedes[0], buffer, BUFSIZ);
			printf("Read %ld bytes: %s\n", data_processed, buffer);
			exit(EXIT_SUCCESS);
		}
		else
		{
			// 父进程
			// 写数据
			data_processed = write(filedes[1], data, strlen(data));
			printf("Wrote %ld bytes: %s\n", data_processed, data);

			// 休眠2秒, 主要是为了等待子进程结束, 这样做也只是纯粹为了输出好看而已
			// 父进程其实没有必要等待子进程结束
			sleep(2);
			exit(EXIT_SUCCESS);
		}
	}
	else
	{
		fprintf(stderr, "Pipe failure\n");
		exit(EXIT_FAILURE);
	}
}

运行结果为:

可见,子进程读取了父进程写到filedes[1]中的数据,如果在父进程中没有sleep()语句,父进程可能在子进程结束前结束,这样你可能将看到两个输入之间有一个命令提示符分隔。

四、把管道用作标准输入和标准输出

下面来介绍一种用管道来连接两个进程的更简洁方法,我们可以把文件描述符设置为一个已知值,一般是标准输入0或标准输出1。这样做最大的好处是可以调用标准程序,即那些不需要以文件描述符为参数的程序。

为了完成这个工作,我们还需要两个函数的辅助,它们分别是dup函数或dup2函数,它们的原型如下:

#include <unistd.h>
int dup(int file_descriptor);
int dup2(int file_descriptor_one, int file_descriptor_two);

dup调用创建一个新的文件描述符与作为它的参数的那个已有文件描述符指向同一个文件或管道。对于dup()函数而言,新的文件描述总是取最小的可用值。而dup2()所创建的新文件描述符或者与int file_descriptor_two相同,或者是第一个大于该参数的可用值。所以当我们首先关闭文件描述符0后调用dup(),那么新的文件描述符将是数字0。

例子

在下面的例子中,首先打开管道,然后fork()一个子进程,然后在子进程中,使标准输入指向读管道,然后关闭子进程中的读管道和写管道,只留下标准输入,最后调用execlp()函数来启动一个新的进程od,但是od并不知道它的数据来源是管道还是终端。父进程则相对简单,它首先关闭读管道,然后在写管道中写入数据,再关闭写管道就完成了它的任务。源文件为pipe2.c,代码如下:

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

int main(int argc, char **argv)
{
	ssize_t data_proccessed = 0;
	int pipes[2];
	const char data[] = "123";
	pid_t pid;

	if (pipe(pipes) == 0)
	{
		pid = fork();
		if (pid == -1)
		{
			fprintf(stderr, "Fork failure\n");
			exit(EXIT_FAILURE);
		}

		if (pid == 0)
		{
			// 子进程中
			// 使标准输入指向 filedes[0]
			close(0);
			dup(pipes[0]);

			// 关闭 pipes[0] 和 pipes[1], 只剩下标准输入
			close(pipes[0]);
			close(pipes[1]);

			// 启动新进程od
			execlp("od", "od", "-c", 0);
			exit(EXIT_FAILURE);
		}
		else
		{
			// 关闭 pipes[0], 因为父进程不用读取数据
			close(pipes[0]);
			data_proccessed = write(pipes[1], data, strlen(data));

			// 写完数据后, 关闭 pipes[1]
			close(pipes[1]);
			printf("%d -Worte %ld bytes\n", getpid(), data_proccessed);
		}
	}
	else
	{
		fprintf(stderr, "Pipe failure\n");
		exit(EXIT_FAILURE);
	}
}

运行结果为:

从运行结果中可以看出od进程正确地完成了它的任务,与在shell中直接输入od -c和123的效果一样。

五、关于管道关闭后的读操作的讨论

现在有这样一个问题,假如父进程向管道file_pipe[1]写数据,而子进程在管道file_pipe[0]中读取数据,当父进程没有向file_pipe[1]写数据时,子进程则没有数据可读,则子进程会发生什么呢?再者父进程把file_pipe[1]关闭了,子进程又会有什么反应呢?

当写数据的管道没有关闭,而又没有数据可读时,read()调用通常会阻塞,但是当写数据的管道关闭时,read()调用将会返回0而不是阻塞。注意,这与读取一个无效的文件描述符不同,read()一个无效的文件描述符返回-1。

六、匿名管道的缺陷

看了这么多相信大家也知道它的一个缺点,就是通信的进程,它们的关系一定是父子进程的关系,这就使得它的使用受到了一点的限制,但是我们可以使用命名管道来解决这个问题。命名管道将在下一篇文章:Linux进程间通信——使用命名管道 中介绍。

参考:

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

http://blog.csdn.net/zhouqi_2011/article/details/7039157

时间: 2024-12-15 01:32:46

Linux进程间通信 -- 使用匿名管道的相关文章

Linux进程间通信 -- 使用命名管道

在前一篇文章—— Linux进程间通信 -- 使用匿名管道 中,我们看到了如何使用匿名管道来在进程之间传递数据,同时也看到了这个方式的一个缺陷,就是这些进程都由一个共同的祖先进程启动,这给我们在不相关的的进程之间交换数据带来了不方便.这里将会介绍进程的另一种通信方式——命名管道,来解决不相关进程间的通信问题. 一.什么是命名管道 命名管道也被称为FIFO文件,它是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但是它的行为却和之前所讲的没有名字的管道(匿名管道)类似. 由于Linux中所有

进程间通信IPC—匿名管道(pipe)和命名管道(fifo)

管道内部如何实现-大小,组织方式,环形队列? 一.进程间通信有多种方式,本文主要讲解对管道的理解.管道分为匿名管道和命名管道. (1)管道( pipe ):又称匿名管道.是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用.进程的亲缘关系通常是指父子进程关系. (2)命名管道 (named pipe或FIFO) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信. 二.管道 1. 管道的特点: (1)管道是半双工的,数据只能向一个方向流动:双方通信时,需要

练习--LINUX进程间通信之无名管道PIPE

IBM上放的这个系统不错,刚好可以系统回温一下LINUX的系统知识. http://www.ibm.com/developerworks/cn/linux/l-ipc/part1/ 感觉年纪大了,前几年看的LINUX内核和系统的东东,忘了很多,要慢慢转化成永久记忆才可以. 今天,又拿起<LINUX内核设计与实现>,慢慢啃下去. ~~~~~~~~~~~~~~ 进程通信有如下一些目的:A.数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几M字节之间B.共享数据:多个进程想

Linux进程通信----匿名管道

Linux进程通信中最为简单的方式是匿名管道 匿名管道的创建需要用到pipe函数,pipe函数参数为一个数组 表示的文件描述字.这个数组有两个文件描述字,第一个是用 于读数据的文件描述符第二个是用于写数据的文件描述符. 不能将用于写的文件描述符进行读操作或者进行读的文件描述 符进写操作,这样都会导致错误. 关于匿名管道的几点说明: 1.匿名管道是半双工的,即一个进程只能读,一个进程只能写    要实现全双工,需要两个匿名管道. 2.只能在父子进程或者兄弟进程进行通信. 3.在读的时候关闭写文件描

进程间通信之匿名管道

管道由pipe函数创建 #include<unistd.h> int pipe(int filedes[2]); 调用pipe函数在内核中开辟一块缓冲区(就是管道)用于通信,filedes[0]指向管道的读端,filedes[1]指向管道的写端.pipe函数调用成功返回0,调用失败返回-1. 比如,父进程关闭读端,子进程关闭写端.代码如下: 1   #include<stdio.h> 2   #include<string.h> 3   #include<unis

简述Linux进程间通信之命名管道FIFO

上文聊到管道(pipe),可以使有亲缘关系的进程间进行通信. 对于没有亲缘关系的进程如何通信?本文来聊一聊命名管道FIFO. 一.概念 命名管道FIFO,提供一个路径名与之关联,以文件形式存储于文件系统中. 一个进程以r方式打开,另一个程序以w方式打开,即可在两个进程之间建立管道. 通过以fifo文件作为媒介,可以使任意两个进程通过该文件进行通信. 命名管道(fifo)特性与管道(pipe)类似,不必赘述. 下面我们看FIFO如何进行进程间通信,首先来介绍一下所用到的函数: 二.函数原型 #in

Python进程间通信之匿名管道

匿名管道 管道是一个单向通道,有点类似共享内存缓存.管道有两端,包括输入端和输出端.对于一个进程的而言,它只能看到管道一端,即要么是输入端要么是输出端. os.pipe()返回2个文件描述符(r, w),表示可读的和可写的.示例代码如下: #!/usr/bin/python import time import os def child(wpipe): print('hello from child', os.getpid()) while True: msg = 'how are you\n'

在Linux下的进程间通信之匿名管道

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到.所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2 在从内核缓冲区把数据读走. 管道是一种最基本的IPC机制,由pipe函数创建: 调用pippe函数时在内核中开辟一块缓冲去(称为管道)用于通信,它有一个读端,一个写端,然后通过fileds参数传出给用户程序两个文件描述符,fileds[0]指向管道的读端,fileds[1]指向管道的写端,所以管道在用户程序看起来

进程间通信_02匿名管道

相关介绍                      分配一块独立于进程的内存,对内存的操作方式和对文件的操作方式类似.有如下特点: 1  只能用于父子进程之间. 2  管道的大小是有限制(一个页面的大小,4K字节). 3  数据是单向流动的. 4  传送的是无格式字节流,需要双方事先确定好长度 格式等. 操作函数 1 创建一个管道 #include <unistd.h> int pipe( int fildes[2] ); //出参,用于管道读写端的文件描述符: 返回值:0(成功)/-1(失败