文章目录
- 第二章 进程管理
- 进程通信(IPC)
- 为什么进程通信需要操作系统支持?
- (一)共享存储
- (1)基于存储区的共享
- (2)基于数据结构的共享
- (二)消息传递
- 什么叫“格式化的消息”?
- (1)直接通信方式
- (2)间接通信方式
- (三)管道通信
- 小结
第二章 进程管理
进程通信(IPC)
这个小节我们会学习进程通信的几种方式,分别是
- 共享存储
- 基于数据结构的共享
- 基于存储区的共享
- 消息传递
- 直接通信方式
- 间接通信方式
- 管道通信
进程之间的通信(Inter-Process Communication, IPC)是指两个进程之间产生数据交互。
在一个系统当中,同时会有多个进程正在运行,那么这些进程之间难免需要相互配合着工作,在这种情况下,进程和进程之间的数据通信就显得很有必要了。
比如正在浏览微博的时候,可以通过分享功能,把一条微博分享给微信好友。这时就发生进程间的通信了。本来那条微博的链接是在微博里面的,直接就分享到微信里面了,这个过程显然是进程和进程之间发生了数据交互、发生了通信 。
既然进程之间的通信是很有必要的,那么该怎样实现进程之间的通信呢?这个需要操作系统的支持。
为什么进程通信需要操作系统支持?
为什么进程之间的通信一定要有操作系统内核的支持,原因是这样的。
我们系统中给各个进程分配内存地址空间的时候,各个进程的内存地址空间是相互独立的,比如进程P它可以访问自己的空间,进程Q可以访问自己的空间。但是进程P不可以访问进程Q的地址空间。
这么规定,是出于安全的考虑。因为,如果一个进程可以随意地访问其他进程的内存地址空间,那么一个进程就可以随意修改其他进程的数据了,或者随意读取其他进程的数据。那这样的话,试想一下,比如你的手机里面不知什么时候安装了一个垃圾软件,这个垃圾软件,如果它可以随意地访问其他进程的地址空间,那有可能它直接把你微信里的私密的一些聊天数据,或者照片之类的,直接读取走了。这显然是不安全的。
因此,出于安全考虑,各个进程只能访问自己的这片内存地址空间,而不能访问其他进程的内存地址空间,无论是读、写,都不行。
因此,如果P和Q,它们之间想要进行数据交互,想要进行进程之间的通信,那么显然,进程P是不可能直接把这个数据写到Q的这片空间里面的。
所以,由于进程不可直接访问其他进程的内存地址空间,因此就必须要有操作系统的支持才可以完成进程之间的通信。
接下来会介绍三种进程之间的通信方式:共享存储、消息传递、管道通信。
(一)共享存储
(1)基于存储区的共享
各个进程只能访问自己的这片空间,但是如果操作系统支持共享存储的功能,那么一个进程,它可以申请一片共享存储区。而这片共享存储区,也可以被其他进程所共享。
这样的话,一个进程P,如果它要给Q传送数据的话,那么P就可以先把数据写到这片共享存储区里面,因为P对共享存储区是有访问权限的。接下来进程Q再从共享存储区里面读出数据。
由于共享存储区可以被多个进程所共享,因此这些数据之间的数据交换,就可以通过这一片被共享的区域来进行。这就是共享存储的进程间通信方式。
比如Linux中,如何实现共享内存:
int shm_open(...); //通过shm_open系统调用,申请一片共享内存区
void * mmap(...); //通过mmap系统调用,将共享内存区映射到进程自己的地址空间
(注:什么叫内存区映射,这是第三章内容。通过“增加页表项/段表项”,即可将同一片共享内存区映射到各个进程的地址空间中。)
另外,还需要注意一个问题,如果多个进程都往这片区域写数据的话,有可能会导致写冲突,会导致数据覆盖的问题。所以,各个进程之间如果使用共享存储的方式来进行通信的话,那么需要保证各个进程对这个共享存储区的访问是互斥的。也就是当进程P正在访问这片区域的时候,那其他进程就不能访问这片区域。
怎么实现这个互斥的功能呢?
操作系统内核会提供一些同步互斥工具(比如在后面会学习的P、V操作),各个进程从而能够对共享存储区实现一种互斥的访问。
刚才我们说的这种共享存储的方案,是基于存储区的共享。操作系统给你划定了这么大的一片区域,之后,若干个进程,到底是想往这片区域里的哪个位置写,或者从哪个位置读,这些都是很自由的。操作系统只负责把这片区域划给你,但是并不管你怎么使用这片区域。
基于存储区的共享:操作系统在内存中划出一块共享存储区,数据的形式、存放位置都由参与通信的进程自己控制,而不是操作系统。这种共享方式速度更快,是一种高级通信方式。
(2)基于数据结构的共享
相比之下,还有一种基于数据结构的共享。
操作系统给你们两个进程,划定的共享区域,它就规定,只能存放一个长度为10的数组。这样的话,各个进程之间的通信自由度就没那么高,并且传送数据的速度也会比较慢。
基于数据结构的共享:比如共享空间里只能放一个长度为10的数组。这种共享方式速度慢、限制多,是一种低级通信方式。
(二)消息传递
如果采用这种通信方式,那么进程之间的数据交换会以格式化的消息(Message)为单位。通过操作系统提供的“发送消息/接收消息”两个原语来进行数据交换。
什么叫“格式化的消息”?
所谓格式化的消息,由两个部分组成:消息头、消息体。
消息头,要写明注明,这个消息是由谁发送的,到底要发送给谁,整个消息的长度是多少,等等这些概要性的信息。
消息体,就是具体的,一个进程想要传送给另一个进程的数据。
这种消息传递的通信方式,又可以进一步划分为:直接通信方式、间接通信方式。
其中,直接通信方式就是,发送进程要指明接收进程的ID。(系统里每一个进程都会有一个ID,叫PID)直接通信方式的意思就是,我发送的时候,直接点明,就是要它接收。
而间接通信方式,会通过一个叫作“信箱”的中间实体来进行通信。所以间接通信方式又称为“信箱通信方式”。
(1)直接通信方式
进程P现在要给进程Q发送一个消息,而在操作系统的内核区域是管理着各个进程的PCB的。
同时,会有各个进程PCB对应的消息队列。比如进程Q就有一个进程Q的消息队列
,也就是其他进程要发送给进程Q、应该被进程Q接收的这些消息,都挂在这个队列里面。
现在,进程P要给进程Q发送一个消息。首先,每个进程自己是有自己的地盘、自己的内存空间的,它会在此先完善这个消息的信息(如图msg),包括消息头、消息体。接下来,进程P会使用到发送原语send(Q, msg)
(操作系统提供的发送原语),用它来指明,我的这个消息msg
,是要发送给Q
这个进程。
这个发送原语,会导致操作系统内核接收到这个消息,并且会把它 挂到进程Q的消息队列里面。
此时,这个消息msg
由进程P的内存空间,复制到了内核空间当中。
接下来,进程Q通过接收原语receive(P, &msg)
,来指明现在要接收一个消息,是P
发来的。此时,操作系统会检查进程Q的消息队列,看一下这几个消息到底哪一个是由P发送过来的。
找到了由P发送过来的消息,那么操作系统内核会把这个消息体的数据,又从操作系统的内核区复制到进程Q的用户区、地址空间。
消息传递——直接通信方式:点名道姓的消息传递。我在发送的时候,指明要发送给谁;我在接收的时候,指明要接收谁发来的消息。
(2)间接通信方式
刚才我们说过,间接通信方式,它需要一个中间实体(所谓的“信箱”)来进行消息的传递(所以又称之为信箱通信方式)。
这种通信方式是这样来实现的:
进程P和进程Q想要进行通信。那么进程P可以通过系统调用,申请一个信箱,当然也可以申请多个邮箱。比如此处,进程P申请了信箱A、信箱B。
现在,这两个进程怎么进行通信呢?
首先,进程P在自己的地址空间里完善消息msg
的内容,然后进程P可以使用发送原语send(A, msg)
,往信箱A
发送消息msg
。
间接通信方式,是指明了我要发送到哪个信箱,并没有指明我要发送给哪个进程。
那,进程Q在使用接收原语的时候,它可以指明,我要从信箱A中接收一个消息体。这样,信箱A中的这一个msg,就会被操作系统复制到进程Q的空间中了。
这就是使用信箱来完成消息传递的过程。
通常来说,操作系统是可以允许多个进程往同一个信箱里send消息,也可以多个进程从同一个邮箱里receive消息。
(三)管道通信
“管道”这个词还是很形象的,它就像一个水管、管道一样。就是,写进程可以从管道的一边写入数据,读进程从管道的另一边取走数据。
这个数据的流动只能是单向的。从左到右,或者从右到左,就像一根水管里的水流一样,不可以是双向同时进行的。
这里的管道,其实是一种特殊的共享文件,又名pipe文件。也就是,如果两个进程要用管道的方式进行进程通信,那么首先我们需要系统调用的方式,来申请一个管道文件,操作系统会新建这个管道文件。这个文件的本质就是在内存当中开辟了一个大小固定的内存缓冲区。然后,两个进程可以往这个内存缓冲区里面写数据和读数据,但是这个数据的读写是先进先出的(FIFO)。
问题:说到这里,管道通信是为两个进程开辟了一块内存缓冲区,而刚才所说的共享存储,也是开辟了一块共享存储区,也是可以被进程P、Q共享访问的。那么它们有什么区别?
区别是这样的:
刚才我们讲的基于存储区的共享,进程P、Q对于共享存储区中,具体的存储、读取位置,没有任何限制,很自由。但是,管道通信的方式,其中是一个数据流的形式,如果前边有空位,则写数据的时候要先往前边写,前边占满了再接着往后边区域写;读数据的时候也一样,只能先把前边的数据读空了,才可以读后边的这些数据,先进先出。
所以,管道通信和共享存储通信,区别还是很大的,管道通信的读写一定是先进先出的,可以把它理解为一个循环队列;而共享存储的读写是没有任何限制的。
1、管道只能采用半双工通信,某一时间段内只能实现单向的传输。如果要实现双向同时通信(全双工通信),则需要设置两个管道。
2、各进程要互斥地访问管道。(由操作系统实现)
3、刚才已经介绍到,这个管道是一个大小固定的内存缓冲区,因此会被写满。当管道写满时,写进程将阻塞,直到读进程将管道中的数据取走,即可唤醒写进程。
4、另一个方面,当管道读空时,读进程将阻塞,直到写进程往管道中写入数据,即可唤醒读进程。
5、(这一点,是很多教材最有争议的一个地方)管道通信的方式就决定了,一旦管道中的数据被读出,那就彻底消失了。所以,如果有多个读进程在读同一个管道的时候,就有可能导致错乱:因为我们管道里面的数据,它并没有指明我到底是要给进程Q的、还是要给进程R的。所以,如果多个进程都从同一个管道这读数据的话,那么就有可能这个数据的读取动作是比较乱的:第一块数据被Q读走了、第二块数据被R读走了。
针对这一问题,不同的操作系统会有不同的解决方案:①一个管道允许多个写进程,一个读进程(2014年408真题高教社官方答案);②允许有多个写进程,多个读进程,但系统会让各个读进程轮流从管道中读数据(Linux的方案)。
有的操作系统是①的方案,有的操作系统是②的方案。因此,有的教材按照①来说,管道允许多个写进程、但只允许一个读进程;而有的教材按照②来说,一个管道允许多个写进程、多个读进程。
对于408考试来说,按照①来说即可。但是从现实应用的角度来看,①②都是存在的。
小结
共享存储、消息传递、管道通信这三种常见的进程间通信方式。这三种通信的功能都是需要操作系统的底层来支持的。
共享存储,会设置一块共享内存区,并映射到进程的虚拟地址空间(这个问题在学完第三章,段表、页表之后再回来看就懂了,这个很简单)。
另外,各个进程对共享空间的访问要互斥地进行,这个互斥的效果是由进程自己进行控制的,如使用P、V操作来实现。实际上,对于共享存储区的互斥访问,是一个经典的同步互斥问题:读者、写者问题(在后面会讲)。
消息传递分为两种通信方式。直接通信方式就是要指名道姓地指明我要把消息发给哪个进程,然后操作系统会把这个消息直接挂到接收进程的消息队列里面。
间接通信方式又叫信箱通信,操作系统会把消息放到指定的信箱当中,而消息的接收者也需要指明自己从哪个信箱当中取走消息。
管道通信。对于操作系统而言,管道通信的这个管道,是一个特殊的共享文件(比如你在Linux系统上是的确能够找到这一文件的),但本质上这个文件就是一个内存缓冲区,如果结合数据结构的知识来看的话,这个内存缓冲区其实就是一个循环队列。那么,如果管道写满了,写进程就会被阻塞;如果管道读空了,读进程就会被阻塞。
此外,一个管道文件只能实现半双工通信。这类似于现实生活中的一根水管,同一时刻,水不可能既从左往右流、又从右往左流。
所以,如果想要从左往右、从右往左的水流同时存在的话,我可以建立两个管道,实现双向同时通信。
读进程想要从管道读数据,只需保证一点:管道不是空的,即可。至于管道是否写满、还是只写了一部分,都无所谓,只要不是空的就能读。
写进程同理,只要管道没满,还有空间让我可以写,就可以往管道中写数据。至于管道是完全空了、还是有一部分数据,都无所谓,只要不满就能写。