现代操作系统:进程与线程(十)
2.4.4 Sleep and Weakup睡眠与唤醒
注意:Tanenbaum提出了忙碌等待(如上所述)和阻塞(进程切换)两种解决方案。我们只研究忙碌等待的解决方案,它更容易实现阻塞解决方案。Sleep和Wakeup是最简单的阻塞原语。休眠自动阻塞进程,而唤醒则解除阻塞休眠进程。然而,睡眠和唤醒是如何实现的还远远不清楚。实际上,在内心深处,他们通常使用TAS或类似的原语。我们将不讨论这些解决方案。
Homework 13
家庭作业:解释繁忙等待和阻塞进程等待之间的区别。
区别:繁忙等待需要消耗CPU资源而阻塞等待无需消耗CPU资源。所有的繁忙等待都有一个while循环以实现阻塞的效果,实际上CPU在不断计算这个while条件是否成立,而阻塞等待已经将进程挂起。
Busy Waiting的问题:如果一台计算机有两个进程,H优先级高,L优先级低。调度规则规定,H处于就绪态就可以直接运行。但是在某一时刻,L处于临界区中,H变为就绪态,准备运行(一条I/O操作结束)。此时H处于Busy Waiting状态,但是由于当H位于就绪态时L并不会被调度,因此L永远也无法离开临界区,所以H将一直处于Busy Waiting状态,这种情况称为优先级反转(Priority Inversion Problem)。
2.4.5 Semaphores信号量
术语说明:Tanenbaum只在阻塞解决方案中使用信号量这个术语。我将使用这个术语来表示我们的忙碌等待解决方案(以及阻塞解决方案,我们没有涉及到)。其他人把我们忙碌等待的解决方案称为自旋锁。
P and V P操作和V操作
入口代码通常称为P,出口代码称为V。因此,关键部分的问题是写出P和V,以便右边的循环满足左边的条件。P操作信号量-1,V操作信号量+1。
loop forever P critical-section V non-critical-section |
- 互斥;
- 没有速度的假设;
- 在NCS(非临界区代码块)中没有被进程阻塞;
- 向前推进过程;
We have just seen a solution to the critical section problem, namely: P is while (TAS(s)) {} V is s<--false |
Binary Semaphores二值信号量(0-1信号量)
二值信号量是TAS指令的一个抽象,类似于TAS中的那个bool变量,二值信号量能表达的信号就是0-close,1-open。
支持P V操作,如下:
while (S==closed) {} S←closed |
其中测试S=open和设置S←Close是单个原子操作,不可分割。
- 非正式的,进程会等待S变为Open,然后跳过轮询设置其为Close;
- 换一种说法,两个同时进行P(S)的进程不可能同时看到S=open;
- 出了临界区记得V (S)把门打开;
上面的代码不是真实的,也就是说,它不是p的真实实现。它需要一个由两条指令组成的原子序列,毕竟,这就是我们首先要实现的,相反,上面的代码是P要具有的效果的定义。
loop forever P(S) CS V(S) NCS |
重复一下:对于任意数量的进程,可以使用P和V来解决临界区问题,如图所示。
对于任意数量的进程,我们看到的唯一解决方案是2.3.4之前的那个,通过test和set实现P(S)。注意:Peterson的软件解决方案要求每个进程知道它的进程号,TAS解决方案没有。而且,P和V的定义不允许使用过程号。因此,严格地说,Peterson并没有提供P和V的实现,但是,他确实解决了临界区问题。
(The Need for) Counting (or Generalized) Semaphores通用信号量
为了解决其他的协调问题,我们希望对二元信号量进程拓展。
- 对于二元信号量,两个连续的v不允许两个后续的p成功(门不能被双开)。就是即便有两个连续的V操作,但是它的最多也就是1,所以再来一个P就变成0了。
- 我们可能希望将临界区中的进程数量限制在3或4个,而不是总是1个。
上述两个(相关的)缺点都可以通过不局限于二进制变量来克服,而是基于非负整数定义一个广义或计数信号量,就相当于把Bool改成Int型。
Intuition for (Binary and Counting) Semaphores二元信号量和计数信号量的感知
设想一个基于TAS的二元信号量S,如果当前S为False则信号量被形容成一扇打开的门,任何进程都可以通过这个门;如果S是True,则表示大门关闭。有一个助理通过检查门的状态让进程进入或者在门外等待,如果门是开着的,那么助理会让一个进程进门然后把门锁死,当这个进程从门中出来后,助理会把门打开让下一个进程进入。
而一个计数信号量的形容就是,一扇内外开关的门变成一个旋转门,只允许一定数量的进程通过,直到另一个进程允许增加通过的进程数量。
- 计数信号量S是一个非负整数值;
- 支持PV操作;
- P操作表示为:
while(S==0){} S--; |
当 时可P且S的减小是原子性的!
- 也就是说,当门只要是打开的就可以允许对应数量的进程进门;
- 原子性的另一种说法是:其实说了半天就是同时只有一个进程/线程可以对信号量变量执行读写操作,悲观锁是一种处理方式,CAS也是一种方式;
initially S=k
loop forever P(S) SCS -- semi-critical-section V(S) NCS |
Counting Semaphores and Semi-critical Sections计数信号量和半临界区
计数信号量可以解决我所说的半临界区问题,在这个区段中允许最多K个进程,答案出现在右边,当k=1时,我们有原始的临界区问题。
Solving the Producer-Consumer Problem Using Semaphores解决生产者消费者问题
回想一下,我对信号量的定义与Tanenbaum的不同(忙等待和阻塞);因此,我对各种协调问题的解决方法与他的不同也就不足为奇了。
与之前所有进程都相同的互斥问题不同,生产者-消费者问题有两类进程:
- 生产者,它产生资料并将它们插入缓冲区;
- 消费者,从缓冲区中删除项目并消费它们;
为了完成对生产者-消费者-消费者问题的定义,我们必须回答两个问题:
问:如果生产者遇到满缓冲区会发生什么?
答:生产进程阻塞,等待缓冲区变为非满的状态(有消费者消耗资料);
问:如果消费者遇到空缓冲区怎么办?
答:消费者等待缓冲区变为非空(有生产者提供资料)。
生产者-消费者问题也称为有界缓冲区问题,这个替代名称是活动实体在较低级别被数据结构替换的另一个例子(Finkel的级别原则)。
解决方案:
假设有一个面包架,每个面包占一个槽,设k为槽的数量(面包架中能容纳面包的数量)。声明e为一个计数信号量(表示空的槽的数量),f为一个计数信号量(表示已被使用的槽的数量)。虽然有K个进程可以同时进入临界区,但是它们如果需要对同一个变量进行操作仍然要对该变量加锁!
Initially e=k, f=0 (counting semaphores) b=open (binary semaphore)
Producer Consumer
loop forever loop forever produce-item P(f) P(e) P(b); take item from buf; V(b) P(b); add item to buf; V(b) V(e) V(f) consume-item |
假设初始时刻面包架是空的,那么初始化e=k,f=0,b=0;我们假设缓冲区本身只能串行访问。也就是说,一次只能做一个操作。这解释了P(b) V(b)周围的缓冲区操作。我使用,并将三条语句放在一行中,表示缓冲区的插入或删除被视为一个原子操作。当然,这种编写风格只是一种约定,原子性的实施是由P/V完成的。P(e),V(f) motif用于强制有界交替。如果k=1,它给出严格的交替。
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <pthread.h> #include <semaphore.h>
#define M 10 #define PROVIDER 3 #define CONSUMER 5
sem_t emptySem, fullSem; pthread_mutex_t bufferLock;
int breadCounter[M] = {0};
void* ProviderThread(void* params) { int providerID = *((int*)params); while (1) { sem_wait(&emptySem); pthread_mutex_lock(&bufferLock); int i = 0; for (i = 0; i < M; i++) { if (breadCounter[i] == 0) { breadCounter[i] = 1; printf("Provider No.%d put bread at No.%d\n", providerID, i); break; } } pthread_mutex_unlock(&bufferLock); sem_post(&fullSem); sleep(1); } }
void* ConsumerThread(void* params) { int consumerID = *((int*)params); while (1) { sem_wait(&fullSem); pthread_mutex_lock(&bufferLock); int i = 0; for (i = 0; i < M; i++) { if (breadCounter[i] == 1) { breadCounter[i] = 0; printf("Consumer No.%d take No.%d bread!\n", consumerID, i); break; } } pthread_mutex_unlock(&bufferLock); sem_post(&emptySem);
sleep(consumerID); }
}
int main() { sem_init(&emptySem, 0, M); sem_init(&fullSem, 0, 0); pthread_mutex_init(&bufferLock, NULL);
pthread_t pthreadList[PROVIDER + CONSUMER]; int* providerIDList = (int*)malloc(sizeof(int) * PROVIDER); int* consumerIDList = (int*)malloc(sizeof(int) * CONSUMER);
int i = 0; for (i = 0; i < PROVIDER; i++) { providerIDList[i] = i + 1; pthread_create(&pthreadList[i], NULL, ProviderThread, (void*)&providerIDList[i]); }
for (i = 0; i < CONSUMER; i++) { consumerIDList[i] = i + 1; pthread_create(&pthreadList[PROVIDER + i], NULL, ConsumerThread, (void*)&consumerIDList[i]); }
for (i = 0; i < PROVIDER + CONSUMER; i++) { pthread_join(pthreadList[i], NULL); }
free(providerIDList); free(consumerIDList);
return 0; } |
2.4.6 Mutex互斥锁
注意:我们用信号量这个术语来表示二进制信号量,并且明确地说正整数版本的广义或计数信号量,Tanenbaum用信号量表示正整数解,用互斥量表示二进制版本。同样,如上所述,Tanenbaum信号量/互斥量意味着阻塞实现;而我在忙碌等待和阻塞实现中都使用二进制/计数信号量。最后,请记住,在本课程中,我们唯一的解决方案是忙碌等待。
下面是一个罗塞塔石碑,用来翻译Tanenbaum和mine的术语。
My Terminology |
|
||
Busy wait |
block/switch |
|
|
critical |
(binary) semaphore |
(binary) semaphore |
|
semi-critical |
counting semaphore |
counting semaphore |
|
Tanenbaum's Terminology |
|||
Busy wait |
block/switch |
||
critical |
enter/leave region |
mutex |
|
semi-critical |
no name |
semaphore |
posted on 2022-01-06 15:03 ThomasZhong 阅读(83) 评论(0) 编辑 收藏 举报