面试遇到这样的问题,Linux包含多种进程间通信(interprocess communication,或者IPC),我们或多或少在面试中都被问及IPC的相关知识,所以这篇文章记录总结一下自己对于IPC的认识和理解吧!
概述
进程间的通信手段大体可以分为两类:
-
通信类
-
同步类
关于POSIX IPC与System V IPC的区别:
- POSIX(Portable Operating System Interface for Computing Systems)是由IEEE 和ISO/IEC 开发的一簇UNIX标准。
- System V, 曾经也被称为 AT&T System V,是Unix操作系统众多版本中的一支。由AT&T的贝尔实验室开发。
区别:
1、System V IPC方法早于POSIX IPC,几乎所有的Unix平台都支持System V IPC,其可移植性较好,但是在使用过程中也暴露出一些弱点。
2、POSIX IPC提供了和System V IPC相对应的工具(它也包括消息队列、信号量和共享内存)。3、从设计的角度上讲,POSIX IPC是优于System V IPC的,接口简单,易于使用。但是POSIXIPC的可移植性并不如System V IPC。
IPC包含以下几种方式:
- 管道
- 消息队列
- 信号量
- 共享内存
管道
管道是最早出现的进程间通信方式,在shell中执行命令,经常会将上一个命令的输出作为下一个命令的输入,由多个命令配合完成一件事,这就是通过管道来完成的。
进程who的标准输出,通过管道传递给下游的wc进程作为标准输入,从而通过相互配合完成了一件任务。
无名管道
无名管道的通信是作用在亲缘进程之间的,所谓亲缘进程是指有同一个公共祖先的进程组,所以管道不止可以用在父子进程,还可以用在兄弟进程、祖孙进程、叔侄进程。
管道实质是一个字节流,并非前面提到的消息,没有消息的边界。如果多个进程发送的字节流混在一起,则无法辨认出各自的内容。所以一般是两个有亲缘关系的进程用管道来通信。从程序设计的角度来讲,当进程调用pipe函数时,哪两个有亲缘关系的进程使用该管道来通信应是事先约定好的,其他有亲缘关系的进程不应该进来搅局。
管道中的内容是阅后即焚的,这个特性指的是读取管道内容是消耗型的行为,即一个进程读取了管道内的一些内容之后,这些内容就不会继续在管道之中了。一般来讲管道是单向的。如果两个进程之间想双向通信怎么办?可以建立两个管道,如图:
管道是一种文件,可以调用read、write和close等操作文件的接口来操作管道。另一方面管道又不是一种普通的文件,它属于一种独特的文件系统:pipefs。管道的本质是内核维护了一块缓冲区与管道文件相关联,对管道文件的操作,被内核转换成对这块缓冲区内存的操作。
在Linux下,可以使用如下接口创建管道:
#include <unistd.h>
int pipe(int pipefd[2]);
如果成功,则返回值是0,如果失败,则返回值是-1,并且设置errno。
成功调用pipe函数之后,会返回两个打开的文件描述符,一个是管道的读取端描述符pipefd[0],另一个是管道的写入端描述符pipefd[1]。
调用pipe函数的进程随后调用fork函数:
用管道通信的两个进程,各持有一个管道文件描述符,不相干的进程应自觉关闭掉这些文件描述符。这么做不仅仅是为了让数据的流向更加清晰,也不仅仅是为了节省文件描述符,更重要的原因是:关闭未使用的管道文件描述符对管道的正确使用影响重大。
管道有如下三条性质:
- 只有当所有的写入端描述符都已关闭,且管道中的数据都被读出,对读取端描述符调用read函数才会返回0(即读到EOF标志)。
- 如果所有读取端描述符都已关闭,此时进程再次往管道里面写入数据,写操作会失败,errno被设置为EPIPE,同时内核会向写入进程发送一个SIGPIPE的信号。
- 当所有的读取端和写入端都关闭后,管道才能被销毁。
管道内存大小
本质来讲管道也是一片内存区域,那它的大小是多少呢?
- 自Linux 2.6.11版本起,管道的默认大小是65536字节.
可以调用fcntl来获取和修改这个值的大小,代码如下:
pipe_capacity = fcntl(fd, ?F_GETPIPE_SZ); //获取管道大小
ret = fcntl(fd, ?F_SETPIPE_SZ, size); //设置管道大小
其上限记录在/proc/sys/fs/pipe-max-size里,对于特权用户,还可以修改该上限值。
命名管道FIFO
无名管道因为没有实体文件与之关联,靠的是世代相传的文件描述符,所以只能应用在有共同祖先的各个进程之间。对于没有亲缘关系的任意两个进程之间,无名管道就爱莫能助了。
命名管道就是为了解决无名管道的这个问题而引入的。FIFO与管道类似,最大的差别就是有实体文件与之关联。由于存在实体文件,不相关的没有亲缘关系的进程也可以通过使用FIFO来实现进程之间的通信。
与无名管道相比,命名管道仅仅是披了一件马甲,其核心与无名管道是一模一样的。
创建命名管道的接口定义方式:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);//mode指FIFO文件的读写权限,与open函数类似
消息队列
相比于管道来讲,消息队列机制中,双方是通过消息来通信的,无需花费精力从字节流中解析出完整的消息。
System V消息队列
消息队列中每条消息都有type字段,消息的读取进程可以通过type字段来选择自己感兴趣的消息,也可以根据type字段来实现按消息的优先级进行读取,而不一定要按照消息生成的顺序来依次读取。
内核为每一个System V消息队列分配了一个msg_queue类型的结构体,它内部是维护一个优先级消息链表。
POSIX消息队列
POSIX消息队列与System V消息队列有一定的相似之处,信息交换的基本单位是消息,但也有显著的区别。
最大的区别当属在Linux实现里POSIX消息队列的句柄本质是文件描述符。这个性质给POSIX消息队列带来了巨大的优势。因为是文件描述符,所以可以使用I/O多路复用系统调用(select、poll或epoll等)来监控这个文件描述符。
其次,POSIX消息队列提供了通知功能,当消息队列中有消息可用时,就会通知到进程。而System V消息队列没有通知功能,所以消息队列上何时有消息进程无从得知,只能阻塞(msgrcv)或轮询(带IPC_NOWAIT标志位的msgrcv)。
最后,System V消息队列的消息提取要比POSIX消息队列灵活。POSIX消息队列本质是个优先级队列。而System V消息中存在类型字段,可以提取类型等于某值的消息,这点POSIX消息队列是做不到的。
信号量
消息队列的作用是进程之间传递消息。而信号量的作用是为了同步多个进程的操作。
一般来说,信号量是和某种预先定义的资源相关联的。信号量元素的值,表示与之关联的资源的个数。内核会负责维护信号量的值,并确保其值不小于0。信号量上支持的操作有:
- 将信号量的值设置成一个绝对值。
- 在信号量当前值的基础上加上一个数量。
- 在信号量当前值的基础上减去一个数量。
- 等待信号量的值等于0。
创建打开信号量
创建或打开信号量的函数为semget,其接口如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);//nsems表示信号量的个数
共享内存
共享内存是所有IPC手段中最快的一种。它之所以快是因为共享内存一旦映射到进程的地址空间,进程之间数据的传递就不须要涉及内核了。
前面已经讨论过的管道、FIFO和消息队列,任意两个进程之间想要交换信息,都必须通过内核,内核在其中发挥了中转站的作用:
- 发送信息的一方,通过系统调用(write或msgsnd)将信息从用户层拷贝到内核层,由内核暂存这部分信息。
- 提取信息的一方,通过系统调用(read或msgrcv)将信息从内核层提取到应用层。
一个通信周期内,上述过程至少牵扯到两次内存拷贝(从用户拷贝到内核空间和从内核空间拷贝到用户空间)和两次系统调用,这其中的开销不容小觑。用户层的体验固然不佳,所以产生了共享内存的通信方式,共享内存是在用户空间不经过内核:
共享内存,这种思路可以通俗地概括为内核搭台,进程唱戏。简单地说,内核负责构建出一片内存区域,两个或多个进程可以将这块内存区域映射到自己的虚拟地址空间,从此之后内核不再参与双方通信。
注意 建立共享内存之后,内核完全不参与进程间的通信,这种说法严格来讲并不是正确的。因为当进程使用共享内存时,可能会发生缺页,引发缺页中断,这种情况下,内核还是会参与进来的。
总结
在Unix发展过程中出现了各类的进程通信方式,目前比较主流的通信方式主要包含管道、信号量、消息队列、共享内存等,从比较它们的底层实现可以看出来共享内存是开销最小的通信方式,源于它和内核的交流比较少,从而降低了开销。
欢迎读者一起交流学习,如本文章有错误之处还望不吝指出!谢谢!
个人github账号:https://github.com/SpecialAll