湖南大学操作系统实验ucore lab7

阅读前注意事项:

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之后,应当看到如下有关于哲学家就餐问题的输出:

猜你喜欢

转载自blog.csdn.net/yyd19981117/article/details/86694674
今日推荐