第六章进程同步
协作进程:可以与在系统内执行的其他进程相互影响的进程。
互相协作的进程可以直接共享逻辑地址空间,或者只通过文件或消息来共享数据。
6.1背景
6.1.1竞争条件
1.竞争条件:多个进程并发访问和操作同一数据且执行结果和访问的特定数据有关。
2.为了避免竞争条件,需要确保一段时间内只有一个进程能操作变量counter,为了实现这种保证,要求进行一定形式的进程同步。
6.2临界区问题
1.临界区:当一个程序进入临界区,没有其他进程可被允许在临界区内执行,即没有两个进程可同时在临界区内执行。
2.进入区:每个进程必须请求允许进入其临界区,实现这一请求的代码段称为进入区
3.退出区:临界区之后
4.剩余区:其他代码
5.临界区问题解答三要素:
(1)互斥:如果P1在临界区内执行,那么其他进程都不能在其临界区内执行。
(2)前进:如果没有进程在其临界区内执行且有进程需要进入临界区,那么只有那些不再剩余区内执行的进程可以参加选择,以确定谁能下一个进入临界区,且这种选择不能无限推迟。
(3)有限等待:从一个进程做出进入临界区的请求,直到该请求允许为止,其他进程允许进入其临界区的次数有上限。
6.处理临界区问题的两种方法:抢占内核与非抢占内。
(1)抢占内核:允许处于内核模式的进程被强占。
(2)非抢占内核:不允许处于内核模式的进程被强占。处于内核模式运行的进程会一直运行,直到他退出内核模式、阻塞或自动退出CPU的控制。
显然非抢占模式的内核数据结构从根本上不会导致竞争条件,因为某个时刻只有一个进程处于内核模式。
而非抢占内核:设计更加困难,多处理器更加困难。
6.3Peterson算法
1.Peterson算法:首先需要在两个进程之间共享两个数据项:
int turn;
boolean flag[2];
turn表示那个进程可以进入其临界区如果turn==i,那么i可以进入其临界区,数组flag表示那个进程想要进入临界区如果flag[i]=true 那么表示pi想要进入临界区。
do{
flag[i]=TRUE;
turn = j;
while(flag[j]&&turn==j);
临界区
flag[i]=FALSE;
剩余区
}while(true)
对于pj的程序与上述程序中的相似,需要i,j互换。
解读:对于Pi和Pj几乎会同时走到turn = i 或turn = j,但是作为turn的值最终只会有一个,也就是i或j其中的一个,所以这样保证了原子性。
6.4硬件同步
1.一般来说,任何临界区问题都需要一个简单的工具——锁。通过要求临界区用锁来防护,就可以避免竞争条件。
即一个进程在进入临界区之前必须得到锁,退出临界区之后必须释放锁。
2.单处理器环境:临界区问题可以简单的加以解决,在修改共享变量的时候需要禁止中断的出现。
3.多处理器环境:在多处理器中由于要将消息传递给所有处理器,所以禁止中断很费时。这种消息传递导致每个临界区都会有延迟,进而降低系统效率。而且该方法影响了系统时钟(如果系统时钟是通过中断来加以更新的)
4.原子性:许多计算机提供了特殊硬件指令以允许能原子地(不可中断地)检查和修改字的内容或交换两个字的内容(作为不可中断的指令)。可以使用特殊的指令来相对简单的解决临界区问题。
5.TestAndSet()指令主要的特点是该指令可以原子的执行。
boolean TestAndSet(boolean *target){
boolean rv = *target;
*target = TRUE;
return rv;
}
通过TestAndSet()可是实现互斥
do{
while(TestAndSet(&lock))
;//do nothing
// critical section
lock = False
//remainder section
}while(true);
6.Swap()指令也保持原子性
void Swap(boolean *a,boolean *b){
boolean temp = *a;
*a = *b;
*b = temp;
}
使用Swap()实现互斥
do{
key = true;
while(key==true)
Swap(&lock,&key);
//critical section;
lock = false;
//remainder section;
}while(true);
小结:虽然上面的算法解决了互斥的问题,但是没有解决有限等待要求。公用的数据结构如下:
boolean waiting[n];
boolean lock;
do{
waiting[i] = true;
key = true;
while(waiting[i]&&key)
key = TestAndSet(&lock);
waiting[i] = false;
//critical section;
j = (i+1)%n;
while(j!=i&&!waiting[j])//找准备好的人
j=(j+1)%n;
if(j==i)
lock = false;//为下次进入做准备
else
waiting[j] = false;//进入
//remainder section;
}while(true);
6.5信号量
引言:用6.4中用到的函数(TestAndSet()与Swap()指令),对于应用程序员来讲比较复杂。为了解决这个困难,也已使用称为信号量的工具。
1.信号量S是一个整数变量,除了初始化以外,他只能通过两个标准原子操作:wait()和signal()来访问。
wait(S){
while(S<=0);
S–;
}
signal(S){
S++;
}
6.5.1用法
1.计数信号量:值域不受限制。
2.二进制信号量:值域是0或1(互斥锁)。
3.可以使用二进制信号量处理多进程的临界区问题。n个进程共享一个信号量mutex,并且初始化为1。
do{
waiting(mutex);
//critical section;
signal(mutex);
//remainder section;
}while(true)
4.计数信号量可以用来控制访问具有若干实例的某种资源。使用时waiting,释放时signal,当信号量计数为零时,所有资源被使用。
6.5.2实现
1.以上定义的信号量主要缺点就是忙等待。
2.忙等待:当一个进程位于其临界区内时,任何其他试图进入其临界区的进程都必须在其进入代码中连续的循环。
3.在单处理器中(多道程序处理),忙等待浪费CPU时钟,本来可以为其他进程所使用的。这种类型的信号量也称为自旋锁,这是因为进程在其等待锁的时候还在运行。
4.自旋锁的优缺点:进程在等待锁的时候不进行上下文的切换,而上下文的切换需要花费很长的时间,如果所占用的时间短,那么自旋锁就有用了。自旋锁常常用于多处理器系统中,这样一个线程在一个处理器自旋的时候,另一个线程可在另一处理器上在其临界区内执行。
5.为了克服忙等:可以转换思路,当信号量的值不是正数的时候那么可以不让他在循环,而是让他阻塞,然后将它放到一个和信号量相关的等待队列中,并且将该进程的状态切换成等待状态。
6.wait()方法的定义:
wait(semaphore *S){
S->value–;
if(S->value<0){
add this process to ->list
block();
}
}
可以产生负值,因为是先–再判断
负值是几,等待的进程就有几个。
7.signal()方法的定义:
signal(semaphore *S){
S->value++;
if(S->value<=0){
remove a process p from S->list
wakeup§;
}
}
8.信号量的关键之处在于原子性。在单处理器环境下,可以在执行wait()和signal()操作时简单地禁止中断。这种方案只能在单处理器环境下能工作,这是因为一旦禁止中断,不同进程指令不会交织在一起。只有当前运行进程执行,直到中断重新允许和调度器能重新获得控制为止。
9.在多个处理器环境下,必须禁止每个处理器的中断;否则,运行在不同处理器上的不同进程可能会以任意不同的方式交织在一起执行。但是,禁止每个处理器的中断不仅会很困难,而且会严重影响性能。因此,SMP系统必须提供其他的加锁计数(如自旋锁),以确保wait()与signal()原子性地执行。
10.必须承认对于这里的wait()和signal()操作的定义,并没有完全取消忙等,而是取消了应用程序进入临界区的忙等。而且,将忙等限制在操作wait()和signal()的临界区内,这些区比较短。因此临界区几乎不被占用,忙等很少发生,且所需时间很短。对于应用程序,却是一种完全不同的情况,临界区可能很长(数分钟甚至数个小时)或几乎总是被占用,这是,忙等效率极低。
6.5.3死锁和饥饿
1.死锁:两个或多个进程无限的等待一个事件,而该事件只能有这些等待进程之一来产生。
2.饥饿或无限期阻塞:即进程在信号量内无限等待。
6.6经典的同步问题
6.6.1有限缓冲问题
生产者消费者问题
共用的参数:empty = n
full = 0
mutex = 1
生产者:
do{
//produce an item in nextp;
wait(empty);
wait(mutex);
//add nextp to buffer;
signal(mutex);
signal(full);
}while(true);
消费者:
do{
wait(full);
wait(mutex);
//remove an item from buffer to nextc;
signal(mutex);
signal(empty);
//consume the item in nextc;
}while(true)
6.6.2读者写者问题
readcount=0;
semaphore mutex = 1;
semaphore wrt = 1;
写者:
do{
wait(wrt);
//writing is performed;
signal(wrt);
}while(true)
读者:
do{
wait(mutex);
readcount++;
if(readcount==1)
wait(wrt);
signal(mutex);
//reading is performed;
wait(mutex);
readcount–;
if(readcount==0)
signal(wrt);
signal(mutex);
}while(true)
6.6.3哲学家就餐问题
共享数据:
semaphore chopstick[5];
每一个chopstick初始化为1
do{
wait(chopstick[i]);
wait(chopstick[(i+1)%5]);
//eat;
signal(chopstick[i]);
signal(chopstick[(i+1)%5]);
//think;
}while(true)
如果都是这样的话就会有死锁问题的发生,因为每一个哲学家都拿着自己左手的筷子而等待自己右手边的筷子就会产生死锁的情况。
解决方案是可以让其中某一位哲学家先拿右手边的筷子,然后去等待左手边的筷子,这样的话就可以解决问题。
do{
wait(chopstick[(i+1)%5]);
wait(chopstick[i]);
//eat;
signal(chopstick[i]);
signal(chopstick[(i+1)%5]);
//think;
}while(true)
6.6.4种树问题
larry挖坑
moe种种子
curly填坑
larry不可以超过curly MAX个坑
并且只有一个铲子用来挖坑和填坑
semaphore shovel=1
semaphore Max =n;
larry:
do{
wait(Max);
wait(shovel);
//dig a hole;
signal(shovel);
signal(emptyholes);
}while(true);
Moe:
do{
wait(emptyholes);
//plant a hole;
signal(seededholes);
}while(true);
curly:
do{
wait(seededholes);
wait(shovel);
//fill a hole;
signal(shovel);
signal(Max);
}while(true);
6.6.5理发师问题
问题描述:有一个理发师,有人来就理发没人来就睡觉,人来了会叫醒理发师,有n个座位,如果满了顾客就会走,没有满顾客就会等着。
公用变量:
semaphore cusomers=0;
semaphore freechairs=n;
semaphore barbers=1;
理发师:
do{
wait(cusomers);
//cut hair;
wait(mutex);
freechairs++;
signal(mutex);
signal(barbers);
}while(true)
顾客:
wait(mutex);
if(freechairs>0){
freechairs–;
signal(mutex);
signal(cusomers);
wait(barbers);
//Have_hair_cut
}else{
signal(mutex);
//leave…;
}