写在前面:
- 本系列笔记主要以《计算机操作系统(汤小丹…)》为参考,大部分内容出于此书,笔者的工作主要是挑其重点展示,另外配合下方视频链接的教程展开思路,在笔记中一些比较难懂的地方加以自己的一点点理解(重点基本都会有标注,没有任何标注的难懂文字应该是笔者因为强迫症而加进来的,可选择性地忽略)。
- 视频链接:操作系统(汤小丹等第四版)_哔哩哔哩_bilibili
一、概述
进程通信是指进程之间的信息交换。由于进程的互斥与同步需要在进程间交换一定的信息,故不少学者将它们也归为进程通信,但只能把它们称为低级进程通信。以信号量机制为例来说明,它们之所以低级的原因在于:
①效率低,生产者每次只能向缓冲池投放一个产品(消息),消费者每次只能从缓冲区中取得一个消息。
②通信对用户不透明,OS只为进程之间的通信提供了共享存储器,关于进程之间通信所需之共享数据结构的设置、数据的传送、进程的互斥与同步,都必须由程序员去实现。
在进程之间要传送大量数据时,应当利用OS提供的高级通信工具,该工具最主要的特点是:
①使用方便。OS隐藏了实现进程通信的具体细节,向用户提供了一组用于实现高级通信的命令(原语),用户可方便地直接利用它实现进程之间的通信,或者说通信过程对用户是透明的,这样就大大减少了通信程序编制上的复杂性。
②高效地传送大量数据。用户可直接利用高级通信命令(原语)高效地传送大量的数据。
二、进程通信的类型
1、共享存储
(1)在共享存储器系统中,相互通信的进程共享某些数据结构或共享存储区,进程之间能够通过这些空间进行通信。
(2)共享存储器系统可分成以下两种类型:
①基于共享数据结构的通信方式。在这种通信方式中,诸进程公用某些数据结构借以实现诸进程间的信息交换,如在生产者-消费者问题中的有界缓冲区。操作系统仅提供共享存储器,公用数据结构的设置及对进程间同步的处理,都是程序员的职责。这种通信方式仅适于传递相对少量的数据,通信效率低下,属于低级通信。
②基于共享存储区的通信方式。为了传输大量数据,在内存中划出了一块共享存储区域,诸进程可通过对该共享区的读或写交换信息,实现通信,数据的形式和位置甚至访问控制都是由进程负责,而不是OS。需要通信的进程在通信前先向系统申请获得共享存储区中的一个分区,并将其附加到自己的地址空间中,便可对其中的数据进行正常读、写,读写完成或不再需要时,将其归还给共享存储区。这种通信方式属于高级通信。
2、管道通信
(1)所谓“管道”,是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名pipe文件。向管道(共享文件)提供输入的发送进程(即写进程)以字符流形式将大量的数据送入管道,而接受管道输出的接收进程(即读进程)则从管道中接收(读)数据。由于发送进程和接收进程是利用管道进行通信的,故又称为管道通信。这种方式首创于UNIX系统,由于它能有效地传送大量数据,因而又被引入到许多其它操作系统中。
(2)为了协调双方的通信,管道机制必须提供以下三方面的协调能力:
①互斥,即当一个进程正在对pipe执行读/写操作时,其它(另一)进程必须等待。
②同步,指当写(输入)进程把一定数量(如 4KB)的数据写入pipe,便去睡眠等待,直到读(输出)进程取走数据后再把它唤醒;当读进程读一空pipe时,也应睡眠等待,直至写进程将数据写入管道后才将之唤醒。
③确定对方是否存在,只有确定了对方已存在时才能进行通信。
(3)一个管道只能实现半双工通信,实现双向同时通信需要建立两个管道。
(4)数据一旦从管道中被读出就会被管道抛弃,这就意味着读进程最多只能有一个,否则可能会有读错数据的情况,对此有两种解决方案:
①一个管道允许有多个写进程、一个读进程访问。
②一个管道允许有多个写进程和读进程访问,但系统会让各个读进程轮流从管道中读数据。
3、消息传递
在该机制中,进程不必借助任何共享存储区或数据结构,而是以格式化的消息层为单位,将通信的数据封装在消息中,并利用操作系统提供的一组通信命令(原语)在进程间进行消息传递,完成进程间的数据交换。
该方式隐藏了通信实现细节,使通信过程对用户透明化,降低了通信程序设计的复杂性和错误率,成为当前应用最为广泛的一类进程间通信的机制。
基于消息传递系统的通信方式属于高级通信方式,因其实现方式的不同,可进一步分成两类:
①直接通信方式:发送进程利用OS所提供的发送原语,直接把消息发送给目标进程。
②间接通信方式:发送和接收进程都通过共享中间实体(称为邮箱)的方式进行消息的发送和接收,完成进程间的通信。(可以多个进程往同一个邮箱发送消息,也可以多个进程从同一个邮箱接收消息)
4、客户机-服务器系统(简介)
前面所述的共享内存、消息传递等技术,虽然也可以用于实现不同计算机间进程的双向通信,但客户机-服务器系统的通信机制,在网络环境的各种应用领域已成为当前主流的通信实现机制,其主要的实现方法分为三类:套接字、远程过程调用和远程方法调用。
三、消息传递通信的实现方式
1、直接消息传递系统
(1)在直接消息传递系统中采用直接通信方式,即发送进程利用OS所提供的发送命令(原语)直接把消息发送给目标进程。
(2)直接通信原语:
①对称寻址方式。该方式要求发送进程和接收进程都必须以显式方式提供对方的标识符,通常系统提供下述两条通信命令(原语):
send(receiver, message); //发送一个消息给接收进程receiver
receive(sender, message); //接收sender发来的消息
②非对称寻址方式。在某些情况下,接收进程可能需要与多个发送进程通信,无法事先指定发送进程,对于这样的应用,在接收进程的原语中不需要命名发送进程,只填写表示源进程的参数,即完成通信后的返回值,而发送进程仍需要命名接收进程。该方式的发送和接收原语可表示为:
send(P, message); //发送一个消息给接收进程P
receive(id, message); //接收任何进程发来的消息
③利用直接通信原语解决生产者-消费者问题:
当生产者生产出一个产品(消息)后,便用send原语将消息发送给消费者进程,而消费者进程则利用receive原语来得到一个消息。若消息尚未产生出来,消费者必须等待,直至生产者进程将消息发送过来。
(3)消息的格式:
①在单机系统环境中,由于发送进程和接收进程处于同一台机器中,有着相同的环境,所以消息的格式比较简单,可采用比较短的定长消息格式,以减少对消息的处理和存储开销。该方式可用于办公自动化系统中,为用户提供快速的便笺式通信,但这种方式对于需要发送较长消息的用户是不方便的。
②变长的消息格式,即进程所发送消息的长度是可变的,对于变长消息,系统无论在处理方面还是存储方面都可能会付出更多的开销,但其优点在于方便了用户。
(4)进程的同步方式:
在进程之间进行通信时同样需要有进程同步机制,以使诸进程间能协调通信。不论是发送进程还是接收进程,在完成消息的发送或接收后都存在两种可能性,即进程或者继续发送(或接收)或者阻塞,由此可得到三种情况:
①发送进程阻塞,接收进程阻塞。这种情况主要用于进程之间紧密同步,发送进程和接收进程之间无缓冲时。
②发送进程不阻塞、接收进程阻塞。这是一种应用最广的进程同步方式,平时发送进程不阻塞,因而它可以尽快地把一个或多个消息发送给多个目标,而接收进程平时则处于阻塞状态,直到发送进程发来消息时才被唤醒。
③发送进程和接收进程均不阻塞。这也是一种较常见的进程同步形式,平时发送进程和接收进程都在忙于自己的事情,仅当发生某事件使它无法继续运行时,才把自己阻塞起来等待。
(5)通信链路:
①为使在发送进程和接收进程之间能进行通信,必须在两者之间建立一条通信链路。
②有两种方式建立通信链路:
[1]显式连接:由发送进程在通信之前用显式的“建立连接”命令请求系统为之建立一条通信链路,在链路使用完后拆除链路。这种方式主要用于计算机网络中。
[2]隐式连接:发送进程无须明确提出建立链路的请求,只须利用系统提供的发送命令,系统会自动地为之建立一条链路。这种方式主要用于单机系统中。
③根据通信方式的不同,则又可把链路分成两种:
[1]单向通信链路,只允许发送进程向接收进程发送消息,或者相反。
[2]双向通信链路,既允许由进程A向进程B发送消息,也允许进程B同时向进程A发送消息。
2、信箱通信
(1)信箱通信属于间接通信方式,即进程之间的通信,需要通过某种中间实体(如共享数据结构等)来完成,该实体建立在随机存储器的公用缓冲区上,用来暂存发送进程发送给目标进程的消息,接收进程可以从该实体中取出发送进程发送给自己的消息,通常把这种中间实体称为邮箱(或信箱),每个邮箱都有一个唯一的标识符。
(2)消息在邮箱中可以安全地保存,只允许核准的目标用户随时读取,因此,利用邮箱通信方式既可实现实时通信,又可实现非实时通信。
(3)信箱通信原语:
①邮箱的创建和撤消。进程可利用邮箱创建原语来建立一个新邮箱,创建者进程应给出邮箱名字、邮箱属性(公用、私用或共享),对于共享邮箱还应给出共享者的名字;当进程不再需要读邮箱时,可用邮箱撤消原语将之撤消。
②消息的发送和接收。当进程之间要利用邮箱进行通信时,必须使用共享邮箱,并利用系统提供的下述通信原语进行通信。
Send(mailbox, message); //发送一个消息到指定邮箱mailbox
Receive(mailbox, message); //从指定邮箱mailbox接收一个消息
(4)信箱的类型:
①私用邮箱。用户进程可为自己建立一个新邮箱,并作为该进程的一部分,邮箱的拥有者有权从邮箱中读取消息,其他用户只能将自己构成的消息发送到该邮箱中,这种私用邮箱可采用单向通信链路的邮箱来实现。当拥有该邮箱的进程结束时,邮箱也随之消失。
②公用邮箱。由操作系统创建,并提供给系统中的所有核准进程使用。核准进程既可把消息发送到该邮箱中,也可从邮箱中读取发送给自己的消息,显然,公用邮箱应采用双向通信链路的邮箱来实现。通常公用邮箱在系统运行期间始终存在。
③共享邮箱。由某进程创建,在创建时或创建后指明它是可共享的,同时须指出共享进程(用户)的名字,邮箱的拥有者和共享者都有权从邮箱中取走发送给自己的消息。
四、消息缓冲队列通信机制
1、消息缓冲队列通信机制中的数据结构
(1)在消息缓冲队列通信方式中,主要利用的数据结构是消息缓冲区,其描述如下:
typrdef struct message_buffer
{
int sender; //发送者进程标识符
int size; //消息长度
char *text; //消息正文
struct message_buffer *next; //指向下一个消息缓冲区的指针
};
(2)在操作系统中采用了消息缓冲队列通信机制时,除了需要为进程设置消息缓冲队列外,还应在进程的PCB中增加消息队列队首指针用于对消息队列进行操作,以及用于实现同步的互斥信号量mutex和资源信号量sm。在PCB中应增加的数据项可描述如下:
typrdef struct processcontrol_block
{
…
struct message_buffer *mq; //消息队列队首指针
semaphore mutex; //消息队列互斥信号量
semaphore sm; //消息队列资源信号量
…
}PCB;
2、发送原语
发送进程在利用发送原语发送消息之前,应先在自己的内存空间设置一发送区a,把待发送的消息正文、发送进程标识符、消息长度等信息填入其中,然后调用发送原语,把消息发送给目标(接收)进程。
发送原语首先根据发送区a中所设置的消息长度a.size来申请一缓冲区i,接着把发送区a中的信息复制到缓冲区i中。为了能将i排在接收进程的消息队列mq上,应先获得接收进程的内部标识符j,然后将i挂在j.mq上。由于该队列属于临界资源,故在执行insert操作的前后都要执行wait和signal操作。
3、接收原语
接收进程调用接收原语receive(b),从自己的消息缓冲队列mq中摘下第一个消息缓冲区i,并将其中的数据复制到以b为首址的指定消息接收区内。