进程通信 [ fork() 管道( pipe() ) FIFO ]

X86/Debian Linux/gcc


1 进程通信手段

每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcessCommunication)。

进程通信 [ fork() 管道( pipe() ) FIFO ]_第1张图片

Figure1.进程通信机制

进程间通信必须通过内核提供的通道,而且必须有一种办法在进程中标识内核提供的某个通道


(1) 进程通信手段总结(抄)

  • 父进程通过fork可以将打开文件的描述符传递给子进程
  • 子进程结束时,父进程调用wait可以得到子进程的终止信息
  • 几个进程可以在文件系统中读写某个共享文件,也可以通过给文件加锁来实现进程间同步
  • 进程之间互发信号,一般使用SIGUSR1SIGUSR2实现用户自定义功能
  • 管道
  • FIFO
  •  mmap函数,几个进程可以映射同一内存区
  • SYS V IPC,以前的SYSV UNIX系统实现的IPC机制,包括消息队列、信号量和共享内存,现在已经基本废弃
  • UNIX Domain Socket目前最广泛使用的IPC机制

此笔记练习管道和FIFO。


(1) 管道

管道是一种最基本的IPC机制,由pipe函数创建:

int pipe(int filedes[2]);

调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过filedes参数传出给用户程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端(很好记,就像0是标准输入1是标准输出一样)。所以管道在用户程序看起来就像一个打开的文件,通过read(filedes[0]);或者write(filedes[1]);向这个文件读写数据其实是在读写内核缓冲区。


开辟了管道之后实现两个进程间的通信,一般是用下列步骤:

进程通信 [ fork() 管道( pipe() ) FIFO ]_第2张图片
Figure2.管道用于进程通信步骤

(2) FIFO和UNIX Domain Socket

文件系统中的路径名是全局的,各进程都可以访问,因此可以用文件系统中的路径名来标识一个IPC通道。


用mkfifo 来创建一个FIFO

mkfifo fifo

ls  -l  fifo

prw-r--r--  1 lly lly 0 Aug 10 16:10 fifo

FIFO文件在磁盘上没有数据块,仅用来标识内核中的一条通道,各进程可以打开这个文件进行read/write,实际上是在读写内核通道(根本原因在于这个file结构体所指向的read、write函数和常规文件不一样),这样就实现了进程间通信。


UNIXDomain Socket和FIFO的原理类似,也需要一个特殊的socket文件来标识内核中的通道。这些文件在磁盘上也没有数据块。


2 利用管道实现进程通信

(1) PIPE(2)  LinuxProgrammer’s Manual

[1] 头文件及原型

#include <unistd.h>

int pipe(int filedes[2]);

[2] 功能简述

进程通信的一种手段,见1.1。


[3] 返回值

pipe函数调用成功返回0,调用失败返回-1。


(2) pipe()用于进程通信的一些情况

[1] 父进程写父进程读

/* Filename:	pipe.c
 * Brife:	Create one pipe to be used communication beteween two progress
 * Author:	One fish
 * Date:	2014.8.10 Sunday
 */
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#define MAXPPSIZE	80
#define	MSG		"Hello, world\n"



int main(void)
{
	int	n;
	int	fd[2];
	pid_t	pid;
	char	pp_content[MAXPPSIZE];

	if (pipe(fd) < 0) {
		perror("pipe");
		exit(1);
	}

	if ( (pid = fork() ) < 0 ) {
		perror("fork");
		exit(1);
	}

	if (pid > 0) {	//In parent progress

		write(fd[1], MSG, strlen(MSG) );
		n	= read(fd[0], pp_content, MAXPPSIZE);
		write(STDOUT_FILENO, pp_content, n);

	} else {	//In child progress
		;
	}
	
	return 0;
}

  • ssize_t  read(int  fd, void *buf, size_t  count);尝试着从文件描述符fd哪里读到count字节保存到缓冲区buf内。当函数执行成功时,函数返回所读到数据的字节数m(读到文件末尾则返回0),并且使文件的位置向前移动m个位置。如果m  < count并不一定代表着错误,遇到以下情况时m 会小于count:只有m个可读字节(可能是到了文件末尾),也有可能正在读一个管道,或者在读终端,或者read()被一个信号打断。如果函数执行错误则返回-1。
  • 父进程往管道的写端写入数据,然后再从管道的读端将数据读出来。

