操作系统面试—进程同步

本文是基于操作系统概念(第七版)第六章的学习总结,不足之处欢迎批评指正。

一、什么是临界区问题?

临界区是指在该区域中,进程可能改变共同变量、更新一个表、写一个文件等。如果多个进程进入临界区进行修改,那么将会引起混乱。

典型进程的通用结构:

do{

进入区;

   临界区;

退出区;

  剩余区;

}while(true);

临界区问题的解答必须满足下面三个要求:

1、互斥。一个进程在临界区执行,那么其他进程是不允许进入的。

2、前进。若临界区没有进程执行,那么其他想进入的进程可以进行选择。

3、有限等待。

二、处理操作系统的临界区问题:

1、抢占内核——适合实时编程,抢占内核的响应更快。

2、非抢占内核——从根本上来说不会引起竞争。

三、peterson算法(这是一种基于软件的临界区问题的解答)

flag——表示哪个进程想要进入临界区。

turn——表示哪个进程可以进入临界区。

下面是进程pi的结构:

do{

flag[i]=true;

turn=j;

while(flag[i]&&turn==j);

临界区;

flag[i]=false;

剩余区; 

}while(true);

说明:当i想进入临界区但是不允许进入临界区,那么程序就一直进行while循环,进入不了临界区。如果此时turn==i,即i可以进入临界区了,那么进程i进入临界区,但是此时进程j是进入不了临界区的。执行完毕后,重新设置进程i,含义为此时i不想进入临界区了。peterson算法的关键在于借助了turn这个变量,大家慢慢体会。

四、硬件同步

采用锁的临界区问题的解答

do{

请求锁;

 临界区;

释放锁;

 剩余区;

}

这里我们将用简单硬件指令来解决临界区问题。

单处理系统——在修改共享变量是禁止中断出现即可。

多处理系统——上面的方法不可行,因为要将消息传递给多处理器,费时。

幸运的是,现代计算机系统大多数提供了特殊硬件指令以原子地检查和修改字的内容或者交换两个字的内容。

什么是原子地?

原子操作即不可中断。

第一个指令testandset()——指令的作用是返回lock的bool值,并设置其为true。因此如果原来为true,则只是简单返回,否则要设置从false->true。

bool testandset(bool *lock){

  bool temp=*lock;

  *lock=true;

  return temp;

}

那么如何利用testandset()来实现互斥呢?

初始化一个全局变量lock,初始化为false;需要说明的lock为false,代表没有上锁,因此资源可用。

为什么需要将lock设置为true呢,因此如果进程进入了,那么需要上锁,从而使得其他进程进入不了临界区。

do{

while(testandset(&lock));

临界区;

lock=false;

剩余区;

}while(true);

第二个指令——swap()

void swap(bool* a, bool *b){

   bool temp;

   temp=*a;

   *a=*b;

    *b=temp;

}

那么如何利用swap()来实现互斥呢?

声明一个全局变量lock,初始化为false,并且每个进程中有一个局部bool变量key。

do{

key=true;

while(key==true) swap(&lock,&key);

临界区;

lock=false;

}while(true);

首先lock为false,资源可用,执行swap指令之后,lock=true,资源上锁,key=false;这里我觉得若key一直为false,那么进程会一直执行临界区,所以在剩余去肯定会对key进行重新设置。

但是上面两个操作虽然解决互斥问题,但并未解决有限等待问题,可能一个进程会一直运行许多次,而另一个等待进程一直在等待。因此可对testandset指令进行改进,

首先引入两个全局变量:

bool waiting[n];

bool lock;

waiting[i]=false变量代表级进程无需等待,可以进入临界区,waiting[i]=true,则表示需要等待。每一次只有一个waiting被设置为false,以满足互斥要求。key的作用是实现无限循环。

只有第一个进程进入时才会发现key==false(testandset的作用),之后lock被设置为true,因此其他进程都必须等待。当进程进入临界区执行完之后,会查找其他是否有其他进程想进入临界区,若没有,则直接释放锁,否则将找到的进程waiting设置为false,这样这个进程就可以进入临界区了。所以任何等待临界区的进程最多只需要等待n-1次,因为是循环等待。

do{

waiting[i]=true;

key=true;

while(waiting[i]&&key)

    key=testandset(&lock);

waiting[i]=true;

临界区;

j=(i+1)%n;

while(j!=i&&!waiting[j]) j=(j+1)%n;

if(j==i)

lock=false;

esle

waiting[j]=false;

剩余区;

}while(true);

五、信号量

信号量的引入原因:由于基于硬件的临界区问题的解决方案对于程序员而言,使用比较复杂。

信号量s是个整数变量,除了初始化外,只能通过两个原子操作来访问,wait()和signal(),即P,V操作。

wait操作的定义:

s可以理解为现可用资源的数量,s<=0代表没有资源可以使用。wait()——获取资源,signal()——释放资源。

wait(s){

while(s<=0);

s--;
}

signal(s){

s++;
}

通常os区分计算信号量和二进制信号量,计数信号量不受值域限制,二进制信号量的值只能为0或者1。

二进制信号量又称为互斥锁。下面程序就可以使用二进制信号量实现n个进程的临界区问题。

do{

wait(mutex);

临界区;

signal(mutex);

剩余区;
}while(true);

六、信号量的实现

上述信号量的主要缺点:忙等待,即任何想进入临界区的进程必须在进入代码中进行连续循环。这种类型的信号量也成为自旋锁,这是因为进程在等待锁是还在运行。

为了克服忙等待,那么如何做呢?

进程不是忙等待,而是阻塞自己,阻塞操作将一个进程放入信号量相关的等待队列中,并将该进程的状态切换成等待状态,这样就是忙等待了。

修改原来原来wait和signal的定义:

typedef struct{
int value;

struct process* list;

}semaphore;

每个信号量都有一个整型值和一个进程链表,signal会从等待进程链表中取一个唤醒。

wait(semaphore* s){

s->value--;

if(s->value<0){

将该进程插入等待进程链表中,

block();——挂起进程。
}
}

signal(semaphore* s){

s->value++;

if(s->value<=0){

从等待进程链表中移除进程p;

wakeup(p);——唤起进程p到就绪队列中。

}
}

死锁和饥饿现象:

死锁:两个或多个进程无限地等待一个事件,而这个事件又是由这些等待进程中的一个产生,是一种互相等待的现象。具体死锁问题和算法将在下一篇博文中介绍。








你可能感兴趣的:(数据结构与算法)