一、基本概念
- 原子操作:一个函数(原语)或动作的指令序列不可分割,要么作为一个整体执行(不可中断),要么都不执行。
- 临界资源:一次仅允许一个进程独自占有使用的不可剥夺资源。
- 临界区:进程访问临界资源的那一段代码。
- 互斥:当一个进程正在临界区中访问临界资源时,其他进程不能进入临界区。
- 同步:合作的并发进程需要按先后次序执行。例如:一个进程的执行依赖于合作进程的消息或信号。当一个进程没有得到来自合作进程的消息或信号时需阻塞等待,直到消息或信号到达才唤醒。
- 死锁:多个进程全部阻塞,形成等待资源的循环链。
- 饥饿:一个就绪进程被调度程序长期忽视、不被调度执行。
- 信号量:用于进程间传递信号的一个整数。在信号两上只可以进行三种操作,即初始化,递增,递减。这三种操作分别都是原子操作。递减作用于阻塞一个进程,递增作用于解除一个进程的阻塞。信号量也称为计数信号量或一般信号量。
二、互斥
1、互斥的要求
- 强制互斥(忙则等待):一次只允许一个进程进入临界区。有进程正在临界区执行时,其他请求进程等待;等待进程退出后,从多个请求进程中选择一个进入临界区。
- 有限等待:请求进程应在有限的等待时间内进入临界区,不能造成进程死锁或饥饿。
- 有空让进:当临界区空闲时,请求进程可立即进入。
- 让权等待:进程不能进入临界区时,应释放处理器,避免忙等。
?#一个正在访问临界资源的进程由于申请等待I/O操作而被中断时,它可以允许其他进程抢占处理器,但是不能进入相关临界区。
2、用硬件实现中断
- 中断禁用
- 专用机器指令
3.信号量(☆☆☆☆☆)
- 信号量:不要求忙等的同步互斥工具。
- 一种信号量表示一种资源。信号量的值表示可用资源的个数。
- 当资源不可用时,进程将阻塞(避免忙等)(加入阻塞队列),直到另一进程释放资源时才被唤醒。
- 信号量s只能被下面的两个原语访问:
#semWait(s) 也称作P(s)操作,wait(s) ——本进程请求分配一个资源
#semSignal(s) 也称作V(s)操作,signal(s) ——本进程释放一个资源
·semWaits(s)、semSignal(s)操作必须成对出现。
·用于互斥时,位于同一进程内,临界区前后。
·用于同步时,交错出现于两个合作进程内。
·多个semWait()的次数不能颠倒,否则可能导致死锁。用于同步的semWait(s1)应出现在用于互斥的semWait(s2)之前。
·多个semSignal()操作的次序可任意。
struct semaphore { int count; queueType queue; }; void semWait(semaphore s){ s.count--; if(s.count < 0){ /*把当前进程插入队列*/ /*阻塞当前进程*/ } } void semSignal(semaphore s){ s.count++; if(s.count <= 0){ /*把进程p从队列中移除*/ /*把进程p插入就绪队列*/ } }
#用于互斥时,s的初值为1,取指为1~-(n-1)。
- s=1时:有一个临界资源可用,一个进程可进入临界区。
- s=0时:临界资源已分配,一个进程已进入临界区。
- s<0时:临界区已被占用,|s|个阻塞进程正在等待进入。
#用于同步时,s初值将>=0
- s>=0:可用资源个数(或可进入临界区的进程个数)。
- s<0 : 该资源的等待队列长度(阻塞进程个数)。
#强信号量:唤醒一个阻塞进程并移出阻塞队列时,采用FIFO移出策略的信号量。
#弱信号量:唤醒一个阻塞进程并移出阻塞队列时,采用随机选择移出策略的信号量。
#用信号量实现互斥:
const int n = /*进程数 */; semaphore s =1;/*s的初值=1*/ void P(int i){ /*进程Pi*/ while(true){ semWait(s); /*临界区*/ semSignal(s); /*其他部分*/ } } void main(){/*n个进程并发*/ parbegin(P(1),P(2),...,P(n)); }#用信号量实现同步:
semaphore s =0;/*s的初值=0*/ Process P1: do{ ...... 代码段A; semSignal(s); } Process P2: do{ semWait(s) 代码段B; ...... }
4.生产者/消费者问题
- 一组生产者进程和一组消费者进程共用一个有sizeofbuffer个缓冲区的缓冲池来交换数据(有限缓冲)。
- 资源,约束条件及信号量的设置
#缓冲池一次只能让一个进程访问。(互斥)
设一信号量s,初值为1。
#生产者需要空缓冲来发送数据。(同步)
设一信号量empty,初值为sizeofbuffer。
#消费者需要满缓冲来获取数据。(同步)
设一信号量full,初值为0。
void producter(){ while(true){ semWait(empty); semWait(s); /*把数据送到缓冲区*/ semSignal(s); semSignal(full); } } void consumer(){ while(true){ semWait(full); semWait(s); /*从缓冲区取出数据*/ semSignal(s); semSignal(empty); } }
5.读者/写者问题
有一个共享的数据对象:
- 允许多个读者进程同时读;
- 一次只允许一个写者进程写;当一个写者正在写时,不允许其他任何读者或写者同时访问该共享对象。
1.读者优先
- 当至少已有一个读者正在读时,随后的读者直接进入,开始读数据对象,但写者将等待。
- 但一个写者正在写时,随后到来的读者和写者都等待。
- 信号量的设置:
#一次只能让一个写者或一顿读者访问数据。
设一互斥信号量wsem,初值为1。
#正在读数据的读者数目由全局变量readcount表示(初值=0),它被多个读者互斥访问。
(第1个读者需对数据加锁,最后1个读者对数据解锁)
为readcount设一互斥信号量x,初值为1。
void reader(){ while(true){ semWait(x); readcount++; if(readcount == 1) semWait(wsem); semSignal(x); /*读数据对象*/ semWait(x); readcount--; if(readcount == 0) semSignal(wsem); semSignal(x); } } void writer(){ while(true){ semWait(wsem); /*写数据对象*/ semSignal(wsem); } }
2.写者优先
当一个写者声明想写时,不允许新的读者进入数据对象,只需要等到已有的读者读完即可开始写。可以避免写者饥饿。
(具体实现略,有时间再补充)
6.管程
管程是由一个或多个过程、一个初始过程、一个初始化序列和局部数据组成的软件模块。主要特点如下:
- 局部数据变量只能被管程的过程访问,任何外部过程都不能访问。
- 一个进程通过调用管程的一个过程进入管程。
- 在任何时候,只能有一个进程在管程中执行,调用管程的任何其他进程都被阻塞,以等待管程可用。
(个人理解:管程就是将semWait()和semSignal()和共享数据封装起来,使得只能通过调用管程里的相应函数才能实现其功能。这样子做好处在于避免了进程有意/无意的互斥/同步错误。)
- 条件变量(condition variable)
#表示进程正在等待的资源或原因。只用于维护等待队列,但没有相关联的值。
- 有两个函数可以操作条件变量:
#cwait(c):条件不满足时,调用cwait(c)的进程被放在条件变量c的等待队列中阻塞。
#csignal(c):唤醒一个条件变量为c的阻塞进程。
管程实现互斥/同步
7.消息
略(有时间再补充)