ucore lab7

lab7

实验目的

  • 理解操作系统的同步互斥的设计实现;
  • 理解底层支撑技术:禁用中断、定时器、等待队列;
  • 在ucore中理解信号量(semaphore)机制的具体实现;
  • 理解管程机制,在ucore内核中增加基于管程(monitor)的条件变量(condition
    variable)的支持;
  • 了解经典进程同步问题,并能使用同步机制解决进程同步问题。

实验内容

实验六完成了用户进程的调度框架和具体的调度算法,可调度运行多个进程。如果多个进程需要协同操作或访问共享资源,则存在如何同步和有序竞争的问题。本次实验,主要是熟悉ucore的进程同步机制—信号量(semaphore)机制,以及基于信号量的哲学家就餐问题解决方案。然后掌握管程的概念和原理,并参考信号量机制,实现基于管程的条件变量机制和基于条件变量来解决哲学家就餐问题。

在本次实验中,在kern/sync/check_sync.c中提供了一个基于信号量的哲学家就餐问题解法。同时还需完成练习,即实现基于管程(主要是灵活运用条件变量和互斥信号量)的哲学家就餐问题解法。哲学家就餐问题描述如下:有五个哲学家,他们的生活方式是交替地进行思考和进餐。哲学家们公用一张圆桌,周围放有五把椅子,每人坐一把。在圆桌上有五个碗和五根筷子,当一个哲学家思考时,他不与其他人交谈,饥饿时便试图取用其左、右最靠近他的筷子,但他可能一根都拿不到。只有在他拿到两根筷子时,方能进餐,进餐完后,放下筷子又继续思考。

练习

对实验报告的要求:

  • 基于markdown格式来完成,以文本方式为主
  • 填写各个基本练习中要求完成的报告内容
  • 完成实验后,请分析ucore_lab中提供的参考答案,并请在实验报告中说明你的实现与参考答案的区别
  • 列出你认为本实验中重要的知识点,以及与对应的OS原理中的知识点,并简要说明你对二者的含义,关系,差异等方面的理解(也可能出现实验中的知识点没有对应的原理知识点)
  • 列出你认为OS原理中很重要,但在实验中没有对应上的知识点
练习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: 理解内核级信号量的实现和基于内核级信号量的哲学家就餐问题(不需要编码)

完成练习0后,建议大家比较一下(可用meld等文件diff比较软件)个人完成的lab6和练习0完成后的刚修改的lab7之间的区别,分析了解lab7采用信号量的执行过程。执行make grade,大部分测试用例应该通过。

请在实验报告中给出内核级信号量的设计描述,并说其大致执行流流程。

请在实验报告中给出给用户态进程/线程提供信号量机制的设计方案,并比较说明给内核级提供信号量机制的异同。

哲学家就餐问题:

uCore中的哲学家就餐主要代码较为简单:每个哲学家拿起叉子,进食,然后放下叉子。

int state_sema[N]; /* 记录每个人状态的数组 */
/* 信号量是一个特殊的整型变量 */
semaphore_t mutex; /* 临界区互斥 */
semaphore_t s[N]; /* 每个哲学家一个信号量 */

struct proc_struct *philosopher_proc_sema[N];

int philosopher_using_semaphore(void * arg) /* i:哲学家号码,从0到N-1 */
{
    int i, iter=0;
    i=(int)arg;
    cprintf("I am No.%d philosopher_sema\n",i);
    while(iter++<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;
}

拿起 / 放下叉子时,由于需要修改当前哲学家的状态,同时该状态是全局共享变量,所以需要获取锁来防止条件竞争。

将叉子放回桌上时,如果当前哲学家左右两边的两位哲学家处于饥饿状态,即准备进餐但没有刀叉时,如果条件符合,则唤醒这两位哲学家并让其继续进餐。

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); /* 离开临界区 */
}

phi_test_sema函数用于设置哲学家的进食状态。如果当前哲学家满足进食条件,则更新哲学家状态,执行哲学家锁所对应的V操作,以唤醒等待叉子的哲学家所对应的线程。

void phi_test_sema(i) /* i:哲学家号码从0到N-1 */
{
    if(state_sema[i]==HUNGRY&&state_sema[LEFT]!=EATING
            &&state_sema[RIGHT]!=EATING)
    {
        state_sema[i]=EATING;
        up(&s[i]);
    }
}
请给出内核级信号量的设计描述,并说明其大致执行流程:

首先,了解一下信号量:

信号量

信号量是一种同步互斥机制的实现,普遍存在于现在的各种操作系统内核里。相对于spinlock 的应用对象,信号量的应用对象是在临界区中运行的时间较长的进程。等待信号量的进程需要睡眠来减少占用 CPU 的开销。

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);如果相应的信号仍然没有发送,则进程被阻塞或睡眠,直到发送完为止。

