2.6.2消息传递通信的实现方式
1. 直接消息传递系统
直接消息传递系统—— 利用OS所提供的发送命令(原语)
直接通信原语
1)对称寻址方式 —— 要求发送进程和接收进程都必须以显式方式提供对方的标识符。
系统提供以下两条通信命令:
send(receiver,message);
receive(sender,message);
不足:一旦改变进程的名称,则可能需要检查所有其他进程的定义,有关对该进程旧名称的所有引用都必须查找到,以便将其修改为新名称,显然,这样的方式不利于实现进程定义的模块化。
2)非对称寻址方式 —— 接收进程可能需要与多个发送进程通信,无法事先指定发送进程。因此,在接受进程的原语中,不需要命名发送进程,只填写表示源进程的参数,即完成通信后的返回值,而发送进程仍需要命名接收进程。
send(receiver,message);
receive(id,message); 接收来自任何进程的消息,id变量可设置为进行通信的发送方进程id或名字。
消息格式
比较短的定长消息格式 —— 减少对消息的处理和存储开销
变长的消息格式 —— 处理、存储方面付出更多的开销,方便用户
进程的同步方式
在完成消息的发送或接收后,存在三种情况:
1)发送进程阻塞,接收进程阻塞 —— 这种情况主要用于进程之间紧密同步,发送进程和接收进程之间无缓冲时。
2)发送进程不阻塞,接收进程阻塞 —— 发送进程不阻塞,因而它可以尽快地把一个或多个消息发送给多个目标;而接收进程平时处于阻塞状态,直到发送进程发来消息时才被唤醒
3)发送进程不阻塞,接收进程不阻塞 —— 发送进程和接收进程都在忙于自己的事情,仅当发生某事件使它无法继续运行时,才把自己阻塞起来等待
通信链路
通信双方建立链路,两种方式:
1)由发送进程在通信之前用显式的“建立连接”命令(原语)请求系统为之建立一条通信链路,在链路使用完成后拆除链路 —— 主要用于计算机网络
2)发送进程无须明确提出建立链路的请求,只须利用系统提供的发送命令(原语),系统会自动地为之建立一条链路 —— 主要用于单机系统中
根据通信方式的不同,把链路分成两种:
1)单向通信链路
2)双向通信链路
2. 信箱通信
间接通信方式 —— 信箱通信
进程之间的通信,需要通过某种中间实体来完成,该实体建立在随机存储器的公用缓冲区上,用来暂存发送进程发送给目标进程的消息;接收进程可以从该实体中取出发送进程发送给自己的消息,通常把这种中间实体称为邮箱,每个邮箱都有唯一的标识符。消息在邮箱中可以安全地保存,只允许核准的目标用户随时读取。既可实现实时通信,又可实现非实时通信。
信箱的结构 —— 一种数据结构,两个部分
1)信箱头,用以存放有关信箱的描述信息 —— 信箱标识符、信箱的拥有者、信箱口令、信箱的空格数等
2)信箱体 —— 由若干个可以存放消息的信箱格组成,信箱格的数目以及每格的大小是在创建信箱时确定的
信箱通信原语
1)邮箱的创建和撤销
进程可利用邮箱创建原语来建立一个新邮箱,创建者进程应给出邮箱名字、邮箱属性(公用、私用或共享);对于共享邮箱,还应给出共享者的名字,当进程不再需要读邮箱时,可用邮箱撤销原语将之撤销。
2)消息的发送和接收。当进程之间要利用邮箱进行通信时,必须使用共享邮箱,并利用系统提供的通信原语进行通信。
Send(mailbox,message);
Receive(mailbox,message);
信箱的类型
1)私用邮箱。用户进程可为自己建立一个新邮箱,并作为该进程的一部分。邮箱的拥有者有权从邮箱中读取消息,其他用户则只能将自己构成的消息发送到该邮箱中,可采用单向通信链路的邮箱来实现。当拥有该邮箱的进程结束时,邮箱也随之消失。
2)公用邮箱。由操作系统创建,并提供给系统种的所有核准进程使用。核准用户既可把消息发到该邮箱,也可从邮箱读取给自己的消息,应采用双向通信链路的邮箱来实现。在系统运行期间始终存在。
3)共享邮箱。由某进程创建,在创建时或创建后指明它是可共享的,同时须指出共享进程的名字。邮箱的拥有者和共享者都有权从邮箱中取走发送给自己的消息。
发送进程和接收进程存在以下四种关系:
1)一对一
2)多对一
3)一对多
4)多对多
————————————————
2.6.3 直接消息传递系统的实例
1.消息缓冲队列通信机制中的数据结构
1)消息缓冲区
typedef struct message_buffer { int sender; //发送者进程标识符 int size; //消息长度 char *text; //消息正文 struct message_buffer *next; //指向下一个消息缓冲区 }
2)PCB有关通信的数据项
在操作系统中采用消息缓冲队列通信机制时,除了需要为进程设置消息缓冲队列以外,还应该在进程的PCB中增加消息队列的首指针,用于对消息队列进行操作,以及用于实现同步的互斥信号量(mutex)和资源信号量(sm)
在PCB增加的数据项
typedef struct processcontrol_block { ... struct message_buffer *mq; //消息队列队首指针 semaphore mutex; //消息队列互斥信号量 semaphore sm; //消息队列资源信号量 ... }PCB
2.发送原语
发送进程在利用发送原语发送消息之前,应先在自己的内存空间设置一发送区a,把带发送的消息正文,发送进程标识符,消息长度等填入其中
//a是发送区首地址 void send(reciver,a) { getbuf(a.size,i); //根据a.size申请缓冲区 i.sender = a.sender; i.size = a.size; copy(i.text,a.text); //将发送区a中的消息复制到缓冲区i中 i.next=0; getid(PCBset,reciver.j); //获得接受进程内部的标识符j wait(j.mutex); //j.mq消息队列是临界资源,操作前要wait() insert(&j.mq,i); signal(j.mutex); signal(j.sm); }
3.接受原语
接收进程调用接收原语receive(b),从自己的消息缓冲队列mq中摘下第一个消息缓冲队列i,并将其中的数据复制到以b为首地址的指定消息接受区内。
void receive(b) { j = internal name; //接收进程内部的标识符 wait(j.sm); wait(j.mutex); remove(j.mq,i); //将消息队列中的第一个消息移出 signal(j.mutex); b.sender = i.sender; b.size = i.size; copy(b.text,i.text); //将消息缓冲区i中的信息复制到接受区b中 releasebuf(i); //释放消息缓冲区 }
————————————————
2.7 线程(Threads)的基本概念
2.7.1 线程的引入
引入进程,是为了使多个程序能并发执行,以提高资源利用率和系统吞吐量。引入线程,则是为了减少程序在并发执行时所付出的时空开销,使 OS 具有更好的并发性。
1.进程的两个基本属性
1)进程是一个可拥有资源的独立单位。
2)进程同时又是一个可独立调度和分派的基本单位。
程序并发执行所需付出的时空开销。
由于进程是一个资源的拥有者,因而在创建、撤消和切换中,系统必须为之付出较大的时空开销,因此限制了系统中设置进程的数量。
线程
设法将进程的上述两个属性分开,由 OS 分开处理,亦即对于作为调度和分派的基本单位,不同时作为拥有资源的单位;而对于拥有资源的基本单位,又不对之进行频繁的切换。正是在这种思想的指导下,形成了线程的概念。
2.7.2 线程和进程的比较
线程又称为轻型进程或进程元,传统进程称为重型进程。
1.调度的基本单位
在传统的 OS 中,作为拥有资源的基本单位和独立调度、分派的基本单位都是进程。而在引入线程的操作系统中,则把线程作为调度和分派的基本单位,而进程作为资源拥有的基本单位。
在同一进程中,线程的切换不会引起进程的切换,但从一个进程中的线程切换到另一个进程中的线程时,将会引起进程的切换。
2.并发性
在引入线程的操作系统中,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间亦可并发执行。
3.拥有资源
不论是传统的 OS,还是引入了线程的 OS,进程都可以拥有资源,并作为系统中拥有资源的一个基本单位。线程本身并不拥有系统资源,而是仅有一点必不可少的、能保证独立运行的资源,例如线程控制块等。
允许一个进程中的多个线程贡献该进程所拥有的资源,这主要表现在:属于同一进程的所有线程都具有相同的地址空间。
4.独立性
在同一进程中的不同线程之间的独立性要比不同进程之间的独立性低得多。因为进程之间是为了防止彼此干扰和破坏;而同一进程中的不同线程往往是为了提高并发性以及进行相互之间的合作而创建的。
5.系统开销
进程的创建、撤销和切换所付出的开销都明显大于线程的创建、撤销和切换的开销。
6.支持多处理机系统
对于单线程进程,不管多少处理机,同一时刻该进程只能运行在一个处理机上。对于多线程进程,就可以将一个进程中的多个线程分配到多个处理机上并行执行。
2.7.3 线程的状态和线程控制块
1.线程运行的三种状态
1)执行状态。2)就绪状态。3)阻塞状态。
2.线程控制块 TCB
所有用于控制和管理线程的信息都记录在线程控制块 TCB 中。线程控制块 TCB 中的信息:
1)线程标识符。2)一组寄存器。3)线程运行状态。4)优先级。5)线程专有存储器。6)信号屏蔽。7)堆栈指针,在指向的堆栈中通常保存有局部变量和返回地址。
3.多线程 OS 中的进程属性
1)进程是一个可拥有资源的基本单位。
2)通常一个进程中都含有若干个(至少一个)相对独立的线程。由进程为这些(个)线程提供资源及运行环境。多个线程可并发执行。
3)进程已不再是一个可执行的实体,而把线程作为独立运行的基本单位。虽然如此,进程仍具有与执行相关的状态。例如,所谓进程处于“执行”状态,实际上是指该进程中的某线程正在执行。此外,对进程所施加的与进程状态有关的操作,也对其线程起作用。例如,在把某个进程挂起时,该进程中的所有线程也都将被挂起;又如,在把某进程激活时,属于该进程的所有线程也都将被激活。
2.8 线程的实现
2.8.1 线程的实现方式
1.内核支持线程 KST
线程的创建、撤消和切换等都是在内核空间实现的。为每一个内核支持线程设置一个线程控制块以对内核线程进行控制和管理。
缺点:对于用户的线程切换而言,其模式切换的开销较大。因为用户进程的线程在用户态运行,而线程调度和管理是在内核实现的,系统开销较大。
2.用户级线程 ULT
用户级线程是在用户空间中实现的。对线程的创建、撤消、线程之间的同步与通信等功能,都无须内核的支持,即用户级线程是与内核无关的。
值得说明的是,对于设置了用户级线程的系统,其调度仍是以进程为单位进行的。
缺点:一个线程被阻塞,它所在的进程中的所有线程都会被阻塞。且多线程应用不能利用多处理机进行多重处理的优点,因为内核每次分配给一个进程的仅有一个 CPU,而进程中仅有一个线程能执行。
3.组合方式
把用户级线程和内核支持线程两种方式进行组合,提供了组合方式 ULT/KST 线程。一些内核支持线程对应多个用户级线程,这是用户级线程通过时分多路复用内核支持线程来实现的。
由于连接方式的不同,有三种不同的模型:
1)多对一模型。将用户级线程映射到一个内核控制线程,这些用户级线程一般属于一个进程。在任一时刻,只有一个线程能够访问内核,类似于前面的单纯的方式 2。
2)一对一模型。每一个用户级线程映射到一个内核支持线程。每创建一个用户线程,相应地就需要创建一个内核支持线程,开销较大。
3)多对多模型。许多用户级线程映射到同样数量或更少数量的内核支持线程上。克服了上述两种模型的缺点。
2.8.2 线程的实现
1.内核支持线程的实现
系统在创建一个新进程时,便为它分配一个任务数据区 PTDA,其中包括若干个线程控制块 TCB 空间。
每当进程要创建一个线程时,便为新线程分配一个 TCB,将有关信息填入该 TCB 中,并为之分配必要的资源。
有的系统中为了减少创建和撤消一个线程时的开销,在撤消一个线程时,并不立即回收该线程的资源和 TCB,当以后再要创建一个新线程时,便可直接利用已被撤消但仍保持有资源和 TCB 的线程作为新线程。
2.用户级线程的实现
用户级线程是在用户空间实现的。所有的用户级线程都运行在一个中间系统上。两种方式实现中间系统:
1)运行时系统。
所谓“运行时系统”,实质上是用于管理和控制线程的函数(过程)的集合,运行时系统中的所有函数都驻留在用户空间,并作为用户级线程与内核之间的接口。
不论在传统的 OS 中,还是在多线程 OS 中,系统资源都是由内核管理的。在传统的 OS 中,进程是利用 OS 提供的系统调用来请求系统资源的,系统调用通过软中断(如 trap)机制进入 OS 内核,由内核来完成相应资源的分配。用户级线程是不能利用系统调用的。当线程需要系统资源时,是将该要求传送给运行时系统,由后者通过相应的系统调用来获得系统资源的。
2)内核控制线程。
这种线程又称为轻型进程 LWP。LWP 可通过系统调用来获得内核提供的服务,这样,当一个用户级线程运行时,只要将它连接到一个 LWP 上,此时它便具有了内核支持线程的所有属性。这种线程实现方式就是组合方式。
每一个 LWP 都要连接到一个内核级线程上,这样,通过 LWP 可把用户级线程与内核线程连接起来,用户级线程可通过 LWP 来访问内核,但内核所看到的总是多个 LWP 而看不到用户级线程。
在内核级线程执行操作时,如果发生阻塞,则与之相连接的多个 LWP 也将随之阻塞,进而使连接到 LWP 上的用户级线程也被阻塞。
2.8.3 线程的创建和终止
在 OS 中有用于创建线程的函数(或系统调用)和用于终止线程的函数(或系统调用)。
1.线程的创建
应用程序在启动时,通常仅有一个“初始化线程”,它的主要功能是用于创建新线程。
2.线程的终止
有些线程(主要是系统线程),在它们一旦被建立起来之后,便一直运行下去而不再被终止。————————————————
习题
最后
小❤️伙🧡 伴💚们💙点 🖤个 🤍赞 🤎呗