Linux 进程间通信基础(三)--pipe管道

最近正好有一些空余时间,在这里总结一下曾经使用过的Linux进程间通信的几种方法,贴出来帮助有需要的人,也有助于自己总结经验加深理解。上一次我们梳理了popen管道的相关知识,这一次梳理pipe管道。

(一)概念

我们先来简单说一下管道的概念。

管道其实是一个很形象的表示术语。当一个进程把它的数据流连接到另一个进程时,我们就说他们构架了管道连接。管道通常不是杂乱的,是能单向。意思就是管道通常只能把一个进程的输出连接到另一个进程的输入,而不能把输入连接输入,输出连接输出。可以把进程想象成一个水管,而其中流动的水就是数据流,我们我们想把一个水管中的水引入另外一个水管,那么只能在他们中间架设小型的水管,而且被引入的水管一端一定要低于引入的水管一端。这个水管就像是管道,从一个进程的输出端连接到领一个进程的输入端。可以参考下面这张简图:


管道其实有很多种,最常用的应该就是shell中的"|"。他其实就是一个管道符,将前面的表达式的输出,引入后面表达式当作输入,比如我们常用的"ps aux|grep ssh"可以查看ssh的相关进程。但是我们常用在进程间通信管道的有两种,一种是pipe管道,又可以叫做亲族管道,与之对应的则是fifo管道,又可以叫做公共管道。

接下来我们来说一下pipe管道。

pipe管道之所以又叫做亲族管道,是因为它只适用于有亲族关系的线程中。比如父进程和子进程,兄弟进程之间。这是你可能会质疑,说Linux系统下的所有进程严格来讲都是属于亲族进程,因为他们实际上都是有init线程创建而来的。的确如此,但是那个亲戚关系有些过于远了。pipe只适用于近亲之间,比如直接使用fork()调用产生的子进程和其父进程之间。

(二)pipe管道的创建与读取、写入数据流

那么我们来尝试使用pip管道。我们可以使用函数pipe()来创建一个管道:

int pipe( int file_descriptor[2] );

其中,

--file_descriptor是一个由两个整数类型的文件描述符组成的数组指针,表示管道的读和写的数据流,可以理解为管道的入口和出口。file_descriptor[0]是读取数据的描述符,file_descriptor[1]是写入数据的描述符。所有写入file_descriptor[1]的数据流,都可以通过file_descriptor[0]读出来,并且数据遵循先入先出的原则。

现在,我们来尝试写一个U型管道,即我们在同一个线程中先向管道写入一些数据,然后再读取它,这样管道的入口和出口都执行一个线程,就像一个U型一样。我们创建一个pipe.c文件:

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

int main()
{
	int result = 0;

	int pipe_entrance[2];

	char read_buffer[24];
	char write_buffer[24];
	int read_len = 0;
	int write_len = 0;

	/* 创建管道 */
	result = pipe( pipe_entrance );
	if( result != 0 )
	{
		printf( "Can't create a pipe.\n" );
		return 1;
	}

	do
	{
		/* 向管道中写入数据 */
		memset( write_buffer, 0, 24 );
		printf( "Please input something:\n" );
		scanf( "%s", write_buffer );
		write_len = write( pipe_entrance[1], write_buffer, strlen( write_buffer ) );
		printf( "Write:%s\n", write_buffer );

		/* 从管道中读取我们刚刚写入的数据 */
		memset( read_buffer, 0, 24 );
		read_len = read( pipe_entrance[0], read_buffer, 24 );
		printf( "Read:%s\n", read_buffer );

	}
	while( memcmp( write_buffer, "close", 5 ) != 0 );

	close( pipe_entrance[0] );
	close( pipe_entrance[1] );

	return 0;
}

现在我们来尝试编译运行一下pipe.c,并且输入一些字符串,最后输入"close"关闭pipe程序:

root@Server:/home/root/workspace/pipe/pipe# gcc pipe.c -o pipe
root@Server:/home/root/workspace/pipe/pipe# ./pipe
Please input something:
hello
Write:hello
Read:hello
Please input something:
hi
Write:hi
Read:hi
Please input something:
fuck
Write:fuck
Read:fuck
Please input something:
close
Write:close
Read:close

可以看到我们输入的字符串都被pipe_entrance[1]传入管道,并从pipe_entrance[0]中再读取出来。

(二)利用管道在进程间传递数据

现在我们来尝试利用管道在不同的进程间传递数据而不是像上面那样在一个进程间先写再读了。可能你已经观察到,我们在上面是通过随机创建的整数文件描述符来作为管道的入口和出口,也就是说这个入口和出口只在这个文件中有效。你可能会问他是如何在进程间传递数据的呢。这也就是我们上面所说的,pipe管道只能在亲族线程之间传递数据。比如我们使用fork()函数来创建一个子进程,子进程会复制父进程的相关数据并继续执行,那时子进程与父进程就同时拥有了上面的整数文件描述符数组,可以进行通信了。

现在我们就尝试创建利用fork()函数创建一个子进程,然后使用pipe管道在两个进程间通信,我们在父进程中向管道写入数据, 子进程中读取管道中的数据。现在我们创建一个新的文件pipe2.c,为了区分父子进程,我们在打印中加入进程的pid号:

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

int main()
{
	int result = 0;

	int pipe_entrance[2];

	char read_buffer[24];
	char write_buffer[24];
	int read_len = 0;
	int write_len = 0;

	/* 创建管道 */
	result = pipe( pipe_entrance );
	if( result != 0 )
	{
		printf( "Can't create a pipe.\n" );
		return 1;
	}

	/* 利用fork()函数创建一个线程 */
	result = fork();
	if( result == -1 )
	{
		printf( "Can't create another thread.\n" );
		close( pipe_entrance[0] );
		close( pipe_entrance[1] );
		return 1;
	}
	/* 如果fork()函数返回值为0,则说明现在处在子进程 */
	else if( result == 0 )
	{
		/* 子进程不需要向管道写入数据,所以关闭写入文件描述符 */
		close( pipe_entrance[1] );
		memset( read_buffer, 0, 24 );
		/* 从管道读取数据 */
		read_len = read( pipe_entrance[0], read_buffer, 24 );
		printf( "Read(%d):%s\n", getpid(), read_buffer );
	}
	else
	{
		/* 父进程不需要从管道读取数据,所以关闭读取文件描述符 */
		close( pipe_entrance[0] );
		memset( write_buffer, 0, 24 );
		printf( "Please input something:\n" );
		scanf( "%s", write_buffer );
		/* 向管道写入数据 */
		write_len = write( pipe_entrance[1], write_buffer, strlen( write_buffer ) );
		printf( "Write(%d):%s\n", getpid(), write_buffer );
	}

	close( pipe_entrance[0] );
	close( pipe_entrance[1] );

	return 0;
}

现在我们来编译运行一下它,输入"hello":

root@Server:/home/root/workspace/pipe/pipe# gcc pipe2.c -o pipe2
root@Server:/home/root/workspace/pipe/pipe# ./pipe2
Please input something:
hello
Write(3684):hello
root@Server:/home/root/workspace/pipe/pipe# Read(3685):hello

可以看到父进程写入了"hello",而子进程读取到了它。

(三)利用管道进行数据的交互

到目前为止我们都是利用管道从一个线程向领一个线程传递数据,这种单向传递的应用可能不是特别广泛。所以我们不禁会产生这样的思考,管道可以双向传递数据吗?答案是可以,管道其实并不真是我们抽象的那种管道,它更像是一种队列。当我们通过file_descriptor[1]写入数据时,数据就加入队列,你从任何一方都可以通过file_descriptor[0]读取他。同样的,你从任何一方都可以通过file_descriptor[1]向管道中写入数据。所以假如你能够保证写入和读取的顺序,那么管道是可以双向通信的。不过你这是应该也已经发现了,要保证数据写入和读取的顺去和方向性是非常困难,而且极易出错的。我们无法保证自己写入的数据不被自己读取,所以利用管道进行数据交互的最好方法就是创建两个管道,而一个管道只负责一个方向通信。

