操作系统-3.2 进程间通信-管道

139 阅读6分钟

概念

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

线程通信:用户空间就可以实现互相通信,比如全局变量。

进程间通信分类

在用户空间是不能实现进程间通信的,只能通过Linux内核空间,创建对应的对象,实现进程间通信;**对象不同,通信方式名字也就不一样。**比如内核创建管道对象,对应的就是管道通信。

  • 单机模式下的进程通信(只有一个linux内核)

    • 管道通信:无名管道、有名管道(文件系统中有名);
    • 信号通信:包括信号的发送、接收、处理;
    • IPC通信:共享内存、消息队列、信号灯;
  • 分布式模式下的进程通信(两个Linux内核)

    • socket通信:一个网络中的两个进程间通信,也是进程间通信的一种;

进程间通信的文件IO思想

进程间通信的思路可以参考文件IO的思想,他们的思想一致,只是函数的实现实现可能不同:

  • open: 创建、打开进程通信对象;

  • write: 向通信对象写入内容;

  • read: 从通信对象中读取内容;

  • close: 关闭、删除进程通信对象

实例

管道

管道文件是一个特殊文件,它由队列实现的。在文件IO中,创建、打开一个文件是由open函数实现,但是无名管道不能用open创建,对应的函数是pipe。

无名管道

无名管道的无名是指在它在文件系统中无文件名。

#include <unistd.h>
int pipe(int fd[2]);//pipe函数调用成功返回0,调用失败返回-1。

调用pipe函数时在内核中创建一个队列用于通信,然后通过fd参数传出给用户程序两个文件描述符,fd[0]指向管道的读端(出队),fd[1]指向管道的写端(入队)。

管道是以队列形式实现,管道内的东西读完就删了。如果管道内没有东西可读,则会阻塞。

#include "unistd.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
int main()
{
  int fd[2];
  int ret;
  char writebuf[]="hello linux";
  char readbuf[128]={0};
  ret=pipe(fd);
  if(ret <0)
  {
	printf("creat pipe failure\n");
	return -1;
  }
  printf("creat pipe sucess fd[0]=%d,fd[1]=%d\n",fd[0],fd[1]);
 
  write(fd[1],writebuf,sizeof(writebuf));
  //start read from pipe
  
  read(fd[0],readbuf,128);
  printf("readbuf=%s\n",readbuf);
 
  //second read from pipe
  
  memset(readbuf,0,128);
 
  read(fd[0],readbuf,128);//会阻塞在这
  printf("second read after\n");
 
  close(fd[0]);
  close(fd[1]);
  return 0;
}

无名管道的缓存是有大小的,缓存满了,会出现写阻塞。

#include "unistd.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
int main()
{
  int fd[2];
  int ret;
  char writebuf[]="hello linux";
  char readbuf[128]={0};
  ret=pipe(fd);
  if(ret <0)
  {
	printf("creat pipe failure\n");
	return -1;
  }
  printf("creat pipe sucess fd[0]=%d,fd[1]=%d\n",fd[0],fd[1]);
  while(1)
  {
   write(fd[1],writebuf,sizeof(writebuf));
  }
  
  printf("write pipe end\n");
 
  close(fd[0]);
  close(fd[1]);
  return 0;
}

先pipe再fork,子进程继承父进程的文件描述符,所以父子进程可以通过管道通信。

  • 父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。

  • 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。

  • 父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。

#include <stdlib.h>
#include <unistd.h>
#define MAXLINE 80

int main(void)
{
	int n;
	int fd[2];
	pid_t pid;
	char line[MAXLINE];

	if (pipe(fd) < 0) {
		perror("pipe");
		exit(1);
	}
	if ((pid = fork()) < 0) {
		perror("fork");
		exit(1);
	}
	if (pid > 0) { /* parent */
		close(fd[0]);
		write(fd[1], "hello world\n", 12);
		wait(NULL);
	} else {       /* child */
		close(fd[1]);
		n = read(fd[0], line, MAXLINE);
		write(STDOUT_FILENO, line, n);
	}
	return 0;
}

有名管道

