概念
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程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机制