实验七:同步互斥
练习0:填写已有实验
使用meld可以简单地将前几个lab的代码填入lab7中,但是要注意在这次实验中,部分代码需要做出修改,如下,主要是trap_dispatch这一个函数
kern/trap/trap.c中lab6的部分代码
...
ticks++;
assert(current != NULL);
run_timer_list(); //lab6中的处理方式是临时的,lab7开始启动计时器机制,具体实现在练习1中解释
break;
...
练习1:理解内核级信号量的实现和基于内核级信号量的哲学家就餐问题
1、同步互斥机制的底层实现
计时器
计时器通过定义在kern/schedule/sched.[ch]中的函数完成,计时器提供了基于时间事件的调节机制,在ucore中利用计时器可以实现基于时间长度的睡眠等待和唤醒机制,每当时钟中断发生时,ucore就可以产生相应的时间事件。计时器相关的数据结构和基本操作如下:
sched.h中定义了timer_t的基本数据结构
typedef struct {
unsigned int expires; //the expire time 计时长度
struct proc_struct *proc; //the proc wait in this timer. If the expire time is end, then this proc will be scheduled 该计时器对应的进程
list_entry_t timer_link; //the timer list 计时器链表
} timer_t;
timer_init对计时器初始化
static inline timer_t *
timer_init(timer_t *timer, struct proc_struct *proc, int expires) {
timer->expires = expires; //初始化计时长度
timer->proc = proc; //初始化计时器绑定的进程
list_init(&(timer->timer_link)); //初始化计时器链表
return timer;
}
add_timer向系统添加已初始化的新计时器
void
add_timer(timer_t *timer) {
bool intr_flag;
local_intr_save(intr_flag);
{
assert(timer->expires > 0 && timer->proc != NULL);
assert(list_empty(&(timer->timer_link)));
list_entry_t *le = list_next(&timer_list);
while (le != &timer_list) { //while循环的作用在于将一个计时器放入到合适的位置,结合del_timer可以看出,每个计时器实际计时的值为计时队列在这个计时器之前的计时值之和
timer_t *next = le2timer(le, timer_link);
if (timer->expires < next->expires) { //例如新计时器值为5,已有的计时队列为2->3->6->7
next->expires -= timer->expires; //则5-2=3,3-3=0,最终新队列为2->3->0->6->7,显然当第1、2个计时器都走完时第3个计时器走0步就走完,符合初始值2+3=5
break; //因此调用run_timer_list时每次只需要减少第一个计时器的值
}
timer->expires -= next->expires;
le = list_next(le);
}
list_add_before(le, &(timer->timer_link));
}
local_intr_restore(intr_flag);
}
del_timer取消一个计时器
void
del_timer(timer_t *timer) {
bool intr_flag;
local_intr_save(intr_flag);
{
if (!list_empty(&(timer->timer_link))) {
if (timer->expires != 0) {
list_entry_t *le = list_next(&(timer->timer_link));
if (le != &timer_list) {
timer_t *next = le2timer(le, timer_link);
next->expires += timer->expires; //结合add_timer的机制可以看出,取消后只需要在下一个计时器上加上取消的计时器的当前计时值就可以保证后续每一个计时器的实际计时值都与设定值一致
}
}
list_del_init(&(timer->timer_link));
}
}
local_intr_restore(intr_flag);
}
run_timer_list更新系统计时并唤醒计时器归零可以被激活的进程
void
run_timer_list(void) {
bool intr_flag;
local_intr_save(intr_flag);
{
list_entry_t *le = list_next(&timer_list);
if (le != &timer_list) {
timer_t *timer = le2timer(le, timer_link);
assert(timer->expires != 0);
timer->expires --; //只需要在第一个计时器上减1即可,由于进程加入计时的频率应远远小于时钟中断的频率,这样设计计时队列计时值的更新,可以减小开销避免每次时钟中断都要遍历整个计时器队列
while (timer->expires == 0) { //当归零时执行唤醒,由于可能存在后续也为0例如add_timer中注释举得例子,用while循环将所有归零的计时器对应的进程激活
le = list_next(le);
struct proc_struct *proc = timer->proc;
if (proc->wait_state != 0) {
assert(proc->wait_state & WT_INTERRUPTED);
}
else {
warn("process %d's wait_state == 0.\n", proc->pid);
}
wakeup_proc(proc);
del_timer(timer);
if (le == &timer_list) {
break;
}
timer = le2timer(le, timer_link);
}
}
sched_class_proc_tick(current); //执行调度算法
}
local_intr_restore(intr_flag);
}
屏蔽与使能中断
中断的屏蔽与使能通过定义在kern/sync/sync.h中的函数完成,源码较为简单,基本调用关系如下:
关中断:local_intr_save -> __intr_save -> intr_disable -> cli
开中断:local_intr_restore -> __intr_restore -> intr_enable -> sti
需要用到中断相关的操作时按如下格式即可:
...
bool intr_flag;
local_intr_save(intr_flag);
{
critical code...
}
local_intr_restore(intr_flag);
...
等待队列
等待队列通过定义在kern/sync/wait.[ch]中的数据结构和函数完成
wait.h中定义了等待队列的基本数据结构
typedef struct {
list_entry_t wait_head;
} wait_queue_t; //wait_queue的头节点
typedef struct {
struct proc_struct *proc; //与该wait节点绑定的进程指针
uint32_t wakeup_flags; //等待原因标志
wait_queue_t *wait_queue; //指向此wait节点所属的wait_queue头节点
list_entry_t wait_link; //等待队列链表,组织wait节点的链接
} wait_t; //wait节点
wait.c中定义了等待队列的基本操作,与链表类似,这里给出接口不分析源码
#define le2wait(le, member) //通过链表节点获得wait节点
void wait_init(wait_t *wait, struct proc_struct *proc); //初始化wait结构
void wait_queue_init(wait_queue_t *queue); //初始化wait_queue结构
void wait_queue_add(wait_queue_t *queue, wait_t *wait); //把wait前插到wait_queue中
void wait_queue_del(wait_queue_t *queue, wait_t *wait); //从wait_queue中删除wait
wait_t *wait_queue_next(wait_queue_t *queue, wait_t *wait); //取wait的后一个wait
wait_t *wait_queue_prev(wait_queue_t *queue, wait_t *wait); //取wait的前一个wait
wait_t *wait_queue_first(wait_queue_t *queue); //取wait_queue的第一个wait
wait_t *wait_queue_last(wait_queue_t *queue); //取wait_queue的最后一个wait
bool wait_queue_empty(wait_queue_t *queue); //判断wait_queue是否为空
bool wait_in_queue(wait_t *wait); //判断wait是否在wait_queue中
//以下高层函数基于了上述底层函数实现了相关操作
//唤醒与wait关联的进程
void wakeup_wait(wait_queue_t *queue, wait_t *wait, uint32_t wakeup_flags, bool del);
//唤醒wait_queue上的第一个wait所关联的进程
void wakeup_first(wait_queue_t *queue, uint32_t wakeup_flags, bool del);
//唤醒wait_queue上的所有wait所关联的进程
void wakeup_queue(wait_queue_t *queue, uint32_t wakeup_flags, bool del);
//将wait于当前进程关联,并让当前进程所关联的wait进入wait_queue,即睡眠当前进程
void wait_current_set(wait_queue_t *queue, wait_t *wait, uint32_t wait_state);
//将当前进程关联的wait从wait_queue删除
#define wait_current_del(queue, wait)
2、信号量
有了同步互斥机制的底层支撑,可以实现信号量。信号量的原理性描述参考操作系统相关的书籍,如下:
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 */;
}
}
在ucore中,信号量通过定义在kern/sync/sem.[ch]中的数据结构和函数实现
sem.h中定义了信号量的基本数据结构
typedef struct {
int value; //信号量的当前值
wait_queue_t wait_queue; //该信号对应的等待队列
} semaphore_t;
__down实现信号量的P操作
static __noinline uint32_t __down(semaphore_t *sem, uint32_t wait_state) {
bool intr_flag;
local_intr_save(intr_flag); //关中断
if (sem->value > 0) { //若信号量的值大于0,可以获得信号量,则减1并开中断后立刻返回
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从等待队列中删除
wait_current_del(&(sem->wait_queue), wait);
local_intr_restore(intr_flag);
if (wait->wakeup_flags != wait_state) { //若唤醒原因与睡眠原因不同,则返回异常标志,否则返回0
return wait->wakeup_flags;
}
return 0;
}
__up实现信号量的V操作
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 ++; //若没有进程在等待,则信号量加1
}
else { //有进程在等待,则唤醒
assert(wait->proc->wait_state == wait_state);
wakeup_wait(&(sem->wait_queue), wait, wait_state, 1);
}
}
local_intr_restore(intr_flag); //开中断并返回
}
3、哲学家就餐问题(信号量)
5个哲学家围绕一张圆桌而坐,桌子上每两个哲学家之间放置1支叉子,共5支;
哲学家的动作包括思考和进餐,进餐时需要同时得到左右两支叉子,思考时放回叉子;
如何保证哲学家动作有序进行?例如不出现有人永远拿不到叉子等异常
哲学家就餐问题(体现在kern/sync/check_sync.c中)利用信号量来解决,给每个哲学家一个信号量s[i],同时记录每个哲学家的状态state_sema[i]为三种THINKING、HUNGRY、EATING,由于叉子是共享资源,因此在一个哲学家拿起/放下叉子时需要临界区互斥,用信号量mutex来实现。实现如下
//---------- philosophers problem using semaphore ----------------------
int state_sema[N]; //记录每个哲学家的状态
semaphore_t mutex; //临界区互斥
semaphore_t s[N]; //每个哲学家一个信号量
struct proc_struct *philosopher_proc_sema[N]; //每个哲学家对应1个进程
void phi_test_sema(i) //测试哲学家i是否能进行EATING
{
if(state_sema[i]==HUNGRY&&state_sema[LEFT]!=EATING
&&state_sema[RIGHT]!=EATING) //哲学家i的状态是HUNGRY且左右都没有人在吃,则可以进入EATING
{
state_sema[i]=EATING;
up(&s[i]); //由于i已开始EATING,执行V操作
}
}
void phi_take_forks_sema(int i) //获得两支叉子
{
down(&mutex); //进入临界区
state_sema[i]=HUNGRY; //设置哲学家i进入HUNGRY
phi_test_sema(i); //测试能否获得叉子并进行EATING
up(&mutex); //离开临界区
down(&s[i]); //注意测试中如果得到了叉子进入EATING则执行了V操作,此时执行P操作配对;若没有得到叉子而执行P操作就进入堵塞,哲学家持续等待HUNGRY状态
}
void phi_put_forks_sema(int i) //放回两支叉子
{
down(&mutex); //进入临界区
state_sema[i]=THINKING; //设置哲学家i进入THINKING
phi_test_sema(LEFT); //测试一下左邻居现在是否能进餐
phi_test_sema(RIGHT); //测试一下右邻居现在是否能进餐
up(&mutex); //离开临界区
}
int philosopher_using_semaphore(void * arg) //哲学家问题基于信号量的完整实现过程
{
int i, iter=0;
i=(int)arg;
cprintf("I am No.%d philosopher_sema\n",i);
while(iter++<TIMES) //测试次数TIMES
{
cprintf("Iter %d, No.%d philosopher_sema is thinking\n",iter,i);
do_sleep(SLEEP_TIME);
phi_take_forks_sema(i); //需要两只叉子,得不到就堵塞
cprintf("Iter %d, No.%d philosopher_sema is eating\n",iter,i);
do_sleep(SLEEP_TIME);
phi_put_forks_sema(i); //放回两只叉子
}
cprintf("No.%d philosopher_sema quit\n",i);
return 0;
}
ucore启动运行后在init_main中通过调用check_sync函数来进行模拟测试过程,代码及解释如下:
...
//check semaphore
sem_init(&mutex, 1); //临界区信号量初始化为1
for(i=0;i<N;i++){ //循环调用kernel_thread创建N=4个哲学家进程
sem_init(&s[i], 0);
int pid = kernel_thread(philosopher_using_semaphore, (void *)i, 0);
if (pid <= 0) {
panic("create No.%d philosopher_using_semaphore failed.\n");
}
philosopher_proc_sema[i] = find_proc(pid);
set_proc_name(philosopher_proc_sema[i], "philosopher_sema_proc");
}
...
练习2:完成内核级条件变量和基于内核级条件变量的哲学家就餐问题
1、管程机制
管程将对共享资源的访问及所需要的同步操作集中并封装起来,由四部分组成:
- 管程内部的共享变量
- 管程内部的条件变量
- 管程内部并发执行的过程
- 局部于管程内部的共享数据设置初始化的语句
局限在管程中的数据结构,只能被局限在管程的操作过程所访问,任何管程之外的操作过程都不能访问它;另一方面,局限在管程中的操作过程也主要访问管程内的数据结构。管程相当于一个隔离区,它把共享变量和对它进行操作的若干个过程围了起来,所有进程要访问临界资源时,都必须经过管程才能进入,而管程每次只允许一个进程进入管程,从而需要确保进程之间互斥。
条件变量(Condition Variables, CV)可以代表一个进程的等待队列,队列中的进程都在等待某一个条件,涉及到的核心操作如下:
Wait操作将自身阻塞在等待队列中,并唤醒一个等待者或释放管程的互斥访问Signal操作将等待队列中的一个线程唤醒,如果等待队列为空则等同于空操作
在实际实现管程时,当前进程正在管程中操作,此时等待的某个条件为真时:若当前进程若继续执行到结束,再检查条件唤醒等待队列的进程,称为Hansen管程;若当前进程立刻放弃管程、进入等待并唤醒这个条件对应的等待队列的进程,待结束后再重新继续执行自身进程,称为Hoare管程
ucore中基于信号量实现了Hoare管程解决哲学家就餐问题。
2、基于信号量的管程实现
管程通过定义在kern/sync/monitor.[ch]中的数据结构和函数实现,如下:
monitor.h定义了管程的基本数据结构
typedef struct condvar{
semaphore_t sem; //用于发出cond_wait操作而使自身等待条件变量并进入睡眠的信号量
int count; //条件变量下等待队列中的进程数
monitor_t * owner; //此条件变量属于哪个管程
} condvar_t;
typedef struct monitor{
semaphore_t mutex; //只允许一个进程进入管程的信号量,初始化为1
semaphore_t next; //用来完成同步操作
int next_count; //记录了由于发出cond_signal操作而睡眠的进程数
condvar_t *cv; //管程中的条件变量
} monitor_t;
monitor_init实现初始化管程的操作
// Initialize monitor.
void
monitor_init (monitor_t * mtp, size_t num_cv) {
int i;
assert(num_cv>0);
mtp->next_count = 0; //发出cond_signal操作而睡眠的进程数初始为0
mtp->cv = NULL;
sem_init(&(mtp->mutex), 1); //初始化管程互斥锁为1
sem_init(&(mtp->next), 0); //初始化同步操作的信号量为0
mtp->cv =(condvar_t *) kmalloc(sizeof(condvar_t)*num_cv);
assert(mtp->cv!=NULL);
for(i=0; i<num_cv; i++){ //循环初始化所需要的多个条件变量
mtp->cv[i].count=0; //条件变量的等待队列进程数初始为0
sem_init(&(mtp->cv[i].sem),0); //条件变量的信号量初始为0
mtp->cv[i].owner=mtp; //条件变量均属于正在初始化的管程
}
}
cond_wait实现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 ++; //该条件变量下等待队列的进程数+1
if(cvp->owner->next_count > 0){
up(&(cvp->owner->next)); //若该条件变量所属的管程有进程在monitor.next信号量上进入睡眠,则唤醒之
}
else{
up(&(cvp->owner->mutex)); //若没有,则释放管程的互斥锁唤醒无法进入管程的进程
}
down(&(cvp->sem)); //将自身进入睡眠并挂在条件变量的等待队列中
cvp->count --; //当被唤醒后从这里开始执行,条件变量的等待队列进程数减1
cprintf("cond_wait end: cvp %x, cvp->count %d, cvp->owner->next_count %d\n", cvp, cvp->count, cvp->owner->next_count);
}
cond_signal实现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 ++; //发出cond_signal操作而睡眠的进程数加1
up(&(cvp->sem)); //唤醒该条件变量下等待队列的进程
down(&(cvp->owner->next)); //将自身进入睡眠,挂在next信号量上
cvp->owner->next_count --; //当被唤醒后,将挂在next信号量上的睡眠进程数减1
}
cprintf("cond_signal end: cvp %x, cvp->count %d, cvp->owner->next_count %d\n", cvp, cvp->count, cvp->owner->next_count);
}
注意,为了使管程正常运行,管程中的每个函数出入口都需要相应的操作,如下:
function_in_monitor(...)
{
down(&(monitor.mutex)); //进程进入管程,获取管程的互斥锁
//-----------------------------
the real body of function;
//-----------------------------
if(monitor.next_count > 0) //若管程中还有睡眠的进程,唤醒,否则释放管程互斥锁
up(&(monitor.next));
else
up(&(monitor.mutex));
}
3、哲学家就餐问题(管程)
哲学家就餐问题(体现在kern/sync/check_sync.c中)利用管程来解决,给每个哲学家一个条件变量mtp->cv[i],同时记录每个哲学家的状态state_condvar[i]为三种THINKING、HUNGRY、EATING,由于叉子是共享资源,用管程来实现。实现如下
//-----------------philosopher problem using monitor ------------
struct proc_struct *philosopher_proc_condvar[N];
int state_condvar[N];
monitor_t mt, *mtp=&mt;
void phi_test_condvar (int i) {
if(state_condvar[i]==HUNGRY&&state_condvar[LEFT]!=EATING
&&state_condvar[RIGHT]!=EATING) {
cprintf("phi_test_condvar: state_condvar[%d] will eating\n",i);
state_condvar[i] = EATING ;
cprintf("phi_test_condvar: signal self_cv[%d] \n",i);
cond_signal(&mtp->cv[i]) ; //可以进入EATING状态,唤醒在条件变量i下等待的哲学家
}
}
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
state_condvar[i] = HUNGRY; //设置哲学家i进入HUNGRY状态
phi_test_condvar(i); //测试能否进入EATING
while(state_condvar[i] != EATING){
cprintf("phi_take_forks_condvar: %d didn't get fork and still wait\n", i);
cond_wait(&mtp->cv[i]); //若没有进入EATING则将自身睡眠
}
//--------leave routine in monitor--------------
if(mtp->next_count>0) //执行到当前进程最后若管程还有在等待的进程则唤醒,否则释放管程互斥锁
up(&(mtp->next));
else
up(&(mtp->mutex));
}
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
state_condvar[i] = THINKING; //设置哲学家i进入THINKING状态
phi_test_condvar(LEFT); //测试左邻居能否EATING
phi_test_condvar(RIGHT); //测试右邻居能否EATING
//--------leave routine in monitor--------------
if(mtp->next_count>0) //执行到当前进程最后若管程还有在等待的进程则唤醒,否则释放管程互斥锁
up(&(mtp->next));
else
up(&(mtp->mutex));
}
//---------- philosophers using monitor (condition variable) ----------------------
int philosopher_using_condvar(void * arg) {
int i, iter=0;
i=(int)arg;
cprintf("I am No.%d philosopher_condvar\n",i);
while(iter++<TIMES)
{
cprintf("Iter %d, No.%d philosopher_condvar is thinking\n",iter,i);
do_sleep(SLEEP_TIME);
phi_take_forks_condvar(i);
cprintf("Iter %d, No.%d philosopher_condvar is eating\n",iter,i);
do_sleep(SLEEP_TIME);
phi_put_forks_condvar(i);
}
cprintf("No.%d philosopher_condvar quit\n",i);
return 0;
}
ucore启动运行后在init_main中通过调用check_sync函数来进行模拟测试过程,代码及解释如下:
//check condition variable
monitor_init(&mt, N);
for(i=0;i<N;i++){
state_condvar[i]=THINKING;
int pid = kernel_thread(philosopher_using_condvar, (void *)i, 0);
if (pid <= 0) {
panic("create No.%d philosopher_using_condvar failed.\n");
}
philosopher_proc_condvar[i] = find_proc(pid);
set_proc_name(philosopher_proc_condvar[i], "philosopher_condvar_proc");
}
总结
完成全部代码后,调用make grade可以获得如下输出,说明实验成功
badsegment: (4.7s)
-check result: OK
-check output: OK
divzero: (2.8s)
-check result: OK
-check output: OK
softint: (2.8s)
-check result: OK
-check output: OK
faultread: (1.7s)
-check result: OK
-check output: OK
faultreadkernel: (1.6s)
-check result: OK
-check output: OK
hello: (3.3s)
-check result: OK
-check output: OK
testbss: (1.7s)
-check result: OK
-check output: OK
pgdir: (3.3s)
-check result: OK
-check output: OK
yield: (2.8s)
-check result: OK
-check output: OK
badarg: (3.3s)
-check result: OK
-check output: OK
exit: (2.8s)
-check result: OK
-check output: OK
spin: (2.8s)
-check result: OK
-check output: OK
waitkill: (4.0s)
-check result: OK
-check output: OK
forktest: (2.9s)
-check result: OK
-check output: OK
forktree: (2.9s)
-check result: OK
-check output: OK
priority: (15.7s)
-check result: OK
-check output: OK
sleep: (11.5s)
-check result: OK
-check output: OK
sleepkill: (2.9s)
-check result: OK
-check output: OK
matrix: (10.9s)
-check result: OK
-check output: OK
Total Score: 190/190
本次实验报告详细介绍了同步互斥的概念,包括内核级信号量的实现,如计时器、中断处理和等待队列。通过信号量解决了哲学家就餐问题。此外,还探讨了内核级条件变量的管程机制,实现了一个基于管程的哲学家就餐问题解决方案。实验通过模拟测试验证了代码的正确性。
2140

被折叠的 条评论
为什么被折叠?