信号量结构体
typedef struct {
    int value;
    wait_queue_t wait_queue;
} semaphore_t;

semaphore_t是最基本的记录型信号量(record semaphore)结构,包含了用于计数的整数值value,和一个进程等待队列wait_queue,一个等待的进程会挂在此等待队列上。

本次实验同步互斥的底层支撑

在ucore中提供的底层机制包括中断开关控制和test_and_set相关原子操作机器指令。kern/sync.c中实现的开关中断的控制函数local_intr_save(x)和local_intr_restore(x),它们是基于kern/driver文件下的intr_enable()、intr_disable()函数实现的。具体调用关系为:

关中断:local_intr_save --> __intr_save --> intr_disable --> cli
开中断:local_intr_restore--> __intr_restore --> intr_enable --> sti

最终的cli和sti是x86的机器指令,最终实现了关中断和开中断,即设置了eflags寄存器中与中断相关的位。通过关闭中断,可以防止对当前执行的控制流被其他中断事件处理所打断。既然不能中断,那也就意味着在内核运行的当前进程无法被打断或被从新调度,即实现了对临界区的互斥操作。所以在单处理器情况下,可以通过开关中断实现对临界区的互斥保护,需要互斥的临界区代码的一般写法为:

local_intr_save(intr_flag);
{
临界区代码
}
local_intr_restore(intr_flag);
……

在ucore中最重要的信号量操作是P操作函数down(semaphore_t *sem)和V操作函数 up(semaphore_t *sem)。但这两个函数的具体实现是_down(semaphore_t *sem, uint32_t wait_state) 函数和__up(semaphore_t *sem, uint32_t wait_state)函数,二者的具体实现描述如下:

__down(semaphore_t *sem, uint32_t wait_state, timer_t *timer):
 static __noinline uint32_t __down(semaphore_t *sem, uint32_t wait_state) {
    bool intr_flag;                                      
    local_intr_save(intr_flag);         //关中断
    if (sem->value > 0) {               //可以获得信号量
        sem->value --;                  //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;
}

总结一下就是:

先关掉中断,然后判断当前信号量的value是否大于0。如果是>0,则表明可以获得信号量,故让value减一,并打开中断返回即可;如果不是>0,则表明无法获得信号量,故需要将当前的进程加入到等待队列中,并打开中断,然后运行调度器选择另外一个进程执行。如果被V操作唤醒,则把自身关联的wait从等待队列中删除(此过程需要先关中断,完成后开中断)。

__up(semaphore_t *sem, uint32_t wait_state):
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 ++;  //value加一
        }
        else {
            assert(wait->proc->wait_state == wait_state);   //有进程等待
            wakeup_wait(&(sem->wait_queue), wait, wait_state, 1); //将第一个wait删除,唤醒进程。
        }
    }
    local_intr_restore(intr_flag); //打开中断
}

总结一下过程:

首先关中断,如果信号量对应的wait queue中没有进程在等待,直接把信号量的value加一,然后开中断返回;如果有进程在等待且进程等待的原因是semophore设置的,则调用wakeup_wait函数将waitqueue中等待的第一个wait删除,且把此wait关联的进程唤醒,最后开中断返回。

等待队列wait_queue定义在另一个结构体中:

// 用于等待队列,存放了当前等待的线程的PCB和唤醒原因和等待队列和用于还原结构体的等待队列标志
typedef  struct {
    struct proc_struct *proc;     //等待进程的指针
    uint32_t wakeup_flags;        //进程被放入等待队列的原因标记
    wait_queue_t *wait_queue;     //指向此wait结构所属于的wait_queue
    list_entry_t wait_link;       //用来组织wait_queue中wait节点的连接
} wait_t;

其中信号量结构体中value的含义如下:

  • value>0, 表示共享资源的空闲数
  • vlaue<0, 表示该信号量的等待队列里的进程数
  • value=0, 表示等待队列为空

对于V操作和P操作,分别用up和down函数来对应,就可以用来实现内核级信号量设计。

回答问题
  • 请在实验报告中给出给用户态进程/线程提供信号量机制的设计方案,并比较说明给内核级提供信号量机制的异同。

用户态的进程/线程的信号量的数据结构与内核态相同。用户态进程/线程的信号量的相关操作通过系统调用来完成。每当用户进程调用信号量相关函数时,都会进入系统调用,由内核进行处理,之后再返回到用户态继续执行。相比于为内核提供的信号量机制,用户态进程/线程由于要执行中断操作等特权指令,需要通过系统调用进入内核态使用内核信号量机制。

