1. 关中断
关中断 —— 最简单的方法之一
进入锁测试之前关闭中断,直到完成锁测试并上锁之后才能打开中断 —— 计算机系统不响应中断,保证锁测试和关锁操作的连续性和完整性
缺点:
1)滥用关中断权力可能导致严重后果
2)关中断时间过长,会影响系统效率,限制了处理器交叉执行程序的能力
3)关中断方法也不适用于多CPU系统,因为在一个处理器上关中断并不能防止进程在其他处理器上执行相同的临界段代码
2. 利用 Test-and-Set指令实现互斥
利用 Test-and-Set指令实现互斥 —— 硬件指令 —— 一条原语
boolean TS(boolean *lock) { boolean old; old = *lock; *lock = TRUE; //TRUE表示资源正被使用,FLASE表示资源空闲 return old; }
用TS指令管理临界区时,为每个临界资源设置一个布尔变量lock,表示该资源的状态,初始值为FALSE,表示资源空闲。
在进入临界区之前,首先用TS指令测试lock,如果值为FALSE,则表示没有进程在临界区内,可以进入,并将lock置为TRUE,等效于关闭了临界资源,使任何进程都不能进入临界区。
do{ while TS(&lock); critical section; // 临界区 lock = FALSE; remainder section; }while(TRUE);
3. 利用Swap指令实现进程互斥
利用Swap指令实现进程互斥 —— 对换指令 —— 用于交换两个字的内容
用对换指令可以简单有效地实现互斥,方法是为每个临界资源设置一个全局的布尔变量lock,其初值为false,在每个进程中再利用一个全局布尔变量key。
do{ key = TRUE; do{ swap(&lock,&key); }while(key != FALSE); 临界区操作; lock = FALSE; 剩余区; }while(TRUE);
2.4.3信号量机制
在长期且广泛应用中,信号量机制得到了很大的发展
整型信号量 —— 记录型信号量 —— “信号量集”机制
1. 整型信号量
整型信号量
整型量S —— 表示资源数目(除初始化外,仅能通过P、V操作)
wait(S)、signal(S) —— 两个标准的原子操作,分别称为P、V操作
wait(S){ while(S <= 0); /*do no-op*/ 即什么都不做 S--; } signal(S){ S++; }
P、V操作是两个原子操作,因此在执行时不可中断,亦即,当一个进程在修改某信号量时,没有其他进程可同时对该信号量进行修改。
整型信号量中的wait操作,只要是信号量S<=0,就会不断地测试。因此,该机制并未遵循“让权等待”的准则,而是使进程处于“忙等”的状态。
2. 记录型信号量
记录型信号量 —— 一种不存在“忙等”现象的进程同步机制
为了防止采取“让权等待”的策略后,会出现的多个进程等待访问同一临界资源的情况,除了需要一个用于代表资源数目的整型变量value外,还应增加一个进程链表指针list,用于链接上述的所有等待进程。记录型信号量是由于采用了记录型数据结构而得名的。
//数据结构 typedef struct{ int value; struct process_control_block *list; }semaphore; wait(semaphore S){ S->value--; if(S->value < 0) block(S->list); } signal(semaphoree S){ S->value++; if(S->value <= 0) wakeup(S->list); //value <= 0 说明还有等待进程
wait()中当S-value < 0时,进程调用block原语进行自我阻塞,放弃处理机,并插入到信号量链表S->list中,此时S->value的绝对值表示在该信号量链表中已阻塞进程的数目。
signal()中当释放一个资源后S->value <= 0,则表示在该信号量链表中仍有等待资源的进程被阻塞,应调用wakeup原语,将S->list链表中第一个等待进程唤醒。
如果S->value初值为1,表示只允许一个进程访问临界资源,此时的信号量转化为互斥信号量,用于进程互斥。
3. AND型信号量
AND信号量 —— 一个进程往往需要获得两个或更多的共享资源
AND同步机制的基本思想是:将进程在整个运行过程中需要的所有资源,一次性全部地分配给进程,待进程使用完后再一起释放。只要尚有一个资源未能分配给进程,其他所有可能为之分配的资源也不分配给它。
也就是说,对若干个临界资源的分配采取原子操作方式:要么把它所请求的资源全部分配给进程,要么一个也不分配。
Swait(S1,S2,...,Sn) { while(TRUE) { if(S1>=1 && ... && Sn>=1){ for(i=1; i<=n; i++) Si--; break; }else{ place the process in the waiting queue associated with the first Si found with Si < 1, and set the program count of this process to the beginning of Swait operation (将此进程放到第一个Si<1的等待队列,并将程序指针指向该进程的Swait 操作开始处) } } } Ssignal() { while(TRUE) { for(i=1; i<=n; i++) { Si++; Remove all the process waiting in the queue associated with Si into the ready queue (将所有与Si相关的等待队列中的进程移动到准备队列) } }
信号量集
前面所述的记录型信号量机制中,wait(S) 和 signal(S)操作仅能对信号量施以加1或减1操作,意味着每次只能对某类临界资源进行一个单位的申请或释放。
如果一次需要N个资源,便要进行N次wait操作,这显然是低效的而且会增加死锁的概率。此外,在有些情况下,为确保系统的安全性,当所申请某类资源数量低于某一下限值时,还必须进行管制,不予以分配。
因此,当进程申请某类临界资源时,在每次分配之前,都必须测试资源的数量,判断是否大于可分配的下限值,决定是否予以分配。
可以对AND信号量机制加以扩充,对进程所申请的所有资源以及每类资源不同的资源需求量,在一次P、V原语操作中完成申请或释放。
进程对信号量Si的测试值不再是1,而是该资源的分配下限ti,要求Si > ti,否则不予以分配。一旦允许分配,进程对该资源的需求值为di。
Swait(S1,t1,d1,…,Sn,tn,dn);
(将上述比较改成Si > ti ; 资源分配改成 Si = Si - di)
Ssignal(S1,d1,…,Sn,dn)
(将上述资源的释放改成Si = Si + di)
一般“信号量集”还有以下几种特殊情况:
1)Swait(S,d,d)。此时在信号量集中只有一个信号量S,但允许它每次申请d个资源,当现有资源数少于d时,不予以分配。
2)Swait(S,1,1)。
S>1 —— 一般的记录型信号量
S=1 —— 互斥信号量
3)Swait(S,1,0)。这是一种很特殊且很有用的信号量操作。当S>=1时,允许多个进程进入某特定区;当S变为0后,将阻止任何进程进入特定区。换言之,它相当于一个可控开关。
2.4.4信号量的应用
1. 利用信号量实现进程互斥
利用信号量实现进程互斥
为使多个进程能互斥地访问某临界资源,只需为该资源设置一个互斥信号量mutex,并设其初始值为1,然后将各进程访问该资源的临界区CS置于wait(mutex)和signal(mutex)操作之间即可。
利用信号量实现两个进程互斥的描述如下:
mutex,初值1,取值范围(-1,0,1)
1
mute取值 意义
1 两个进程都未进入需要互斥的临界区
0 一个进程进入临界区运行,另一个必须等待,挂入阻塞队列
-1 一个进程正在临界区运行,另外一个进程因等待而阻塞在信号量队列中,需要被当前已在临界区运行的进程退出时唤醒
semaphore mutex=1; P1(){ while(1){ wait(mutex); 临界区; signal(mutex); 剩余区; } } P2(){ while(1){ wait(mutex); 临界区; signal(mutex); 剩余区; } }
在利用信号量机制实现进程互斥时,wait(mutex)和signal(mutex)必须成对地出现。
缺少wait(mutex) —— 不能保证对临界资源的互斥访问
缺少signal(mutex) —— 临界资源永不释放,从而使因等待该资源而阻塞的进程不能被唤醒
2. 实现前驱关系
实现前驱关系
P1有语句S1,P2有语句S2,需要在S1执行之后再执行S2
为实现这种前驱关系,只需使进程P1和P2共享同一个信号量S,并赋予其初值为0,将signal(S)放在S1后面,wait(S)放在S2前面。
即进程P1 —> S1; siganl(S);
进程P2 —> wait(S); S2;
2.4.5 管程机制
虽然信号量机制是一种既方便、又有效的进程同步机制,但每个要访问临界资源的进程都必须具备同步操作wait(S)和signal(S)。这就使大量的同步操作分散在各个进程中。这样不仅给系统的管理带来了麻烦,而且还会因同步操作的使用不当而导致系统死锁。
1. 管程的定义
系统中的各种硬件资源和软件资源均可用数据结构抽象地描述其特性,即用少量信息和对该资源所执行的操作来表征该资源,而忽略它们的内部结构和实现细节。
因此,可以利用共享数据结构抽象地表示系统中的共享资源,并且将对该共享数据结构实施的特定操作定义为一组过程。进程对共享资源的申请、释放和其他操作必须通过这组过程,间接地对共享数据结构实现操作。对于请求访问共享资源的诸多并发进程,可根据资源的情况接受或阻塞,确保每次仅有一个进程进入管程,执行这组过程,使用共享资源,达到对共享资源所有访问的统一管理,有效地实现进程互斥。
管程 —— 代表共享资源的数据结构以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序共同构成了一个操作系统的资源管理模块
管程被请求和释放资源的进程所调用。
Hansan对其定义:“一个管程定义了一个数据结构和能为并发进程所执行的一组操作,这组操作能同步进程和改变管程中的数据”
管程由四部分组成:
1)局部于管程的共享数据结构说明
2)对该数据结构进行操作的一组过程
3)对局部于管程的共享数据设置初始值的语句
管程包含了面向对象的思想,将表征共享资源的数据结构及其对数据结构操作的一组过程,包括同步机制,都封装在一个对象内部,隐藏了实现细节。
封装于管程内部的数据结构仅能被封装于管程内部的过程所访问,任何管程外的过程都不能访问它;
反之,封装于管程内部的过程也仅能访问管程内的数据结构。
所有过程要访问临界资源时,都只能通过管程间接访问,而管程每次只准许一个进程进入管程,执行管程内的过程,从而实现了进程互斥。
管程的特性 | 解释 |
模块化 | 管程是一个基本程序单位,可以单独编译 |
抽象数据类型 | 管程中不仅有数据,而且有对数据的操作 |
信息屏蔽 | 管程中的数据结构只能被管程中的过程访问,这些过程是在管程内部定义的,供管程外的进程调用,而管程中的数据结构以及过程(函数)的具体实现外部不可见 |
管程 | 进程 |
公共数据结构(如消息队列) | 私有数据结构(PCB) |
进行同步操作和初始化操作 | 由顺序程序执行相关操作 |
为了解决共享资源互斥使用问题 | 为了实现系统的并发性 |
被动工作方式(被进程调用) | 主动工作方式 |
管程不能与调用者并发 | 进程之间可以并发执行 |
操作系统中的一个资源管理模块 | 具有动态性,创建而生,撤销而亡 |
2. 条件变量
管程的条件变量
当某进程通过管程请求获得临界资源而未能满足时,管程便调用wait原语使该进程等待,并将其排在等待队列上。仅当另一进程访问完成并释放资源后,管程才又调用signal原语,唤醒等待队列中的队首过程。
如果一个进程调用了管程,在管程中时被阻塞或挂起,直到阻塞或挂起的原因解除,在此期间,如果该进程不释放管程,则其他进程无法进入管程,被迫长时间的等待。
为了解决这个问题,引入了条件变量condition,一个进程被阻塞或挂起的条件(原因)可有多个,因此在管程中设置了多个条件变量,对这些条件变量的访问只能在管程中进行。
管程中对每个条件变量都予以说明,其形式为condition x,y; 对条件变量的操作仅仅是wait和signal,因此条件变量也是一种抽象数据类型,每个条件变量保存了一个链表,用于记录因该条件变量而阻塞的所有进程,同时提供的两个操作即可表示为
1)x.wait: 正在调用管程的进程因x条件需要被阻塞或挂起,则调用x.wait将自己插入到x条件的等待队列上,并释放管程,直到x条件变化。此时其他进程可以使用该管程。
2)x.signal:正在调用管程的进程发现x条件变化,则调用x.signal,重新启动一个因x条件而阻塞或挂起的进程,如果存在多个,则选择其中的一个,如果没有,继续执行原进程。
如果有进程Q因x条件处于阻塞状态,当正在调用管程的进程P执行了x.signal操作后,进程Q被重启,此时有两种方式:
1)P等待,直至Q离开管程或等待另一条件
2)Q等待,直至P离开管程或等待另一条件
2.5 经典进程的同步问题
2.6 进程通信
进程通信是指进程之间的信息交换。
2.6.1 进程通信的类型
高级通信机制可归纳为四类:
共享存储器系统
消息传递系统
管道通信系统
客户机—服务器系统
1. 共享存储器系统
共享存储器系统 Shared-Memory System
两种类型:
基于共享数据结构的通信方式 | 基于共享存储区的通信方式 |
进程公用某些数据结构,借以实现进程间的信息交换,操作系统仅提供共享存储器,由程序员负责对公用数据结构的设置及对进程间同步的处理 | 为了传输大量数据,在内存中划出了一块共享存储区域,诸进程可通过对该共享区的读或写交换信息,实现通信,数据的形式和位置甚至访问控制都是由进程负责,而不是OS |
低级通信,仅适于传递相对少量的数据,通信效率低下 | 高级通信,需要通信的进程在通信前,先向系统申请获得共享存储区的一个分区,并将其附加到自己的地址空间中,便可对其中的数据进行正常读、写,读写完成或不再需要时,将其归还给共享存储区 |
2. 管道(pipe)通信系统
管道(pipe)通信系统
管道 —— 指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名pipe文件。
向管道提供输入的发送进程(写进程)以字符流形式将大量的数据送入管道;而接收管道输出的接收进程(读进程)则从管道中接收数据。
这种方式首创于UNIX系统,由于它能有效地传送大量数据,因而又被引入到许多其他操作系统中。
为了协调双方的通信,管道机制必须提供以下三方面的协调能力:
1)互斥,当一个进程正在对pipe执行读/写操作时,其他(另一个)进程必须等待。
2)同步,当写(输入)进程把一定数据写入pipe,便去睡眠等待,直到读(输出)进程取走数据后再把它唤醒。当读进程读一空pipe时,也应睡眠等待,直至写进程将数据写入管道后才将之唤醒。
3)确定对方是否存在,只有确定了对方已存在时才能进行通信。
3. 消息传递系统 Message passing system —— 高级通信方式
在此机制中,进程不必借助任何共享存储区或数据结构,而是以格式化的消息为单位,将通信的数据封装在消息中,并利用操作系统提供的一组通信命令(原语),在进程间进行消息传递,完成数据间的数据交换。
该方式隐藏了通信实现细节,使通信过程对用户透明化,降低了通信程序设计的复杂性和错误率,成为当前应用最为广泛的一类进程通信间通信的机制。
例如:计算机网络中的报文;微内核操作系统中的微内核与服务器的通信;
这机制很好地支持多处理机系统、分布式系统和计算机网络,因此成为这些领域最主要的通信工具。
根据实现方式不同,分成两类:
直接通信方式 | 间接通信方式 |
发送进程利用OS所提供的发送原语,直接把消息发送给目标进程 | 发送和接收进程,都通过共享中间体(称为邮箱)的方式进行消息的发送和接收,完成进程间的通信 |
4. 客户机—服务器系统 client-Server system
客户机—服务器系统 client-Server system
客户机-服务器系统的通信机制,在网络环境的各种应用领域已成为当前主流的通信实现机制,其主要的实现方法分为三类:
套接字、远程过程调用和远程方法调用
套接字 —— 一个通信标识类型的数据结构,包含了通信目的的地址、通信使用的端口号、通信网络的传输层协议、进程所在的网络地址,以及针对客户或服务器程序提供的不同系统调用(或API函数)等,是进程通信和网络通信的基本构件。
套接字优势:不仅适用于同一台计算机内部的进程通信,也适用于网络环境中不同计算机间的进程通信。由于每个套接字拥有唯一的套接字号,这样系统中所有的连接都持有唯一的一对套接字及端口连接,对于来自不同应用程序进程或网络连接的通信,能够方便地加以区分,确保了通信双方之间逻辑链路的唯一性,便于实现数据传输的并发服务,而隐藏了通信设施及实现细节,采用统一的接口进行处理。
远程过程调用和远程方法调用
远程过程调用RPC(Remote Process Call) —— 一个通信协议,用于通过网络连接的系统。该协议允许运行于一台主机系统上的进程调用另一台主机系统上的进程