实验目的
- 理解操作系统的同步互斥的设计实现;
- 理解底层支撑技术:禁用中断、定时器、等待队列;
- 在ucore中理解信号量(semaphore)机制的具体实现;
- 理解管程机制,在ucore内核中增加基于管程(monitor)的条件变量(condition variable)的支持;
- 了解经典进程同步问题,并能使用同步机制解决进程同步问题。
实验内容
实验六完成了用户进程的调度框架和具体的调度算法,可调度运行多个进程。如果多个进程需要协同操作或访问共享资源,则存在如何同步和有序竞争的问题。本次实验,主要是熟悉ucore的进程同步机制—信号量(semaphore)机制,以及基于信号量的哲学家就餐问题解决方案。然后掌握管程的概念和原理,并参考信号量机制,实现基于管程的条件变量机制和基于条件变量来解决哲学家就餐问题。
在本次实验中,在kern/sync/check_sync.c中提供了一个基于信号量的哲学家就餐问题解法。同时还需完成练习,即实现基于管程(主要是灵活运用条件变量和互斥信号量)的哲学家就餐问题解法。哲学家就餐问题描述如下:有五个哲学家,他们的生活方式是交替地进行思考和进餐。哲学家们公用一张圆桌,周围放有五把椅子,每人坐一把。在圆桌上有五个碗和五根筷子,当一个哲学家思考时,他不与其他人交谈,饥饿时便试图取用其左、右最靠近他的筷子,但他可能一根都拿不到。只有在他拿到两根筷子时,方能进餐,进餐完后,放下筷子又继续思考。
进程之间可能出现三种关系:
- 互斥(mutual exclusion):一个进程占用资源,其他进程不能使用
- 死锁(deadlock):多个进程占用部分资源,形成循环等待
- 饥饿(starvation):其他进程可能轮流占用资源,一个进程一直得不到资源
临界区的访问规则
空闲则入
忙则等待
有限等待
让权等待
实现临界区的方法
禁用中断
软件方法
更高级的抽象方法
需要修改的地方:
且在trap_dispatch函数中修改之前对时钟中断的处理,使得ucore能够利用定时器提供的功能完成调度和睡眠唤醒等操作。
struct semaphore {
int count;
queueType queue;
};
void semWait(semaphore s)
{
s.count--;
if (s.count < 0) {
/* place this process in s.queue */;
/* block this process */;
}
}
void semSignal(semaphore s)
{
s.count++;
if (s.count<= 0) {
/* remove a process P from s.queue */;
/* place process P on ready list */;
}
}
基于上诉信号量实现可以认为,当多个(>1)进程可以进行互斥或同步合作时,一个进程会由于无法满足信号量设置的某条件而在某一位置停止,直到它接收到一个特定的信号(表明条件满足了)。为了发信号,需要使用一个称作信号量的特殊变量。为通过信号量s传送信号,信号量的V操作采用进程可执行原语semSignal(s);为通过信号量s接收信号,信号量的P操作采用进程可执行原语semWait(s);如果相应的信号仍然没有发送,则进程被阻塞或睡眠,直到发送完为止。
练习0:填写已有实验
本实验依赖实验1/2/3/4/5/6。请把你做的实验1/2/3/4/5/6的代码填入本实验中代码中有“LAB1”/“LAB2”/“LAB3”/“LAB4”/“LAB5”/“LAB6”的注释相应部分。并确保编译通过。注意:为了能够正确执行lab7的测试应用程序,可能需对已完成的实验1/2/3/4/5/6的代码进行进一步改进。
比较合并代码,需要修改的地方:
trap.c里面,lab6为
case IRQ_OFFSET + IRQ_TIMER:
ticks ++;
assert(current != NULL);
sched_class_proc_tick(current);
break;
改为
case IRQ_OFFSET + IRQ_TIMER:
ticks ++;
assert(current != NULL);
run_timer_list();
break;
练习1: 理解内核级信号量的实现和基于内核级信号量的哲学家就餐问题(不需要编码)
完成练习0后,建议大家比较一下(可用meld等文件diff比较软件)个人完成的lab6和练习0完成后的刚修改的lab7之间的区别,分析了解lab7采用信号量的执行过程。执行make grade
,大部分测试用例应该通过。
请在实验报告中给出内核级信号量的设计描述,并说明其大致执行流程。
信号量的数据结构:
typedef struct {
int value;
wait_queue_t wait_queue;
} semaphore_t;
进入临界区:
void down(semaphore_t *sem); // 实现 void down(semaphore_t *sem) { uint32_t flags = __down(sem, WT_KSEM); assert(flags == 0); } // 调用__down static __noinline uint32_t __down(semaphore_t *sem, uint32_t wit_state) { bool intr_flag; local_intr_save(intr_flag); if (sem->value > 0) { sem->value --; local_intr_restore(intr_flag); return 0; } wait_t __wait, *wait = &__wait; wait_current_set(&(sem->wait_queue), wait, wait_state); local_intr_restore(intr_flag);
schedule(); local_intr_save(intr_flag); wait_current_del(&(sem->wait_queue), wait); local_intr_restore(intr_flag); if (wait->wakeup_flags != wait_state) { return wait->wakeup_flags; } return 0;
}
退出临界区:
void up(semaphore_t *sem);
// 实现
void
up(semaphore_t *sem) {
__up(sem, WT_KSEM);
}
// 调用__up
static __noinline void __up(semaphore_t *sem, uint32_t wait_state) {
bool intr_flag;
local_intr_save(intr_flag);
{
wait_t *wait;
if ((wait = wait_queue_first(&(sem->wait_queue))) == NULL) {
sem->value ++;
}
else {
assert(wait->proc->wait_state == wait_state);
wakeup_wait(&(sem->wait_queue), wait, wait_state, 1);
}
}
local_intr_restore(intr_flag);
}
请在实验报告中给出给用户态进程/线程提供信号量机制的设计方案,并比较说明给内核级提供信号量机制的异同。
给用户态进程/线程提供信号量机制时,需要给用户态进程/线程提供接口,用户态使用这些接口,通过系统调用使用内核提供的信号量机制。
相同点:
核心逻辑是一样的
不同点:
- 内核态的信号量机制可以直接调用内核的服务,用户态进程/线程提供信号量机制时,需要内核提供的接口来实现。
- 内核态的信号量存储于内核栈中;但用户态的信号量存储于用户栈中。
练习2: 完成内核级条件变量和基于内核级条件变量的哲学家就餐问题(需要编码)
首先掌握管程机制,然后基于信号量实现完成条件变量实现,然后用管程机制实现哲学家就餐问题的解决方案(基于条件变量)。
philosopher_using_semaphore中哲学家循环往复的进行如下操作:
1. 哲学家进行思考(通过do_sleep系统调用进行休眠阻塞,模拟哲学家思考)
2. 通过phi_take_forks_sema函数尝试着同时拿起左右两个叉子(如果无法拿到左右叉子,则会陷入阻塞状态)
3. 哲学家进行就餐(通过do_sleep系统调用进行休眠阻塞,模拟哲学家就餐)
4. 通过phi_put_forks_sema函数同时放下左右两个叉子
philosopher_using_condvar中哲学家循环往复的进行如下操作(整体流程和信号量的实现大体一致):
1. 哲学家进行思考(通过do_sleep系统调用进行休眠阻塞,模拟哲学家思考)
2. 通过phi_take_forks_condvar函数尝试着同时拿起左右两个叉子(如果没有拿到左右叉子,陷入阻塞)
3. 哲学家进行就餐(通过do_sleep系统调用进行休眠阻塞,模拟哲学家就餐)
4. 通过phi_put_forks_condvar函数同时放下左右两个叉子,回到思考状态
管程机制:管程在功能上和信号量及PV操作类似,属于一种进程同步互斥工具。管程封装了同步操作,对进程隐蔽了同步细节,简化了同步功能的调用界面。用户编写并发程序如同编写顺序(串行)程序。
引入管程机制的目的:1、把分散在各进程中的临界区集中起来进行管理;2、防止进程有意或无意的违法同步操作;3、便于用高级语言来书写程序,也便于程序正确性验证。
管程的定义
管程是由局部于自己的若干公共变量及其说明和所有访问这些公共变量的过程所组成的软件模块。
组成部分
1)局部于管程的共享变量;
2)对数据结构进行操作的一组过程;
3)对局部于管程的数据进行初始化的语句。
管程的属性
共享性:管程可被系统范围内的进程互斥访问,属于共享资源
安全性:管程的局部变量只能由管程的过程访问,不允许进程或其它管程直接访问,管程也不能访问非局部于它的变量。
互斥性:多个进程对管程的访问是互斥的。任一时刻,管程中只能有一个活跃进程。
封装性:管程内的数据结构是私有的,只能在管程内使用,管程内的过程也只能使用管程内的数据结构。进程通过调用管程的过程使用临界资源。管程在Java中已实现。
代码如下:
cond_wait
// Suspend calling thread on a condition variable waiting for condition Atomically unlocks // mutex and suspends calling thread on conditional variable after waking up locks mutex. Notice: mp is mutex semaphore for monitor's procedures void cond_wait (condvar_t *cvp) { //LAB7 EXERCISE1: YOUR CODE cprintf("cond_wait begin: cvp %x, cvp->count %d, cvp->owner->next_count %d\n", cvp, cvp->count, cvp->owner->next_count); /* * cv.count ++; * if(mt.next_count>0) * signal(mt.next) * else * signal(mt.mutex); * wait(cv.sem); * cv.count --; */
cvp->count++; if (cvp->owner->next_count > 0) { up(&(cvp->owner->next)); } else { up(&(cvp->owner->mutex)); } down(&(cvp->sem)); cvp->count--; cprintf("cond_wait end: cvp %x, cvp->count %d, cvp->owner->next_count %d\n", cvp, cvp->count, cvp->owner->next_count);
}
cond_signal
// Unlock one of threads waiting on the condition variable. void cond_signal (condvar_t *cvp) { //LAB7 EXERCISE1: YOUR CODE cprintf("cond_signal begin: cvp %x, cvp->count %d, cvp->owner->next_count %d\n", cvp, cvp->count, cvp->owner->next_count); /* * cond_signal(cv) { * if(cv.count>0) { * mt.next_count ++; * signal(cv.sem); * wait(mt.next); * mt.next_count--; * } * } */ if (cvp->count > 0) { cvp->owner->next_count++; up(&(cvp->sem)); down(&(cvp->owner->next)); cvp->owner->next_count--; }
cprintf("cond_signal end: cvp %x, cvp->count %d, cvp->owner->next_count %d\n", cvp, cvp->count, cvp->owner->next_count);
}
phi_take_forks_condvar
void phi_take_forks_condvar(int i) {
down(&(mtp->mutex));
//--------into routine in monitor--------------
// LAB7 EXERCISE1: YOUR CODE
// I am hungry
// try to get fork
// I am hungry
state_condvar[i]=HUNGRY;
// try to get fork
phi_test_condvar(i);
while (state_condvar[i] != EATING) {
cprintf("phi_take_forks_condvar: %d didn't get fork and will wait\n",i);
cond_wait(&mtp->cv[i]);
}
//--------leave routine in monitor--------------
if(mtp->next_count>0)
up(&(mtp->next));
else
up(&(mtp->mutex));
}
phi_put_forks_condvar
void phi_put_forks_condvar(int i) {
down(&(mtp->mutex));
//--------into routine in monitor--------------
// LAB7 EXERCISE1: YOUR CODE
// I ate over
// test left and right neighbors
// I ate over
state_condvar[i]=THINKING;
// test left and right neighbors
phi_test_condvar(LEFT);
phi_test_condvar(RIGHT);
//--------leave routine in monitor--------------
if(mtp->next_count>0)
up(&(mtp->next));
else
up(&(mtp->mutex));
}
请在实验报告中给出内核级条件变量的设计描述,并说明其大致执行流程。
条件变量condvar代码如下
typedef struct condvar{
semaphore_t sem; // the sem semaphore is used to down the waiting proc, and the signaling proc should up the waiting proc
int count; // the number of waiters on condvar
monitor_t * owner; // the owner(monitor) of this condvar
} condvar_t;
sem 是用来实现down等待中的proc,且signaling的proc需要up等待中的proc
count 等待条件变量的数量
owner是指向管程的指针
其方法主要有以下两个
// Unlock one of threads waiting on the condition variable.
void cond_signal (condvar_t *cvp);
// Suspend calling thread on a condition variable waiting for condition atomically unlock mutex in monitor,
// and suspends calling thread on conditional variable after waking up locks mutex.
void cond_wait (condvar_t *cvp);
管程的数据结构
typedef struct monitor{
semaphore_t mutex; // the mutex lock for going into the routines in monitor, should be initialized to 1
semaphore_t next; // the next semaphore is used to down the signaling proc itself, and the other OR wakeuped waiting proc should wake up the sleeped signaling proc.
int next_count; // the number of of sleeped signaling proc
condvar_t *cv; // the condvars in monitor
} monitor_t;
请在实验报告中给出给用户态进程/线程提供条件变量机制的设计方案,并比较说明给内核级提供条件变量机制的异同。
给用户态进程/线程提供条件变量机制需要提供对应的系统调用接口,对应的接口为
// Initialize variables in monitor.
void monitor_init (monitor_t *cvp, size_t num_cv);
// Unlock one of threads waiting on the condition variable.
void cond_signal (condvar_t *cvp);
// Suspend calling thread on a condition variable waiting for condition atomically unlock mutex in monitor,
// and suspends calling thread on conditional variable after waking up locks mutex.
void cond_wait (condvar_t *cvp);
请在实验报告中回答:能否不用基于信号量机制来完成条件变量?如果不能,请给出理由,如果能,请给出设计说明和具体实现。
能够基于信号量来完成条件变量机制,只需要将使用信号量来实现条件变量和管程中使用的锁和等待队列即可。
扩展练习 Challenge : 在ucore中实现简化的死锁和重入探测机制
在ucore下实现一种探测机制,能够在多进程/线程运行同步互斥问题时,动态判断当前系统是否出现了死锁产生的必要条件,是否产生了多个进程进入临界区的情况。 如果发现,让系统进入monitor状态,打印出你的探测信息。
后续完成
扩展练习 Challenge : 参考Linux的RCU机制,在ucore中实现简化的RCU机制
在ucore 下实现下Linux的RCU同步互斥机制。可阅读相关Linux内核书籍或查询网上资料,可了解RCU的设计实现细节,然后简化实现在ucore中。 要求有实验报告说明你的设计思路,并提供测试用例。下面是一些参考资料:
- http://www.ibm.com/developerworks/cn/linux/l-rcu/
- http://www.diybl.com/course/6_system/linux/Linuxjs/20081117/151814.html
后续完成
参考资料:
ucore操作系统学习(七) ucore lab7同步互斥 - 小熊餐馆 - 博客园 (cnblogs.com)
uCore实验 - Lab7 | Kiprey's Blog