内核为用户态进程/线程提供信号量机制时,需要设计多个应用程序接口,而用户态线程只能通过这些内核提供的接口来使用内核服务。借鉴于Linux提供的标准接口,内核提供的这些接口可分别为:

/*Initialize semaphore object SEM to VALUE.  If PSHARED then share it
   with other processes.  */
int sem_init (sem_t *__sem, int __pshared, unsigned int __value);
/* Free resources associated with semaphore object SEM.  */
// 将信号量所使用的资源全部释放
int sem_destroy (sem_t *__sem);

/* Open a named semaphore NAME with open flags OFLAG.  */
// 开启一个新信号量,并使用给定的flag来指定其标志
sem_t *sem_open (const char *__name, int __oflag, ...);

/* Close descriptor for named semaphore SEM.  */
// 将当前信号量所使用的描述符关闭
int sem_close (sem_t *__sem);

/* Remove named semaphore NAME.  */
int sem_unlink (const char *__name);

/* Wait for SEM being posted.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
// 一个P操作,如果sem value > 0,则sem value--;否则阻塞直到sem value > 0
int sem_wait (sem_t *__sem);

/* Test whether SEM is posted.  */
int sem_trywait (sem_t *__sem);

/* Post SEM.  */
// 一个V操作,把指定的信号量 sem 的值加 1,唤醒正在等待该信号量的任意线程。
int sem_post (sem_t *__sem);

/* Get current value of SEM and store it in *SVAL.  */
// 获取当前信号量的值
int sem_getvalue (sem_t *__restrict __sem, int *__restrict __sval);
  • 相同点
    • 其核心的实现逻辑是一样的
  • 不同点
    • 内核态的信号量机制可以直接调用内核的服务,而用户态的则需要通过内核提供的接口来访问内核态服务,这其中涉及到了用户态转内核态的相关机制。
    • 内核态的信号量存储于内核栈中;但用户态的信号量存储于用户栈中。
练习2: 完成内核级条件变量和基于内核级条件变量的哲学家就餐问题(需要编码)

首先掌握管程机制,然后基于信号量实现完成条件变量实现,然后用管程机制实现哲学家就餐问题的解决方案(基于条件变量)。

执行:make grade
。如果所显示的应用程序检测都输出ok,则基本正确。如果只是某程序过不去,比如matrix.c,则可执行

make run-matrix

命令来单独调试它。大致执行结果可看附录。

请在实验报告中给出内核级条件变量的设计描述,并说其大致执行流流程。

请在实验报告中给出给用户态进程/线程提供条件变量机制的设计方案,并比较说明给内核级提供条件变量机制的异同。

请在实验报告中回答:能否不用基于信号量机制来完成条件变量?如果不能,请给出理由,如果能,请给出设计说明和具体实现。

基于信号量实现完成条件变量实现,给出内核级条件变量的设计描述,并说明其大致执行流程。

管程由一个锁和多个条件变量组成,以下是管程和条件变量的结构体代码

typedef struct monitor monitor_t;

typedef struct condvar{
    semaphore_t sem;        // 条件变量所对应的信号量
    int count;              // 等待当前条件变量的等待进程总数
    monitor_t * owner;      // 当前条件变量的父管程
} condvar_t;

typedef struct monitor{
    semaphore_t mutex;      // 管程锁,每次只能有一个进程执行管程代码。该值初始化为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;          // 当前管程中存放所有条件变量的数组
} monitor_t;

注意:monitor结构中next信号量的功能请在下文结合cond_signal说明来理解。

管程中的成员变量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而睡眠的进程个数。

管程

引入了管程是为了将对共享资源的所有访问及其所需要的同步操作集中并封装起来。Hansan为管程所下的定义:“一个管程定义了一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据”。由上述定义可知,管程由四部分组成:

  • 管程内部的共享变量;

  • 管程内部的条件变量;

  • 管程内部并发执行的进程;

  • 对局部于管程内部的共享数据设置初始值的语句。

局限在管程中的数据结构,只能被局限在管程的操作过程所访问,任何管程之外的操作过程都不能访问它;另一方面,局限在管程中的操作过程也主要访问管程内的数据结构。由此可见,管程相当于一个隔离区,它把共享变量和对它进行操作的若干个过程围了起来,所有进程要访问临界资源时,都必须经过管程才能进入,而管程每次只允许一个进程进入管程,从而需要确保进程之间互斥。

条件变量(CV)

一个条件变量CV可理解为一个进程的等待队列,队列中的进程正等待某个条件C变为真。每个条件变量关联着一个断言 “断言 (程序)”)Pc。当一个进程等待一个条件变量,该进程不算作占用了该管程,因而其它进程可以进入该管程执行,改变管程的状态,通知条件变量CV其关联的断言Pc在当前状态下为真。

