进程间通信的各种方法以及优缺点和适用场景

正文

每个进程的用户地址空间都是独立的,一般而言是不能相互访问的,但是所有的进程都共享一个内核空间,所以进程间想要通信就必须通过内核,那么进程间的通信方式有哪些呢?每种方式又有哪些缺点呢?适用在什么场景下呢?

以下内容借鉴了一个博主的文章,我觉得写的很好,为了巩固一下这些知识点,所以我就打算自己归纳总结一下

借鉴:公众号小林conding

管道

管道又分为有名管道和匿名管道,管道是UNIX系统中IPC(进程通信)的最古老的形式,所有UNIX都支持这种通信机制

ls | wc -l
//中间那个"|"就是在shell脚本下的管道符

管道的特点

  • 管道是一个在内核内存中维护的缓冲区,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同

  • 在Linux下,可以说一切皆是文件,其实内核内存中的这个缓冲区也是一个文件,那么可以操作文件的API,同样也能操控缓冲区

  • 一个管道是一个字节流,使用管理时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小为多少

  • 通过管道传递的数据时顺序的,从管道中读取出来的字节的顺序和写入的顺序时完全一样的,就像是队列一样,先进先出

  • 从管道读取数据是一次性操作,只要读取完,那么读取的数据就会被抛弃而不会留在缓冲区以便有更多的空间来写数据,所以无法使用lseek()函数来随机访问数据

  • 匿名管道只能在具有公共祖先进程的时候(例如:父进程与子进程通信,两个兄弟进程通信)才能使用,有名管道用于无亲缘关系的进程

为什么匿名管道能够进行通信?

当我们对一个进程使用fork()创建一个子进程的时候,子进程会复制一份父进程的内核区,不过其中的进程号是不一样的,同时复制的还有文件描述符表,那么子进程的文件描述符表上也就会有关于管道读写端的记录,这样就能操作同一个管道来进行通信了,这也就意味着,必须要采用fork()函数之前先创建好匿名管道

匿名管道

//创建匿名管道
int pipe(int pipe[2]);
参数:是一个传出参数,调用这个API后,pipe[0]为读端,pipe[1]为写端
返回值:成功返回0,失败返回-1

其他写入数据、读取数据函数都与操作文件的函数一样
read(pipe[0],buf,sizeof(buf));
write(pipe[1],buf,sizeof(buf));

有名管道

有名管道主要用于无血缘关系的俩进程之间的通信,有名管道也可以叫命名管道,是一个FIFO文件,也就是队列,有名管道与匿名管道有一些特点是不一样的,但也有一些是不同的,比如:

  • FIFO文件在文件系统中作为一个特殊文件存在,但FIFO中的内容是存放在内存中的

  • 当使用FIFO的进程退出后,FIFO文件将继续保存在文件系统中以便以后使用

  • FIFO有名字,不相关的进程可以通过打开有名管道进行通信

//创建一个有名管道
mkfifo 有名管道的名字 (shell命令)

//通过相关API创建有名管道
包含头文件
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname,mode_t mode)
参数:
    pathname:管道名称的路径
    mode:文件权限,与open()函数中的mode是一样的

其他IO函数都可以用在有名管道上

使用有名管道时候的注意事项

  • 一个为只读而打开的一个管道的进程会阻塞,直到另外一个进程以只写打开管道

  • 两个进程想要能够互相发送消息和接受消息的时候,需要创建两个有名管道,原因是如果两个进程对一个管道的权限都是可以读又可以写的话,那么就会出现错误,比如A进程写入的数据A进程自己读取到了

管道的优势和劣势

优势:简单,我们也容易得知管道里的数据已经被另外一个进程读取

劣势:通信效率低下

消息队列

消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。

消息这种模型,就好像两个进程发邮件一样,你来一封,我来一封,可以频繁沟通

消息队列的优势和劣势

优势:相对于管道,消息队列交换数据的效率明显提高了

劣势:1.通信不及时 2.附件也有大小的限制

共享内存

消息队列的读写都发生了用户态到内核态的转变,过多的转变其实也很占用CPU资源,为了减少这种转变,共享内存就很好的解决了这一问题

现代操作系统,对于内存管理,采用的是虚拟内存技术,也就是每个进程都有自己独立的虚拟内存空间,不同进程的虚拟内存映射到不同的物理内存中。所以,即使进程 A 和 进程 B 的虚拟地址是一样的,其实访问的是不同的物理内存地址,对于数据的增删查改互不影响。

共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。

信号量

用了共享内存通信方式,带来新的问题,那就是如果多个进程同时修改同一个共享内存,很有可能就冲突了。例如两个进程都同时写一个地址,那先写的那个进程会发现内容被别人覆盖了。就有点类似线程的安全冲突问题。

为了防止多进程竞争共享资源,而造成的数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只能被一个进程访问。正好,信号量就实现了这一保护机制。

信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据

关于信号量的一些详细的知识点建议去小林conding上面仔细看一下,因为我自己学的和小林讲的有一点不一样,我也不知道哪个是对的,也可能都是对的

信号

上面说的进程间通信,都是常规状态下的工作模式。对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。

信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式。

  1. 执行默认操作。Linux 对每种信号都规定了默认操作,例如,上面列表中的 SIGTERM 信号,就是终止进程的意思。Core 的意思是 Core Dump,也即终止进程后,通过 Core Dump 将当前进程的运行状态保存在文件里面,方便程序员事后进行分析问题在哪里。

  1. 捕捉信号。我们可以为信号定义一个信号处理函数。当信号发生时,我们就执行相应的信号处理函数。

  1. 忽略信号。当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,它们用于在任何时候中断或结束某一进程。

这些都是用于在同一个主机上的进程间通信,想要在两台不同的主机上进行通信,就需要用到网络编程的Socket,这个我们后面再说啦

1.19

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值