阅读前注意事项:
1、我的博客从lab2之后,如果没有特殊说明,所有标注的代码行数位置,以labcodes_answer(答案包)里的文件为准!!!因为你以后会发现做实验用meld软件比较费时费力,对于咱们学校的验收不如直接对着答案来;
2、感谢网上的各路前辈大佬们,本人在这学期初次完成实验的过程中,各位前辈们的博客给了我很多有用的指导;本人的博客内容在现有的内容上,做了不少细节的增补内容,有些地方属个人理解,如果有错在所难免,还请各位大佬们批评指正;
3、所有实验的思考题,我把它规整到了文章最后;
4、所有实验均默认不做challenge,对实验评分无影响;
5、湖南大学的实验顺序为1 4 5 6 7 2 3 8,在实验4-7过程中涉及到实验二三的页表虚存问题,当做黑盒处理,没有过多探索。
一、实验内容
在本次实验中, 在kern/sync/check_sync.c中提供了一个基于信号量的哲学家就餐问题解法。同时还需完成练习,即实现基于管程(主要是灵活运用条件变量和互斥信号量)的哲学家就餐问题解法。哲学家就餐问题描述如下:有五个哲学家,他们的生活方式是交替地进行思考和进餐。哲学家们公用一张圆桌,周围放有五把椅子,每人坐一把。在圆桌上有五个碗和五根筷子,当一个哲学家思考时,他不与其他人交谈,饥饿时便试图取用其左、右最靠近他的筷子,但他可能一根都拿不到。只有在他拿到两根筷子时,方能进餐,进餐完后,放下筷子又继续思考。
二、目的
熟悉ucore中的进程同步机制,了解操作系统为进程同步提供的底层支持;
在ucore中理解信号量(semaphore)机制的具体实现;
理解管程机制,在ucore内核中增加基于管程(monitor)的条件变量(condition variable)的支持;了解经典进程同步问题,并能使用同步机制解决进程同步问题。
三、实验设计思想和流程
练习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的代码进行进一步改进。
使用meld软件进行比对,发现需要更改的文件为:
proc.c
default_pmm.c
pmm.c
swap_fifo.c
vmm.c
trap.c
sched.c
进一步比对发现,无需改进代码实现,直接使用即可。
练习1:理解内核级信号量的实现和基于内核级信号量的哲学家就餐问题(不需要编码)
哲学家就餐问题描述:哲学家就餐问题,即有五个哲学家,他们的生活方式是交替地进行思考和进餐。哲学家们公用一张圆桌,周围放有五把椅子,每人坐一把。在圆桌上有五个碗和五根筷子,当一个哲学家思考时,他不与其他人交谈,饥饿时便试图取用其左、右最靠近他的筷子,但他可能一根都拿不到。只有在他拿到两根筷子时,方能进餐,进餐完后,放下筷子又继续思考。
以下是本次实验中,使用信号量和管程所可能涉及到的底层框架逻辑结构:
既然要理解信号量的实现方法,首先来看课本(《操作系统概念》中文第9版)上6.6节的伪代码中,描述的信号量的实现方法:
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);
}
}
基于信号量实现可以这样描述:当多个进程可以进行互斥或同步合作时,一个进程会由于无法满足信号量设置的某条件而在某一位置停止,直到它接收到一个特定的信号(表明条件满足了)。为了发信号,需要使用一个称作信号量的特殊变量。为通过信号量s传送信号,信号量通过wait和signal操作来修改传送信号量。
value> 0,表示共享资源的空闲数
vlaue< 0,表示该信号量的等待队列里的进程数
value= 0,表示等待队列为空
实验7的主要任务是实现基于信号量和管程去解决哲学家就餐问题,我们知道,解决哲学家就餐问题需要创建与之相对应的内核线程,而所有内核线程的创建都离不开pid为1的那个内核线程——idle,此时我们需要去寻找在实验四中讨论过的地方,如何创建并初始化idle这个内核线程。
在实验七中,具体的信号量数据结构被定义在(kern/sync/sem.h)中:
typedef struct {
int value;
wait_queue_t wait_queue;
} semaphore_t;
找到相关函数init_main(kern/process/proc.c,838——863行)
static int
init_main(void *arg) {
size_t nr_free_pages_store = nr_free_pages();
size_t kernel_allocated_store = kallocated();
int pid = kernel_thread(user_main, NULL, 0);
if (pid <= 0) {
panic("create user_main failed.\n");
}
extern void check_sync(void);
check_sync(); // check philosopher sync problem
while (do_wait(0, NULL) == 0) {
schedule();
}
cprintf("all user-mode processes have quit.\n");
assert(initproc->cptr == NULL && initproc->yptr == NULL && initproc->optr == NULL);
assert(nr_process == 2);
assert(list_next(&proc_list) == &(initproc->list_link));
assert(list_prev(&proc_list) == &(initproc->list_link));
assert(nr_free_pages_store == nr_free_pages());
assert(kernel_allocated_store == kallocated());
cprintf("init check memory pass.\n");
return 0;
}
该函数与实验四基本没有不同之处,唯一的不同在于它调用了check_sync()这个函数去执行了哲学家就餐问题:
分析check_sync函数(kern/sync/check_sync.c,182+行):
void check_sync(void)
{
int i;
//check semaphore
sem_init(&mutex, 1);
for(i=0;i<N;i++) { //N是哲学家的数量
sem_init(&s[i], 0); //初始化信号量
int pid = kernel_thread(philosopher_using_semaphore, (void *)i, 0);//线程需要执行的函数名、哲学家编号、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");
}
//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");
}
}
通过观察函数的注释,我们发现,这个check_sync函数被分为了两个部分,第一部分使用了信号量来解决哲学家就餐问题,第二部分则是使用管程的方法。因此,练习一中我们只需要关注前半段。
首先观察到利用kernel_thread函数创建了一个哲学家就餐问题的内核线程(kern/process/proc.c,270——280行)
int
kernel_thread(int (*fn)(void *), void *arg, uint32_t clone_flags) {
struct trapframe tf; //中断相关
memset(&tf, 0, sizeof(struct trapframe));
tf.tf_cs = KERNEL_CS;
tf.tf_ds = tf.tf_es = tf.tf_ss = KERNEL_DS;
tf.tf_regs.reg_ebx = (uint32_t)fn;
tf.tf_regs.reg_edx = (uint32_t)arg;
tf.tf_eip = (uint32_t)kernel_thread_entry;
return do_fork(clone_flags | CLONE_VM, 0, &tf);
}
简单的来说,这个函数需要传入三个参数:
第一个fn是一个函数,代表这个创建的内核线程中所需要执行的函数;
第二个arg是相关参数,这里传入的是哲学家编号i;
第三部分是共享内存的标记位,内核线程之间内存是共享的,因此应该设置为0。
其余地方则是设置一些寄存器的值,保留需要执行的函数开始执行的地址,以便创建了新的内核线程之后,函数能够在内核线程中找到入口地址,执行函数功能。
接下来,让我们来分析需要创建的内核线程去执行的目标函数philosopher_using_semaphore
(kern/sync/check_sync.c,52——70行)
int philosopher_using_semaphore(void * arg)
{
int i, iter=0;
i=(int)arg; //传入的参数转为int型,代表哲学家的编号
cprintf("I am No.%d philosopher_sema\n",i);
while(iter++<TIMES) //TIMES=4
{
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;
}
参数及其分析:
传入参数*arg,代表在上一个函数中“参数”部分定义的(void *)i,是哲学家的编号。
iter++<TIMES,表示循环4次,目的在于模拟多次试验情况。
从这个函数,我们看到,哲学家需要思考一段时间,然后吃一段时间的饭,这里面的“一段时间”就是通过系统调用sleep实现的,内核线程调用sleep,然后这个线程休眠指定的时间,从某种方面模拟了吃饭和思考的过程。
以下是do sleep的实现:(kern/process/proc.c,922+行)
int
do_sleep(unsigned int time) {
if (time == 0) {
return 0;
}
bool intr_flag;
local_intr_save(intr_flag);//关闭中断
timer_t __timer, *timer = timer_init(&__timer, current, time);
//声明一个定时器,并将其绑定到当前进程current上
current->state = PROC_SLEEPING;
current->wait_state = WT_TIMER;
add_timer(timer);
local_intr_restore(intr_flag);
schedule();
del_timer(timer);
return 0;
}
我们看到,睡眠的过程中是无法被打断的,符合我们一般的认识,因为它在计时器使用的过程中通过local_intr_save关闭了终端且利用了timer_init定时器函数,去记录指定的时间(传入的参数time),且在这个过程中,将进程的状态设置为睡眠,调用函数add_timer将绑定该进程的计时器加入计时器队列。当计时器结束之后,打开中断,恢复正常。
而反过来看传入的参数,即为定时器的定时值time,在上一层函数中,传入的是kern/sync/check_sync.c,14行的宏定义,TIME的值为10。
相关的图解如下:
目前看来,最关键的函数是phi_take_forks_sema(i),和phi_take_forks_sema(i);
phi_take_forks_sema;phi_take_forks_sema:(kern/sync/check_sync,c,34——50行)
void phi_take_forks_sema(int i) /* i:哲学家号码从0到N-1 */
{
down(&mutex); /* 进入临界区 */
state_sema[i]=HUNGRY; /* 记录下哲学家i饥饿的事实 */
phi_test_sema(i); /* 试图得到两只叉子 */
up(&mutex); /* 离开临界区 */
down(&s[i]); /* 如果得不到叉子就阻塞 */
}
void phi_put_forks_sema(int i) /* i:哲学家号码从0到N-1 */
{
down(&mutex); /* 进入临界区 */
state_sema[i]=THINKING; /* 哲学家进餐结束 */
phi_test_sema(LEFT); /* 看一下左邻居现在是否能进餐 */
phi_test_sema(RIGHT); /* 看一下右邻居现在是否能进餐 */
up(&mutex); /* 离开临界区 */
}
参数解释:
传入参数i:当前哲学家的编号;
mutex,state_sema:定义在当前文件的第17——19行,分别为每个哲学家记录当前的状态。
其中,mutex的数据类型是“信号量结构体”,其定义在kern/sync/sem.h中:
typedef struct {
int value;
wait_queue_t wait_queue;
} semaphore_t;
具体功能和之前的伪代码差不多,这里不再重复。
函数解释:
现在来到了最关键的核心问题解决部分,首先是down和up操作:
up;down:(kern/sync/sem.c,16——54行)
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); //开启中断,正常执行
}
up函数的作用是:首先关中断,如果信号量对应的wait queue中没有进程在等待,直接把信号量的value加一,然后开中断返回;如果有进程在等待且进程等待的原因是semophore设置的,则调用wakeup_wait函数将waitqueue中等待的第一个wait删除,且把此wait关联的进程唤醒,最后开中断返回。
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,那么说明信号量可用,因此可以分配给当前进程运行,分配完之后关闭中断
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函数查找下一个可以被运行调度的进程,此时,如果能够查到,那么唤醒,并将其中队列中删除并返回
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;
}
down函数的作用是:首先关掉中断,然后判断当前信号量的value是否大于0。如果是>0,则表明可以获得信号量,故让value减一,并打开中断返回即可;如果不是>0,则表明无法获得信号量,故需要将当前的进程加入到等待队列中,并打开中断,然后运行调度器选择另外一个进程执行。如果被V操作唤醒,则把自身关联的wait从等待队列中删除(此过程需要先关中断,完成后开中断)。
其中,这里调用了local_intr_save和local_intr_restore两个函数,它们被定义在(kern/sync/sync.h,11——25行):
static inline bool
__intr_save(void) {
if (read_eflags() & FL_IF) {
intr_disable();
return 1;
}
return 0;
}
static inline void
__intr_restore(bool flag) {
if (flag) {
intr_enable();
}
}
很容易发现他们的功能是关闭和打开中断。
分析完了up和down,让我们来分析一下test函数:
phi_test_sema(LEFT); /* 看一下左邻居现在是否能进餐 */
phi_test_sema(RIGHT); /* 看一下右邻居现在是否能进餐 */
该函数被定义在(kern/sync/check_sync.c,86——94行):
void phi_test_sema(i)
{
if(state_sema[i]==HUNGRY&&state_sema[LEFT]!=EATING
&&state_sema[RIGHT]!=EATING)
{
state_sema[i]=EATING;
up(&s[i]);
}
}
在试图获得筷子的时候,函数的传入参数为i,即为哲学家编号,此时,他自己为HUNGRY,而且试图检查旁边两位是否都在吃。如果都不在吃,那么可以获得EATING的状态。
在从吃的状态返回回到思考状态的时候,需要调用两次该函数,传入的参数为当前哲学家左边和右边的哲学家编号,因为他试图唤醒左右邻居,如果左右邻居满足条件,那么就可以将他们设置为EATING状态。
其中,LEFT和RIGHT的定义如下:
#define LEFT (i-1+N)%N
#define RIGHT (i+1)%N
由于哲学家坐圆桌,因此可以使用余数直接获取左右编号。
练习一的总体执行流程如下:
练习2:完成内核级条件变量和基于内核级条件变量的哲学家就餐问题(需要编码)
首先掌握管程机制,然后基于信号量实现完成条件变量实现,然后用管程机制实现哲学家就餐问题的解决方案(基于条件变量)。
一个管程定义了一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据。
管程主要由这四个部分组成:
1、管程内部的共享变量;
2、管程内部的条件变量;
3、管程内部并发执行的进程;
4、对局部于管程内部的共享数据设置初始值的语句。
管程相当于一个隔离区,它把共享变量和对它进行操作的若干个过程围了起来,所有进程要访问临界资源时,都必须经过管程才能进入,而管程每次只允许一个进程进入管程,从而需要确保进程之间互斥。
但在管程中仅仅有互斥操作是不够用的。进程可能需要等待某个条件C为真才能继续执行。
所谓条件变量,即将等待队列和睡眠条件包装在一起,就形成了一种新的同步机制,称为条件变量。一个条件变量CV可理解为一个进程的等待队列,队列中的进程正等待某个条
件C变为真。每个条件变量关联着一个断言 "断言" PC。当一个进程等待一个条件变量,该进程不算作占用了该管程,因而其它进程可以进入该管程执行,改变管程的状态,通知条件变量CV其关联的断言Pc在当前状态下为真。
因而条件变量两种操作如下:
wait_cv: 被一个进程调用,以等待断言Pc被满足后该进程可恢复执行. 进程挂在该条件变量上等待时,不被认为是占用了管程。如果条件不能满足,就需要等待。
signal_cv:被一个进程调用,以指出断言Pc现在为真,从而可以唤醒等待断言Pc被满足的进程继续执行。如果条件可以满足,那么可以运行。
在ucore中,管程数据结构被定义在(kern/sync/monitor.h)中:
typedef struct monitor{
// 二值信号量,只允许一个进程进入管程,初始化为1
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
// 条件变量cv
condvar_t *cv; // the condvars in monitor
} monitor_t;
管程中的成员变量mutex是一个二值信号量,是实现每次只允许一个进程进入管程的关键元素,确保了互斥访问性质。
管程中的条件变量cv通过执行wait_cv,会使得等待某个条件C为真的进程能够离开管程并睡眠,且让其他进程进入管程继续执行;而进入管程的某进程设置条件C为真并执行signal_cv时,能够让等待某个条件C为真的睡眠进程被唤醒,从而继续进入管程中执行。
管程中的成员变量信号量next和整形变量next_count是配合进程对条件变量cv的操作而设置的,这是由于发出signal_cv的进程A会唤醒睡眠进程B,进程B执行会导致进程A睡眠,直到进程B离开管程,进程A才能继续执行,这个同步过程是通过信号量next完成的;
而next_count表示了由于发出singal_cv而睡眠的进程个数。
其中,条件变量cv的数据结构也被定义在同一个位置下:
typedef struct condvar{
semaphore_t sem; //用于发出wait_cv操作的等待某个条件C为真的进程睡眠
int count; //在这个条件变量上的睡眠进程的个数
monitor_t * owner; //此条件变量的宿主管程
} condvar_t;
条件变量的定义中也包含了一系列的成员变量,信号量sem用于让发出wait_cv操作的等待某个条件C为真的进程睡眠,而让发出signal_cv操作的进程通过这个sem来唤醒睡眠的进程。count表示等在这个条件变量上的睡眠进程的个数。owner表示此条件变量的宿主是哪个管程。
其实本来条件变量中需要有等待队列的成员,以表示有多少线程因为当前条件得不到满足而等待,但这里,直接采用了信号量替代,因为信号量数据结构中也含有等待队列。
那么现在开始解决哲学家就餐问题,使用管程,它的实现在(kern/sync/check_sync,199+行)
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");
我们发现,这个实现过程和使用信号量无差别,不同之处在于,各个线程所执行的函数不同,此处执行的为philosopher_using_condvar函数:
philosopher_using_condvar函数:被定义在(kern/sync/check_sync,162——180行)
int philosopher_using_condvar(void * arg) { /* arg is the No. of philosopher 0~N-1*/
int i, iter=0;
i=(int)arg;
cprintf("I am No.%d philosopher_condvar\n",i);
while(iter++<TIMES)
{ /* iterate*/
cprintf("Iter %d, No.%d philosopher_condvar is thinking\n",iter,i); /* thinking*/
do_sleep(SLEEP_TIME);
phi_take_forks_condvar(i);
/* need two forks, maybe blocked */
cprintf("Iter %d, No.%d philosopher_condvar is eating\n",iter,i); /* eating*/
do_sleep(SLEEP_TIME);
phi_put_forks_condvar(i);
/* return two forks back*/
}
cprintf("No.%d philosopher_condvar quit\n",i);
return 0;
}
我们发现这里和用信号量还是没有本质的差别,不同之处在于,获取筷子和放下都使用了不同的,配套管程使用的函数phi_take_forks_condvar和phi_put_forks_condvar。
phi_take_forks_condvar和phi_put_forks_condvar被定义在(kern/sync/check_sync,121——159行)
其中,mtp为一个管程,声明于同一文件下的第108行,state_convader数组记录哲学家的状态,声明于第107行。
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) { //没拿到,需要等待,调用wait函数
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));
}
//这个地方的意思是,如果当前管程的等待数量在唤醒了一个线程之后,还有进程在等待,那么就会唤醒控制当前进程的信号量,让其他进程占有它,如果没有等待的了,那么直接释放互斥锁,这样就可以允许新的进程进入管程了。
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));
}
和信号量的实现差不多,我们在拿起筷子和放下的时候,主要都还要唤醒相邻位置上的哲学家,但是,具体的test操作中,实现有所不同。test函数被定义在(同文件,110——118行)
void phi_test_condvar (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]);
//如果可以唤醒,那么signal操作掉代表这个哲学家那个已经睡眠等待的进程。和wait是对应的。
}
}
现在看来,最主要的部分在于管程的signal和wait操作,ucore操作系统中对于signal和wait操作的实现是有专门的函数的:
他们是cond_signal和cond_wait(kern/sync/monitor.c,26——72行,代码实现部分)
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); //这是一个输出信息的语句,可以不管
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);
}
首先判断cvp.count,如果不大于0,则表示当前没有睡眠在这一个条件变量上的进程,因此就没有被唤醒的对象了,直接函数返回即可,什么也不需要操作。
如果大于0,这表示当前有睡眠在该条件变量上的进程,因此需要唤醒等待在cv.sem上睡眠的进程。而由于只允许一个进程在管程中执行,所以一旦进程B唤醒了别人(进程A),那么自己就需要睡眠。故让monitor.next_count加一,且让自己(进程B)睡在信号量monitor.next(宿主管程的信号量)上。如果睡醒了,这让monitor.next_count减一。
这里为什么最后要加一个next_conut--呢?这说明上一句中的down的进程睡醒了,那么睡醒,就必然是另外一个进程唤醒了它,因为只能有一个进程在管程中被signal,如果有进程调用了wait,那么必然需要signal另外一个进程,那么我们来看wait函数:
【这里是本次实验的难点,请好好理解注释!】
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);
cvp->count++; //条件变量中睡眠的进程数量加加
if(cvp->owner->next_count > 0)
up(&(cvp->owner->next)); //如果当前有进程正在等待,且睡在宿主管程的信号量上,此时需要唤醒,让该调用了wait的睡,此时就唤醒了,对应上面讨论的情况。这是一个同步问题。
else
up(&(cvp->owner->mutex)); //如果没有进程睡眠,那么当前进程无法进入管程的原因就是互斥条件的限制。因此唤醒mutex互斥锁,代表现在互斥锁被占用,此时,再让进程睡在宿主管程的信号量上,如果睡醒了,count--,谁唤醒的呢?就是前面的signal啦,这其实是一个对应关系。
down(&(cvp->sem)); //因为条件不满足,所以主动调用wait的进程,会睡在条件变量cvp的信号量上,是条件不满足的问题;而因为调用signal唤醒其他进程而导致自身互斥睡眠,会睡在宿主管程cvp->owner的信号量上,是同步的问题。两个有区别,不要混了,超级重要鸭!!!
cvp->count --;
cprintf("cond_wait end: cvp %x, cvp->count %d, cvp->owner->next_count %d\n", cvp, cvp->count, cvp->owner->next_count);
}
如果进程A执行了cond_wait函数,表示此进程等待某个条件C不为真,需要睡眠。因此表示等待此条件的睡眠进程个数cv.count要加一。接下来会出现两种情况。
情况一:如果monitor.next_count如果大于0,表示有大于等于1个进程执行cond_signal函数且睡着了,就睡在了monitor.next信号量上。假定这些进程形成S进程链表。因此需要唤醒S进程链表中的一个进程B。然后进程A睡在cv.sem上,如果睡醒了,则让cv.count减一,表示等待此条件的睡眠进程个数少了一个,可继续执行。
情况二:如果monitor.next_count如果小于等于0,表示目前没有进程执行cond_signal函数且睡着了,那需要唤醒的是由于互斥条件限制而无法进入管程的进程,所以要唤醒睡在monitor.mutex上的进程。然后进程A睡在cv.sem上,如果睡醒了,则让cv.count减一,表示等待此条件的睡眠进程个数少了一个,可继续执行了!
四、思考题
1、请在实验报告中给出内核级信号量的设计描述,并说其大致执行流流程。
2、请在实验报告中给出给用户态进程/线程提供信号量机制的设计方案,并比较说明给内核级提供信号量机制的异同。
答:见上述分析和伪代码,该过程已经被详细描述了。
五、运行结果
如果未做challenge,得分应当是98 / 185,如果make grade无法满分,尝试注释掉tools/grade.sh的221行到233行(在前面加上“#”)。
执行make qemu之后,应当看到如下有关于哲学家就餐问题的输出: