进程间通信
进程间通信
进程间的通信,它的数据空间的独立性决定了它的通信相对比较复杂,需要通过操作系统。以前进程间的通信只能是单机版的,现在操作系统都继承了基于套接字(socket)的进程间的通信机制。这样进程间的通信就不局限于单台计算机了,实现了网络通信。
进程的通信机制主要有:管道、有名管道、消息队列、信号量、共享映射区、共享内存、信号、套接字。
Signal | 信号
信号是在软件层次上对中断机制的一种模拟,即模拟由系统内核发出,由于错误内存冲突等原因引起产生的请求。在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
信号可以看做异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过
信号分为可靠信号和不可靠信号,实时信号和非实时信号。进程有三种方式响应信号:忽略信号、捕捉信号、执行默认操作。
Semaphore | 信号量
信号量也可以说是一个计数器,常用来处理进程或线程同步的问题,特别是对临界资源的访问同步问题;信号量的本质是一种数据操作锁,它本身不具有数据交换的功能,而是通过控制其他的通信资源
信号量分为有名与无名,信号量在进程是以有名信号量进行通信的,在线程是以无名信号进行通信的,因为线程
当信号量的值大于或等于
Pipe | 管道
管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用,其中进程的亲缘关系通常是指父子进程关系。没有名字并且大小受限,传输的是无格式的流,所以两进程通信时必须约定好数据通信的格式。管道它就像一个特殊的文件,但这个文件只存在于内存中,在创建管道时,系统为管道分配了一个页面作为数据缓冲区,进程对这个数据缓冲区进行读写,以此来完成通信。其中一个进程只能读一个只能写,所以叫半双工通信,为什么一个只能读一个只能写呢
管道实现进程间通信的方式如下:
-
父进程创建管道,得到两个⽂件描述符指向管道的两端。
-
父进程
fork 出子进程,⼦进程也有两个⽂件描述符指向同⼀管道。 -
父进程关闭
fd[0], 子进程关闭fd[1] ,即⽗进程关闭管道读端, ⼦进程关闭管道写端。
因为管道只支持单向通信,⽗进程可以往管道⾥写
Named Pipe | 命名管道
命名管道
匿名管道只能在具有亲缘关系的进程间通信,命名管道与它不同,它提供了一个路径名与之关联,有了自己的传输格式。命名管道和管道的不同之处还有一点是
Message Queue | 消息队列
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。与管道不同的是,消息队列存放在内核中,只有在内核重启时才能删除一个消息队列,同样消息队列的大小也是受限制的。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。每个数据块都被认为是一个管道,接收进程可以独立地接收含有不同管道的数据结构。
消息队列与命名管道一样,每个数据块都有一个最大长度的限制。我们可以将每个数据块当作是一种消息类型,发送和接收的内容就是这个类型对应的消息,每个类型相当于一个独立的管道,相互之间互不影响。消息队列进行通信的一些操作:
-
使用
msgget() 函数创建打开队列; -
使用
msgrcv() 函数从队列中读数据; -
使用
msgsnd() 函数写数据到队列中; -
使用
msgctl() 函数控制消息队列。
Shared Memory | 共享内存
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的
在使用共享内存区前,必须通过
采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。
实际上,进程之间在共享内存时,并不总是读写少量数据后就 解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
-
实现共享内存的步骤如下:创建内存共享区,进程
1 通过操作系统提供的api 从内存中申请一块共享区域,linux 系统中可以通过shmget 函数实现,生成的共享内存块与某个特定的key 进行绑定。 -
映射共享内存到进程
1 中:在Linux 环境中,可以通过shmat 实现。 -
映射共享内存到进程
2 中:进程2 通过进程1 的shmget 函数和同一个key 值,然后执行shmat ,将这个内存映射到进程2 中。 -
进程
1 与进程2 中相互通信:共享内存实现两个映射后,可以利用该区域进行信息交换,由于没有同步机制,需要参与通信的进程自己协商处理。 -
撤销内存映射关系:完成通信之后,需要撤销之前的映射操作,通过
shmdt 函数实现。 -
删除共享内存区:在
Linux 中通过shctl 函数来实现。
共享内存映射区
通过
-
有血缘关系的进程间通信:因为子进程和父进程永远共享的有两个部分,第一是文件描述符,第二就是内存映射区。所以只需要父进程先用
open() 函数打开一个文件得到fd ,再利用mmap() 函数创建一个内存映射区,然后fork() 一个子进程出来,子进程就可以直接通过mmap() 返回的内存映射区首地址对共享部分进行读写了,相当于就是有血缘关系的进程间通信机制。 -
没有血缘关系的进程间通信:还是让一个进程先通过
open() 函数打开一个文件得到fd ,再利用mmap() 创建一个内存映射区并得到首地址ptr ,然后另外一个无血缘关系的进程通过打开这个文件得到fd1 ,也利用mmap() 函数将fd1 传参进去,就会得到那个进程创建的内存映射区首地址ptr1 了,这样两个互不相关的进程通过ptr 和ptr1 各自进行读写即可。 (因为这两个内存映射区指向的磁盘存储区都是同一个) 。
注意,在共享内存中打开的具体的文件是无所谓的,并不会被用到。
Socket | 套接字
套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机间的进程通信。