创建两个管道和上面一个管道的方法大同小异,这里就不再过多描述了。但是我还是给出一个两个管道的例子写在下面,以便于自己以后或者有需要的人进行参考。我们创建第三个文件pipe3.c:

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

int main()
{
	int result = 0;

	/* 这里我们用tofa表示流向父进程的管道,toch表示流向子进程的管道 */
	int pipe_entrance_tofa[2];
	int pipe_entrance_toch[2];

	char read_buffer[24];
	char write_buffer[24];
	int read_len = 0;
	int write_len = 0;

	/* 创建管道 */
	result = pipe( pipe_entrance_tofa );
	if( result != 0 )
	{
		printf( "Can't create a pipe.\n" );
		return 1;
	}
        result = pipe( pipe_entrance_toch );
	if( result != 0 )
	{
		printf( "Can't create a pipe.\n" );
		return 1;
	}

	/* 利用fork()函数创建一个线程 */
	result = fork();
	if( result == -1 )
	{
		printf( "Can't create another thread.\n" );
		close( pipe_entrance_tofa[0] );
		close( pipe_entrance_tofa[1] );
		close( pipe_entrance_toch[0] );
		close( pipe_entrance_toch[1] );
		return 1;
	}
	/* 如果fork()函数返回值为0,则说明现在处在子进程 */
	else if( result == 0 )
	{
		/* 子进程不需要读取流入父进程的管道的数据,所以关闭读取文件描述符 */
		close( pipe_entrance_tofa[0] );
		/* 子进程不需要向流入子进程的管道写入数据,所以关闭写入文件描述符 */
		close( pipe_entrance_toch[1] );
		
		/* 向管道写入数据 */
		memset( write_buffer, 0, 24 );
		strcpy( write_buffer, "child send." );
		write_len = write( pipe_entrance_tofa[1], write_buffer, strlen( write_buffer ) );
		printf( "Write(%d):%s\n", getpid(), write_buffer );
		
		/* 从管道读取数据 */
		memset( read_buffer, 0, 24 );
		read_len = read( pipe_entrance_toch[0], read_buffer, 24 );
		printf( "Read(%d):%s\n", getpid(), read_buffer );
	}
	/* 在父进程 */
	else
	{
		/* 父进程不需要向流入父进程的管道写入数据,所以关闭写入文件描述符 */
		close( pipe_entrance_tofa[1] );
		/* 父进程不需要读取流入子进程的管道的数据,所以关闭读取文件描述符 */
		close( pipe_entrance_toch[0] );
		
		/* 从管道读取数据 */
		memset( read_buffer, 0, 24 );
		read_len = read( pipe_entrance_tofa[0], read_buffer, 24 );
		printf( "Read(%d):%s\n", getpid(), read_buffer );

		/* 向管道写入数据 */
		memset( write_buffer, 0, 24 );
		strcpy( write_buffer, "father send." );
		write_len = write( pipe_entrance_toch[1], write_buffer, strlen( write_buffer ) );
		printf( "Write(%d):%s\n", getpid(), write_buffer );
	}

	close( pipe_entrance_tofa[0] );
	close( pipe_entrance_tofa[1] );
	close( pipe_entrance_toch[0] );
	close( pipe_entrance_toch[1] );

	return 0;
}

编译运行一下:

root@Server:/home/root/workspace/pipe/pipe# gcc pipe3.c -o pipe3
root@Server:/home/root/workspace/pipe/pipe# ./pipe3
Read(3726):child send.
Write(3726):father send.
root@Server:/home/root/workspace/pipe/pipe# Write(3727):child send.
Read(3727):father send.

可以看到进行了双向通信。

猜你喜欢

转载自blog.csdn.net/hyklose/article/details/80346048