因此对条件变量CV有两种主要操作:

  • wait_cv: 被一个进程调用,以等待断言Pc被满足后该进程可恢复执行. 进程挂在该条件变量上等待时,不被认为是占用了管程。
  • signal_cv:被一个进程调用,以指出断言Pc现在为真,从而可以唤醒等待断言Pc被满足的进程继续执行。
condvar_t的定义
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表示此条件变量的宿主是哪个管程。

monitor_init()函数:

对条件变量进行初始化,设置next_count为0,对mutex,next进行初始化, sem_init(&(mtp->mutex), 1); //unlocked
并分配num个condvar_t,设置cv的count为0,初始化cv的sem和owner。

for(i=0; i<num_cv; i++){

mtp->cv[i].count=0;
sem_init(&(mtp->cv[i].sem),0);
mtp->cv[i].owner=mtp;
}
cond_signal()函数: 唤醒睡在条件变量上的线程

如果cv的count>0,说明有proc在等待,那么需要唤醒等待在cv.sem上的proc,并使自己进行睡眠,同时monitor.next_count++,在被唤醒后执行monitor.next_count–;如果cv的count == 0,说明没有proc在等待cv.sem,直接返回函数。

首先进程B判断cv.count,如果不大于0,则表示当前没有睡眠的进程,因此就没有被唤醒的对象了,直接函数返回即可; 如果大于0,这表示当前有睡眠的进程A,因此需要唤醒等待在cv.sem上睡眠的进程A。由于只允许一个进程在管程中执行,所以一旦进程B唤醒了别人(进程A),那么自己就需要睡眠。故让monitor.next_count加一,且让自己(进程B)睡在信号量monitor.next上。如果睡醒了,这让monitor.next_count减一。

for(i=0; i<num_cv; i++){

if(cvp ->count > 0){
cvp->owner -> next_count ++;
up(&(cvp->sem));
down(&(cvp -> owner->next));//这就是Hoare机制,直接切换回等待条件变量的进程,等其执行完毕,这个线程才能执行 count--
cvp -> owner -> next_count --;
 }
cond_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)); //唤醒进程链表中的下一个进程
      else
         up(&(cvp->owner->mutex)); //唤醒睡在monitor.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);
}

可以看出如果进程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减一,表示等待此条件的睡眠进程个数少了一个,可继续执行了!

基于条件变量和管程的哲学家就餐问题的实现
  • 这题涉及到了两个函数,分别是phi_take_forks_condvarphi_put_forks_condvar。与信号量所实现的哲学家就餐问题类似,大体逻辑是一致的。
  • 首先,哲学家需要尝试获取刀叉,如果刀叉没有获取到,则等待刀叉。

phi_put_forks_condvar函数表示释放当前哲学家占用的叉子,并且唤醒相邻的因为得不到资源而进入等待的哲学家:

  • 首先获取管程的锁,将自己的状态修改成THINKING;
  • 检查相邻的哲学家是否在自己释放了叉子的占用之后满足了进餐的条件,如果满足,将其从等待中唤醒
  • 释放锁,离开管程

phi_take_forks_condvar函数表示指定的哲学家尝试获得自己所需要进餐的两把叉子,如果不能获得则阻塞,具体实现流程为:

  • 给管程上锁,将哲学家的状态修改为HUNGER;
  • 判断相邻的哲学家是否正在进餐;
  • 如果能够进餐,将自己的状态修改成EATING,然后释放锁,离开管程即可;
  • 如果不能进餐,等待在自己对应的条件变量上,等待相邻的哲学家释放资源的时候将自己唤醒;
void phi_take_forks_condvar(int i) {
     down(&(mtp->mutex));
//--------into routine in monitor--------------
     // LAB7 EXERCISE1: YOUR CODE
     // I am hungry
     state_condvar[i]=HUNGRY; /* 记录下哲学家i饥饿的事实 */
     // try to get fork
     phi_test_condvar(i);
     if (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));
}

之后,当哲学家放下刀叉时,如果左右两边的哲学家都满足条件可以进餐,则设置对应的条件变量。

void phi_put_forks_condvar(int i) {
     down(&(mtp->mutex));
//--------into routine in monitor--------------
     // LAB7 EXERCISE1: YOUR CODE
     // 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));
}

以下是哲学家尝试进餐的代码

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]) ;
    }
}

由于每个哲学家只可能占有所有需要的资源或者完全不占用资源,因此不会出现部分占有资源的现象,从而避免了死锁的产生;最终必定所有哲学将都能成功就餐。

实验结果:

运行make qemu查看运行结果:

在这里插入图片描述

运行make grade 查看成绩:

在这里插入图片描述

扩展练习 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值