面试题:管道、消息队列、共享内存哪个开销最小?

面试遇到这样的问题,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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章