无名管道只能在有亲缘关系的进程中进行通信,针对这一缺点进行改进,出现有名管道。 有名,即文件系统中存在对应文件节点,每个文件节点都有一个inode号。

mkfifo 函数形式:int mkfifo(const char * filename, mode_t mode); 功能:创建管道文件 参数:管道文件名;权限,文件权限仍然和umask有关 返回:成功返回0,失败返回-1

mkfifo调用内核,内核在文件系统中生成文件名,并没有在内核中生成管道。 只有在调用open代开有名管道时,才会在内核中创建管道。

#include "unistd.h"
#include "stdio.h"
#include "sys/types.h"
#include "stdlib.h"
#include "fcntl.h"
int main()
{
  int ret,fd;
  ret=mkfifo("./myfifo",0777);
  if(ret <0)
  { 
	printf("creat myfifo failure\n");
	return -1;
  }
  printf("creat myfifo sucess\n");
  
  fd=open("./myfifo",O_WRONLY);
  if(fd <0)
  {
    printf("open myfifo failure\n");
    return -1;
  }
  printf("open myfifo sucess\n");

  write(fd,"hello world\n", 12);
  while(1);

  return 0;
}
#include "unistd.h"
#include "stdio.h"
#include "sys/types.h"
#include "stdlib.h"
#include "fcntl.h"
int main()
{  
  int fd;
  char buf[128]={0};
  fd=open("./myfifo",O_RDONLY);
  if(fd <0)
  {
    printf("open myfifo failure\n");
    return -1;
  }
  printf("open myfifo sucess\n");

  read(fd, buf, 128);
  printf("read from myfifo %s\n", buf);
  while(1);

  return 0;
}

其它IPC机制

进程间通信必须通过内核提供的通道,而且必须有一种办法在进程中标识内核提供的某个通道,管道是用打开的文件描述符来标识的。如果要互相通信的几个进程没有从公共祖先那里继承文件描述符,它们怎么通信呢?内核提供一条通道不成问题,问题是如何标识这条通道才能使各进程都可以访问它?文件系统中的路径名是全局的,各进程都可以访问,因此可以用文件系统中的路径名来标识一个IPC通道。

FIFO和UNIX Domain Socket这两种IPC机制都是利用文件系统中的特殊文件来标识的。可以用mkfifo命令创建一个FIFO文件:

$ mkfifo hello
$ ls -l hello
prw-r--r-- 1 akaedu akaedu 0 2008-10-30 10:44 hello
  • FIFO文件在磁盘上没有数据块,仅用来标识内核中的一条通道,各进程可以打开这个文件进行read/write,实际上是在读写内核通道(根本原因在于这个file结构体所指向的read、write函数和常规文件不一样),这样就实现了进程间通信。
  • UNIX Domain Socket和FIFO的原理类似,也需要一个特殊的socket文件来标识内核中的通道,例如/var/run目录下有很多系统服务的socket文件:
$ ls -l /var/run/
total 52
srw-rw-rw- 1 root        root           0 2008-10-30 00:24 acpid.socket
...
srw-rw-rw- 1 root        root           0 2008-10-30 00:25 gdm_socket
...
srw-rw-rw- 1 root        root           0 2008-10-30 00:24 sdp
...
srwxr-xr-x 1 root        root           0 2008-10-30 00:42 synaptic.socket

文件类型s表示socket,这些文件在磁盘上也没有数据块。UNIX Domain Socket是目前最广泛使用的IPC机制,到后面讲socket编程时再详细介绍。

现在把进程之间传递信息的各种途径(包括各种IPC机制)总结如下:

  • 父进程通过fork可以将打开文件的描述符传递给子进程

  • 子进程结束时,父进程调用wait可以得到子进程的终止信息

  • 几个进程可以在文件系统中读写某个共享文件,也可以通过给文件加锁来实现进程间同步

  • 进程之间互发信号,一般使用SIGUSR1和SIGUSR2实现用户自定义功能

  • 管道

  • FIFO

  • mmap函数,几个进程可以映射同一内存区

  • SYS V IPC,以前的SYS V UNIX系统实现的IPC机制,包括消息队列、信号量和共享内存,现在已经基本废弃

  • UNIX Domain Socket,目前最广泛使用的IPC机制