程序运行结果如下:

Hello, world

 

[2] 父进程写,子进程读

将父进程调用fork()后的一段代码改为:

	if (pid > 0) {	//In parent progress
		write(fd[1], MSG, strlen(MSG) );
		wait(NULL);
	} else {	//In child progress
		n	= read(fd[0], pp_content, MAXPPSIZE);
		write(STDOUT_FILENO, pp_content, n);
	}

根据fork创建子进程的机制,父子进程同时拥有管道的读、写端描述符。程序运行结果如下:

Hello, world

这个Hello world是子进程读出来并输出来的。


[3] 父进程写,子进程读,子进程读

将父进程调用fork()后的一段代码改为:

	if (pid > 0) {	//In parent progress
		write(fd[1], MSG, strlen(MSG) );
		wait(NULL);
		
		n	= read(fd[0], pp_content, MAXPPSIZE);
		write(STDOUT_FILENO, pp_content, n);
	} else {	//In child progress
		n	= read(fd[0], pp_content, MAXPPSIZE);
		write(STDOUT_FILENO, pp_content, n);
		
		//Read again
		printf("Child read again\n");
		n	= read(fd[0], pp_content, MAXPPSIZE);
		write(STDOUT_FILENO, pp_content, n);
	}

程序运行结果如下:

Hello, world

Child read again

 

如果是在Linux字符界面下,可以看到光标在最后一行闪动,说明子进程阻塞。管道读端将数据读完后,再次从管道读端read就会遭遇阻塞


[4] 父进程写子进程读,子进程写父进程读

将父进程调用fork()后的一段代码改为:

	if (pid > 0) {	//In parent progress
		write(fd[1], MSG, strlen(MSG) );
		wait(NULL);

		printf("After child, read pipe's data which child write in:\n");
		n	= read(fd[0], pp_content, MAXPPSIZE);
		write(STDOUT_FILENO, pp_content, n);
	} else {	//In child progress

		printf("Read pipe's data which parent write in:\n");
		n	= read(fd[0], pp_content, MAXPPSIZE);
		write(STDOUT_FILENO, pp_content, n);
		
		//Child progress write data for parent progress
		write(fd[1], MSG, strlen(MSG));
	}

程序运行结果如下:

Read pipe's data which parent write in:

Hello, world

After child, read pipe's data which child write in:

Hello, world

可见,在这个例子里面,父子进程至少可以用管道实现一个轮回的相互通信


3 利用FIFO实现进程通信

(1) 创建FIFO文件

用mkfifo 来创建一个FIFO

mkfifo fifo

ls  -l  fifo

prw-r--r--  1 lly lly 0 Aug 10 16:10 fifo


(2) FIFO进程通信代码

/*Filename:	fifo.c
 *Brife:	Two progress communicate by fifo
 *Author:	One fish
 *Date:		2014.8.10 Sunday
 */
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>



#define	FIFO_FILE	"fifo"
#define	MSG		"Hello, world\n"
#define MAX_CT		20

int main(void)
{
	int 	fd;
	pid_t	pid;
	

	fd	= open(FIFO_FILE, O_RDWR);
	if (-1 == fd) {
		perror("open");
		exit(1);
	}
	
	write(fd, "HW\n", 3);

	if ( ( pid = fork() ) < 0 ) {
		perror("fork");
		exit(1);
	}

	if (pid > 0) {	//Parent 
		write(fd, MSG, strlen(MSG));
		wait(NULL);
	} else {	//Child
		int n;
		char buf[MAX_CT];
		n	= read(fd, buf, MAX_CT);
		write(STDOUT_FILENO, buf, n);
	}
	return 0;
}

程序运行结果:

HW

Hello, world

在父进程中写write(fd, "HW\n",3);语句主要是为了测试如果子进程先运行的情况。手动多运行了几次都是以上运行结果(虽然很可能是碰巧)。


[2014.8.10– 15.05  --- 2014.8.10– 17.15]
LCNote Over.

你可能感兴趣的:(进程通信 [ fork() 管道( pipe() ) FIFO ])