目录
例题
基本概念
背景
基本概念
Peterson算法
硬件同步
信号量 & PV原语:
管程
死锁和饥饿
1.In Section 5.4, we mentioned that disabling interrupts frequently can affect the system’s clock. Explain why this can occur and how such effects can be minimized.
Answer:
系统时钟的更新是由时钟中断决定的(每个时钟中断更新一次)。如果中断被禁止了(假设时间比较长),那么系统时钟就有可能损失当前时间。另外系统时钟还用于调度,比如RR算法的时间片的计算。
要减少这种情况的发生,可以将对时钟中断的禁止时间设置为尽可能的小。
2.Explain why Windows, Linux, and Solaris implement multiple locking mechanisms. (多个锁定机制)Describe the circumstances under which they use spinlocks(自旋锁), mutex locks(互斥锁), semaphores(信号量), adaptive mutex locks(自适应互斥锁), and condition variables(条件变量). In each case, explain why the mechanism is needed.
Answer:题目要求解释为何OS使用多个锁定机制并解释其适用条件。
①互斥锁适用于单处理器系统以及阻塞时间比较长的情况,一个线程占用了当前共享资源,使用互斥锁将其lock住之后,其他线程就无法访问,必须等到unlock之后,其他线程才能利用共享资源里面的内容。
②自旋锁即采用“忙等待”的方式来控制进程同步,多处理器情况忙等待就比较好,因为切换的开销是很大的。
③信号量(Samephore)由一个值和一个指针组成,指针指向等待该信号量的进程。信号量的值表示相应资源的使用情况。信号量S>=0时,S表示可用资源的数量。执行一次P操作意味着请求分配一个资源,因此S的值减1;当S<0时,表示已经没有可用资源,S的绝对值表示当前等待该资源的进程数。请求者必须等待其他进程释放该类资源,才能继续运行。而执行一个V操作意味着释放一个资源,因此S的值加1;若S<0,表示有某些进程正在等待该资源,因此要唤醒一个等待状态的进程,使之运行下去。
信号量是选择睡眠的方式来对共享工作停止访问的(即创建一个与该信号量相关的等待队列)。
信号量是用于解决进程/线程之间的同步和互斥问题的一种通信机制,是用来保证两个或多个关键代码不被并发调用。
信号量的适用条件?感觉绝大多数情况下都适用,互斥锁可以理解为二进制信号量,管程也基于信号量......
④适应互斥:adaptive mutex是Solaris提供的保护对临界数据项的访问的方法。在多处理器系统中,适应互斥以自旋锁实现的标准信号量而开始。如果数据已加锁,那么适应互斥有两个选择。如果锁是被正在另一个CPU上运行的线程所拥有,那么拥有锁的线程可能会很快结束,所以请求锁的线程就自旋并等待。如果拥有锁的线程现在不处于运行状态,那么线程就阻塞并进入睡眠,直到释放时被唤醒。(因为睡眠线程拥有的锁通常不能被很快释放,所以进行睡眠以避免自旋)
简而言之,适应互斥相当于选择自旋还是睡眠。减少了开销。
⑤condition variables用于管程模型,管程是一种抽象数据结构,其不可或缺的一个数据成员就是条件变量。以下是其特点:
---采用面向对象方法,简化线程同步
---同一时刻仅有一个线程在管程中工作
---可临时放弃管程的访问权,叫醒一个在等待队列中的线程,这是其他方法都没有的(原子锁和信号量一旦进入临界区就必须执行完临界区代码才能退出),而实现这一点采用的就是条件变量
3.Explain why spinlocks are not appropriate for single-processor systems yet are often used in multiprocessor systems.
Answer:
在单处理器系统中,使用自旋锁会造成其他进程忙等待(任何其他试图进入临界区的进程都必须在其进入代码中连续地循环),这样就浪费了CPU时钟。而对于多处理器系统,当一个进程在一个处理器自旋时,另一个进程可以在另一处理器上在其临界区内执行,且使用自旋锁,进程在等待锁时还在运行,不用进行上下文切换。
4.
Assumethat that a finite number of resources of a single resource type must be managed.Processes may ask for a number of these resources and will return them once finished. As an example, many commercial software packages provide a given number of licenses, indicating the number of applications that may run concurrently.When the application is started, the license count is decremented.When the application is terminated,the license count is incremented. If all licenses are in use, requests to start the application are denied. Such requests will only be granted when an existing license holder terminates the application and a license is returned. The following program segment is used to managea finite number of instances of an available resource. The maximum number of resources and the number of available resourcesare declared as follows:
#define MAX RESOURCES 5
int available resources = MAX RESOURCES;
When a process wishes to obtain a number of resources, it invokes the decrease count() function:
The preceding program segment produces a race condition(前面的程序段产生一个竞争条件). Do the following:
a. Identify the data involved in the race condition.
b. Identify the location (or locations) in the code where the race condition occurs.
c. Using a semaphore(信号量) or mutex lock(互斥锁),fix the race condition. It is permissible to modify the decrease count() function so that the calling process is blocked until suffient resources are available.
Answer:
a.竞争条件:如果两个进程并发地执行decrease_count函数,那么它们对共享变量avaliable_resources的操作请求便会产生竞争从而导致无法确定到底执行哪个进程或者资源不足却执行了两个进程的情况。
b.变量available_resources是产生竞争的原因
c.代码如下,因为题目给的条件不够充足,所以给出以下设定
①available_resources为全局变量(总资源数),初始为100
②以下示例代码只让2个线程在跑,其循环调用decrease_count函数,每次让资源数-1
#include
#include
#include
pthread_mutex_t mutex;
int available_resources = 100;
int decrease_count(int count)
{
pthread_mutex_lock(&mutex); //调用该函数,线程会阻塞直到互斥锁占有pthread_mutex_unlock()函数为止
if(available_resources < count)
return -1;
else
{
available_resources -= count;
printf("The current available_resources is:%d\n",available_resources);
pthread_mutex_unlock(&mutex);
usleep(100); //解锁后的线程一般要等待一会
return 0;
}
}
void *thread1(void *arg)
{
int cnt = *(int *)arg;
while(available_resources != 0)
{
decrease_count(cnt);
}
}
void *thread2(void *arg)
{
int cnt = *(int *)arg;
while(available_resources != 0)
{
decrease_count(cnt);
}
}
int main()
{
int decrease = 1;
pthread_mutex_init(&mutex,NULL); //创建互斥锁
pthread_t tid1,tid2;
pthread_create(&tid1,NULL,thread1,&decrease);
pthread_create(&tid2,NULL,thread2,&decrease);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
return 0;
}
我们可以简单看下不加锁的后果(把pthread_mutex_lock注释掉即可)
显然对资源的使用会出现混乱(如果不让线程等待100微秒的话就更乱了)
然后我们再看看加了互斥锁后的效果
可以看出是按每次-1的顺序跑下来的,没有资源的抢占情况。
5.
更多习题推荐:https://wenku.baidu.com/view/39d6ea1b580216fc700afd7c.html
如果生产者进程和消费者进程同时对共享变量操作,那么就会导致竞争条件而出错,为了避免这样的错误,同一时间只允许一个进程改变共同变量、共同表等东西。
Atomic operation(原子操作) means an operation that completes in its entirety without interruption
这个操作能不中断地完整地执行,换个思路来想,执行该操作的时候不会发生线程/进程切换(CPU实际上只会运行1个当前线程,多CPU的话需要加额外的条件这里不讨论)
Race condition: The situation where several processes access – and manipulate shared data concurrently. The final value of the shared data depends upon which process finishes last.
竞争条件:多个进程并发地访问和操作同一数据且执行结果与访问发生的特定顺序有关。
To prevent race conditions, concurrent processes must be synchronized. 为了阻止出现竞争的条件,并发进程必须同步。
同步(synchronization) 协调(coordination)
临界区问题The Critical-Section Problem
每个进程必须请求进入其临界区(critical section),实现这一请求的代码段称为进入区(entry section),临界区之后可有退出区(exit section)。其他代码段称为剩余区(remainder section)。
临界区就是一个代码段,在这部分代码段中,会改变共同变量、共同表、写一个文件等。所以我们需要有这样一个机制,用来保证一个进程进入临界区,没有其他进程被允许在临界区内执行。
临界区问题的解答必须满足以下三个要求:
有两种办法解决操作系统内的临界区问题:抢占内核与非抢占内核。
---仅供学习基于软件的解决临界区问题的办法,没有实际意义(该算法假设只有i和j两个进程)
peterson算法需要在两格进程之间共享两个数据项: int turn; boolean flag[2];
turn表示哪个进程可以进入其临界区,数组flag[i]表示哪个进程想要进入临界区。
分析上面的算法,当flag[j]为真,说明另一个进程也要进入临界区,turn==j有有两种情况,第一说明另一程序还没有执行到改变turn的这一步,但此时flag[j] = true,说明它已经执行了改变turn的上面的那句,那么下一步马上就要执行turn = i这步了,这样一来,这边的i进程马上就可以进入临界区开始执行。在临界区执行的这段时间flag[i] = true,并且刚刚turn也修改为i,所以进程j不可能进入临界区,而当这边的进程离开临界区的时候,flag[i]就会被设置为false,这样另一进程就可以进来了(保证一个进程不会循环进入临界区(一直为该进程))
进程互斥的硬件解法
用特殊指令来达到保护临界区的目的;
1. 开关中断指令:
1)简单、高效
2)代价高,限制CPU的并发能力
3)不适用于多处理器
4)适用于操作系统本身,不适用于用户程序
2. 测试并加锁指令:
3. 交换指令:
4. 忙等待:进程在得到临界区访问权限之前,持续做测试而不做其他事情;(单CPU不提倡)
自旋锁:(多处理器情况),忙等待就比较好,因为切换的开销是很大的;
指的是对信号量的两个标准原子操作。(注意理解原子操作!不要乱加mutex)
P操作:
wait(S){
while(S<=0)
; // no-op
S--;
}
V操作:
signal(S){
S++;
}
P(荷兰语proberen,测试) V(荷兰语verhogen,增加),由荷兰科学家Dijkstra提出
荷兰科学家Dijkstra
主要贡献:
提出信号量和PV原语;
解决了“哲学家聚餐”问题;
Dijkstra最短路径算法和银行家算法的创造者;
改进的PV操作
对自旋锁改进,让等待的进程在等待队列中等待
该方案首先将信号量定义为一种结构体
typedef struct{
int value;
struct process *list;
}semaphore;
每个信号量有一个整型值和一个进程链表,当一个进程必须等待信号量时,就加入到进程链表上。
然后对wait和signal操作改进
wait(semaphore *S){
S->value--;
if(S->value < 0){
add this process to S ->list;
block();
}
}
signal(semaphore *S){
S->value++;
if(S->value<=0){
remove a process P from S->list;
wakeup(P);
}
注意这里的信号量是允许为负数的(绝对值表示等待队列中的进程个数)
关于管程的描述:
管程是一种用于多线程互斥访问共享资源的程序结构
①采用面向对象方法,简化了线程间的同步控制
②任一时刻最多只有一个线程执行管程代码
③正在管程中的线程可临时放弃管程的互斥访问,等待事件出现时恢复
条件变量(Condition Variable)
条件变量是管程内的等待机制
①进入管程的线程因资源被占用而进入等待状态
②每个条件变量表示一种等待原因,对应一个等待队列
Wait()操作
①将自己阻塞在等待队列中
②唤醒一个等待者或释放管程的互斥访问
Signal()操作
①将等待队列中的一个线程唤醒
②如果等待队列为空,则等同于空操作
条件变量初值是0,信号量初值是资源个数
管程的数据结构大概是这样的
typedef struct monitor{
semaphore_t mutex; // 二值信号量,只允许一个进程进入管程,初始化为1
semaphore_t next; //配合cv,用于进程同步操作的信号量
int next_count; // 睡眠在next的等待队列上的进程数量
condvar_t *cv; // 条件变量cv
} monitor_t;
typedef struct condvar{
semaphore_t sem; //用于发出wait_cv操作的等待某个条件C为真的进程睡眠
int count; // 在这个条件变量上的睡眠进程的个数
monitor_t * owner; // 此条件变量的宿主管程
} condvar_t;
两个或多个进程无限地等待一个事件,而该事件只能由这些等待进程之一产生,处于这种状态时,这些进程就称为死锁(deadlocked)
进程在信号量内无限期等待,称这种状态为无限期阻塞(indefinite blocking)或饥饿(starvation)
推荐实验:清华大学ucore lab.........https://objectkuan.gitbooks.io/ucore-docs/content/lab7/lab7_3_4_monitors.html