文章目录
- 一.进程同步、互斥
- 二.实现临界区互斥的基本方法
- (一)软件实现方法
- (二)硬件实现方法
- 三.互斥锁
- 四.信号量机制
- 五.经典同步问题
- (一)生产者-消费者问题
- (二)读者-写者问题
- (三)哲学家进餐问题
- (四)吸烟者问题
- 六.管程
一.进程同步、互斥
不同的进程之间存在什么关系?进程之间存在同步和互斥的制约关系。
1.同步
也称直接制约关系,指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。
2.互斥
也称间接制约关系。当一个进程进入临界区使用临界资源时,另一个进程必须等待,当占用临界资源的进程退出临界区后,另一进程才允许去访问此临界资源。
遵循原则:空闲让进、忙则等待、有限等待、让权等待(不能进入临界区时应当释放处理器)
3.临界资源的访问过程分为四个部分
(1)进入区
“上锁”。为了进入临界区使用临界资源,在进入区要检查可否进入临界区,若能进入临界区,则应设置正在访问临界区的标志,以阻止其他进程同时进入临界区
(2)临界区
进程中访问临界资源的那段代码,又称临界段
(3)退出区
“解锁”。将正在访问临界区的标志清除
(4)剩余区
代码中的其余部分
4.为什么要引入进程同步的概念?
在多道程序共同执行的条件下,进程与进程是并发执行的,不同进程之间存在不同的相互制约关系。为了协调进程之间的相互制约关系,引入了进程同步的概念
二.实现临界区互斥的基本方法
(一)软件实现方法
1.单标志法
第③步让给P1使用,若P1一直不使用,P0也会卡住。
违背“空闲让进”
2.双标志法先检查
flag[0]=true表示P0想进入临界区
flag[1]=true表示P1想进入临界区
双方先检查对方是否想进入,若不,表明自己进入意愿。若按①⑤②⑥的顺序执行代码,则二者同时进入临界区。
违背“忙则等待”
原因:“检查”和“上锁”不是一气呵成的
3.双标志法后检查
按①⑤②⑥的顺序执行,双方同时上锁,②⑥不断循环,双方都无法进入临界区
违背“空闲让进”、“有限等待”
4.Peterson算法
②表示P0谦让,⑦表示P1谦让,最后做出谦让的需等待
最后谦让方卡在while循环上不断检查,一直在CPU上运行,违背了“让权等待”
(二)硬件实现方法
1.中断屏蔽方法
当一个进程正在使用处理机执行它的临界区代码时,防止其他进程进入其临界区进行访问的最简方法是,禁止一切中断发生,或称为屏蔽中断、关中断。
即:关中断;临界区;开中断。关中断后不允许当前进程被中断,也不会发生进程切换。直到当前进程访问完临界区,再执行开中断指令,才有可能有别的进程上处理机并访问临界区
优点:简单、高效
缺点:不适用于多处理机(关中断是针对指定处理机的,对其他处理机不受影响,可能出现两个处理机上的两个进程同时访问处理机);只适用于操作系统内核进程,不适用于用户进程(因为开/关中断指令只能运行在内核态,这组指令如果能让用户随意使用会很危险)
2.硬件指令方法
(1)TestAndSet指令/TS/TSL指令:读出指定的标志后把该标志设置为真。这条指令是原子操作,即执行该代码时不允许被中断。
若初始lock为true,则old返回仍为true,while一直循环。
当前访问临界区的进程退出临界区,将lock改为false,下一个进程才能跳出while循环
违背“让权等待”
(2)Swap指令/XCHG指令:交换两个字(字节)的内容
违背“让权等待”
三.互斥锁
进程时间片用完才下处理机,违背“让权等待”
解决临界区最简单的工具就是互斥锁。一个进程在进入临界区时应获得锁;在退出临界区时释放锁。函数acquire()获得锁,而函数release()释放锁。每个互斥锁有一个布尔变量available,表示锁是否可用。如果锁是可用的,调用acqiure()会成功,且锁不再可用。当一个进程试图获取不可用的锁时,会被阻塞,直到锁被释放。
需要连续循环忙等的互斥锁,都可称为自旋锁,如TSL指令、swap指令、单标志法
优点:等待期间不用切换进程上下文,多处理器系统中,若上锁的时间短,则等待代价很低
常用于多处理器系统,一个核忙等,其他核照常工作,并快速释放临界区。不太适用于单处理机系统,忙等的过程中不可能解锁。
四.信号量机制
信号量机制可用来解决互斥与同步问题,它只能被两个标准的原语wait(S)和signal(S),也可记为“P操作”和“V操作”。
1.整型信号量
违背“让权等待”
wait(S) {
while (S <= 0);
S = S - 1;
}
signal(S) {
S = S + 1;
}
2.记录型信号量
满足“让权等待”
typedef struct {
int value;//剩余资源数
struct process *L;//等待队列L
}semaphore;
当S.value<0时,S.value的绝对值表示等待进程个数
void wait(semaphore S) {//请求一个资源
S.value--;
if (S.value < 0) {
add this process to S.L;//插入该类资源的等待队列S.L
block(S.L);//自我阻塞,放弃处理机
}
}
void signal(semahore S) {//释放一个资源
S.value++;
if (S.value <= 0) {
remove a process P from S.L;//从该类资源的等待队列S.L上移除,进入就绪队列
wakeup(P);//将S.L中的第一个等待进程唤醒
}
}
3.利用信号量实现进程同步
语句x执行后y才能执行
semaphore S = 0;
P1() {
x;
V(S);
}
P2() {
P(S);
y;
}
4.利用信号量实现进程互斥
临界资源的互斥访问
semaphore S = 1;
P1() {
P(S);
进程P1的临界区;
V(S);
}
P2() {
P(S);
进程P2的临界区;
V(S);
}
5.利用信号量实现前驱关系
要为每一对前驱关系各设置一个同步信号量,在“前操作”之后对相应的同步信号量执行V操作,在“后操作”之前对相应的同步信号量执行P操作(前V后P)
五.经典同步问题
(一)生产者-消费者问题
生产者和消费者对缓冲区互斥访问,生产者生产之后消费者才能消费。
1.基本问题
semaphore mutex = 1;
semaphore empty = n;
semaphore full = 0;
producer() {
while (1) {
produce an item in nextp;//生产数据
P(empty);//获取空缓冲区单元
P(mutex);//进入临界区
add nextp to buffer;//将数据放入缓冲区
V(mutex);//离开临界区,释放互斥信号量
V(full);//满缓冲区数加1
}
}
consumer() {
while (1) {
P(full);//获取满缓冲区单元
P(mutex);
remove an item from buffer;//从缓冲区取出数据
V(mutex);
V(empty);//空缓冲区数加1
consume the item;//消费数据
}
}
2.复杂问题
本例中对盘子的互斥访问无需设置mutex信号量
semaphore plate = 1, apple = 0, orange = 0;
dad() {
while (1) {
prepare an apple;
P(plate);
put the apple on the plate;
V(apple);
}
}
mom() {
while (1) {
prepare an orange;
P(plate);
put the orange on the plate;
V(orange);
}
}
son() {
while (1) {
P(orange);
take an orange from the plate;
V(plate);
eat the orange;
}
}
daughter() {
while (1) {
P(apple);
take an apple from the plate;
V(plate);
eat the apple;
}
}
(二)读者-写者问题
有读者和写者两组并发进程,共享一个文件,当两个或以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求:①允许多个读者可以同时对文件执行读操作;②只允许一个写者往文件中写信息;③任一写者在完成写操作之前不允许其他读者或写者工作;④写者执行写操作前,应让已有的读者和写者全部退出。
int conut = 0;//读者数量
semaphore mutex = 1;
semaphore rw = 1;//保证读者和写者互斥地访问文件
writer() {
while (1) {
P(rw);
writing;
V(rw);
}
}
reader() {
while (1) {
P(mutex);//对count互斥访问
if (count == 0)
P(rw);//第一个读者来的时候rw减一即可(只有第一个读者需要加锁)
count++;//读者计数器加1
V(mutex);
reading;//读取
P(mutex);//对count互斥访问
count--;//读者退出
if (count == 0)
V(rw);//最后一个读者退出,rw加1
V(mutex);
}
}
在上面算法中,读进程优先,会使得写操作被延迟。若有源源不断的读进程,会导致写进程饿死
代码改为
int conut = 0;//读者数量
semaphore mutex = 1;
semaphore rw = 1;
semaphore w = 1;
writer() {
while (1) {
P(w);//新增
P(rw);
writing;
V(rw);
V(w);//新增
}
}
reader() {
while (1) {
P(w);//② 写进程执行P(w)后下一个读进程将被卡住,防止写进程饿死
P(mutex);
if (count == 0)
P(rw);
count++;
V(mutex);
V(w);//① w+1后,写进程即可执行P(w),卡在P(rw)
reading;
P(mutex);
count--;
if (count == 0)
V(rw);
V(mutex);
}
}
(三)哲学家进餐问题
一张圆桌边上坐着5名哲学家,每两名哲学家之间的桌上摆一根筷子,两根筷子中间是一碗米饭。哲学家们倾注毕生精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿时,才试图拿起左、右两根筷子(一根一根地拿起)。若筷子已在他人手上,则需要等待。饥饿的哲学家只有同时拿到了两根筷子才可以开始进餐,进餐完毕后,放下筷子继续思考。
semaphore chopstick[5] = { 1,1,1,1,1 };
semaphore mutex = 1;
Pi() {
do {
P(mutex);//若同时拿起左边筷子,会造成死锁,所以要求至少有一人在左右筷子均可拿起时才动手
P(chopstick[i]);//取左边筷子
P(chopstick[(i + 1) % 5]);//取右边筷子
V(mutex);
eat;
V(chopstick[i]);//放回左边筷子
V(chopstick[(i + 1) % 5]);//放回右边筷子
think;
} while (1);
}
(四)吸烟者问题
假设一个系统有三个抽烟者进程和一个供应者进程。每个抽烟者不停地卷烟并抽掉它,但要卷起并抽掉一支烟,抽烟者需要有三种材料:烟草、纸和胶水。三个抽烟者中,第一个拥有烟草,第二个拥有纸,第三个拥有胶水。供应者进程无限地提供三种材料,供应者每次将两种材料放到桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者一个信号告诉已完成,此时供应者就会将另外两种材料放到桌上,如此重复(让三个抽烟者轮流地抽烟)。
int num = 0;
semaphore offer1 = 0;//烟草和纸的组合
semaphore offer2 = 0;//烟草和胶水的组合
semaphore offer3 = 0;//纸和胶水的组合
semaphore finish = 0;
process P1() {//供应者
while (1) {
num++;
num = num % 3;
if (num == 0)
V(offer1);
else if (num == 1)
V(offer2);
else
V(offer3);
任意两种材料放在桌子上;
P(finish);
}
}
process P2() {
while (1) {
P(offer3);
拿纸和胶水,卷成烟,抽掉;
V(finish);
}
}
process P3() {
while (1) {
P(offer2);
拿烟草和胶水,卷成烟,抽掉;
V(finish);
}
}
process P4() {
while (1) {
P(offer1);
拿烟草和纸,卷成烟,抽掉;
V(finish);
}
}
六.管程
管程是代表共享资源的数据结构,以及由对该共享数据结构实时操作的一组过程所组成的资源管理程序。
管程的特性保证了进程互斥,无须程序员自己实现互斥,从而降低了死锁发生的可能性。同时管程提供了条件变量,可以让程序员灵活地实现进程同步。
管程由四部分组成:
①管程的名称
②局部于管程内部的共享结构数据说明(如:缓冲区)
③对该数据结构进程操作的一组过程(或函数)
④对局部于管程内部的共享数据设置初始值的语句
管程的引入是为了解决临界区分散所带来的管理和控制问题。在没有管程之前,对临界区的访问分散在各个进程之中,不易发现和纠正分散在用户程序中不正确使用P、V操作等问题。管程将这些分散在各进程中的临界区集中起来,并加以控制和管理,管程一次只允许一个进程进入管程内,从而既便于系统管理共享资源,又能保证互斥。
1.用管程解决生产者消费者问题
(1)需要在管程中定义共享数据(如生产者消费者问题的缓冲区)
(2)需要在管程中定义用于访问这些共享数据的“入口”(如生产者消费者问题中,可以定义一个函数用于将产品放入缓冲区,再定义一个函数用于从缓冲区取出产品)
(3)只有通过这些特定的“入口”才能访问共享数据
(4)管程中有很多“入口”,但是每次只能开放其中一个“入口”,并且只能让一个进程或线程进入(如生产者消费者问题中,各进程需要互斥地访问共享缓冲区。管程的这种特性即可保证一个时间段内最多只会有一个进程在访问缓冲区。注意:这种互斥特性是由编译器负责实现的,程序员不用关心)
(5)可在管程中设置条件变量及等待/唤醒操作以解决同步问题。可以让一个进程或线程在条件变量上等待(此时,该进程应先释放管程的使用权,也就是让出“入口”);可以通过唤醒操作将等待在条件变量上的进程或线程唤醒。
2.Java中类似于管程的机制
Java中,如果用关键字synchronized来描述一个函数,那么这个函数同一时间段内只能被一个线程调用。