进程间通信方式
- 管道
- 消息队列
- 共享内存
- 信号量
- 套接字
进程为什么需要通信
有时候两个进程需要实现数据传输、资源共享、通知事件等,但是由于进程组织的时候是一个 pcb 结构体,而且是相互独立的,有独立的虚拟地址空间,所以 IPC 给我们提供了接口
进程间通信需要“介质”,也就是两个进程都可以访问到的公共资源
1、管道
比如说一个管道符,就可以理解为是一个匿名管道,也是两个进程通信的介质,前面的输出作为后面的输入
netstat -apu | grep 8080
管道可以理解为一个进程连接到另一个进程的数据流
文件描述符角度:管道相当于内核中的一块缓冲区(内存)构成一个队列;使用一对文件描述符来进行访问这个内存,读文件描述符就是从队列中取出数据,写文件描述符就是往队列中插入数据;
fork 共享管道原理:调用pipe的父进程有一个读端,有一个写端,当调用 fork 函数后子进程也有一个读端,一个写端,然后分别关闭父进程的写端,和子进程的读端,两个进程各有一个读端,一个写端,就可以通信了.
内核角度:内核中的一块缓冲区,多个进程通过访问同一块缓冲区实现数据通信
管道特性和规则
-
管道中没有数据,则 read 一直阻塞不能读数据,直到有数据来到为止
-
管道中放满数据,则 write 一直阻塞不能写数据,直到有进程读走数据为止
-
若管道所有写端对应的文件描述符被关闭,则 read 读取完数据返回 0
-
若管道所有读端对应的文件描述符被关闭,则 write 写数据会触发异常
管道自带同步与互斥, 保证了操作的原子性
- 当要写入的数据量不大于管道缓冲区,linux 会保证写入的原子性,否则不保证原子性
匿名管道有一定的局限性,比如只能用于同一亲缘关系的进程间通信,如果是两个毫无关系的进程那就要得使用命名管道
命名管道用 mkfifo 函数创建,用 open 打开,匿名管道是用 pipe 函数创建并打开,当打开完之后,命名和匿名管道就没什么大区别了.
命名管道读写特性和匿名管道是一样的
命名管道的打开特性:
- 如果当前操作是为了读而打开命名管道,则会阻塞,直到有相应的进程为写而打开该管道
- 如果当前操作是为了写而打开命名管道,则会阻塞,直到有相应的进程为读打开该管道
命名管道提供面向字节流服务,会造成数据粘连,因为数据之间没有边界,而且适用于任意进程,半双工通信.
半双工通信就是:允许在两个方向传输,但是在同一时间只能允许一个方向的传输,所以它其实是转换方向的单工通信,比如说我们已经快淘汰的对讲机,像我们的手机通信,电话都是全双工通信.
管道通信的效率比较低,如果 A 进程向 B 进程传输数据,只有当B进程读取后数据,A 进程才能返回.
2、消息队列
是内核中维护的一个队列 ,消息队列提供了从一个进程向另外一个进程发送数据的方法,发送消息的进程,只管往队列中添加数据就行了,让接受数据的进程在队列中取出信息
进程间通过对同一个消息队列添加/获取数据块结点实现通信,传输的是有类型的数据块(数据节点)
另外如果一个进程发送的数据占的内存比较大,并且两个进程之间的通信特别频繁的话,消息队列模型就不大适合了,因为发送的数据很大的话,意味 发送消息(拷贝) 这个过程需要花很多时间来读内存
为了减少开销时间,还有一种通信叫共享内存
3、共享内存
这是最快的进程间通信方式,因为两个进程共用相同的内存,不需要进程拷贝数据.
当系统加载一个进程的时候,分配给进程的内存并不是实际物理内存,而是虚拟内存空间,那么我们可以让两个进程各自拿出一块虚拟地址空间来,然后映射到相同的物理内存中,这样,两个进程虽然有着独立的虚拟内存空间,但有一部分却是映射到相同的物理内存,这就完成了内存共享机制了。这种通信相较于其他进程间通信方式,少了两步用户态与内核态之间的数据拷贝过程.
在 Linux中,每个进程都有属于自己的进程控制块和地址空间,并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元(MMU)进行管理,两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域就是共享的内存
共享内存一共分为五个步骤
-
创建共享内存 shmget
-
将共享内存映射到虚拟地址空间 shmat
-
直接通过虚拟地址进行内存操作 memcpy
-
解除映射连接关系 shmdt
-
删除共享内存 shmctl
参考文章:共享内存
但是需要注意的是:共享内存并没有实现同步的机制,也就是说当进行写操作的时候,是不能读取的,那要怎么保证这一点呢?就得引出信号量
4、信号量
信号量可以保证多进程竞争内存的安全问题,实现进程间同步与互斥.
信号量的本质就是一个计数器,例如信号量的初始值是 1,然后 a 进程来访问内存1的时候,我们就把信号量的值设为 0,然后进程b 也要来访问内存1的时候,看到信号量的值为 0 就知道已经有进程在访问内存1了,这个时候进程 b 就无法访问内存1
5、套接字
套接字可以对不同的主机之间进程通信,通过自定义协议的格式进行通信,也可以利用现有的知名协议进行进程间通信
例如,客户端和服务器之间的通信,我们平时通过浏览器发起一个 http 请求,然后服务器给你返回对应的数据,这种就是采用 Socket 的通信方式了,再比如我们用最多的微信聊天,当我们给女朋友发送一个信息的时候,这个信息首先会发送给微信后台服务器,然后由服务器转发给女朋友的微信客户端.
进程间通信目的
-
数据传输:一个进程 需要将它的数据发送给另一个进程
-
资源共享:多个进程之间共享同样的资源
-
通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件(进程终止时要通知父进程)