操作系统ucore lab7实验报告

练习0

填写已有实验
本实验依赖实验1~实验6.请把已做的实验1~实验6的代码填入本实验中代码中有lab1、lab2、lab3、lab4、lab5、lab6的注释相应部分,并确保编译通过。
注意:为了能够正确执行lab7的测试应用程序,可能需对已完成的实验1~实验5的代码进一步改进

操作系统ucore lab7实验报告_第1张图片

发现缺失的是kdebug.c、trap.c、default_pmm.c、pmm.c、swap_fifo.c、vmm.c、proc.c、sche.c八个文件的相关代码,补全后不需要在6的基础上改了

练习1

理解内核级信号量的实现和基于内核级信号量的哲学家就餐问题(不需要编码)

哲学家就餐问题

  首先通过查阅资料,理解哲学家就餐问题

  哲学家就餐问题,即有五个哲学家,他们的生活方式是交替地进行思考和进餐。哲学家们公用一张圆桌,周围放有五把椅子,每人坐一把。在圆桌上有五个碗和五根筷子,当一个哲学家思考时,他不与其他人交谈,饥饿时便试图取用其左、右最靠近他的筷子,但他可能一根都拿不到。只有在他拿到两根筷子时,方能进餐,进餐完后,放下筷子又继续思考。

  筷子是临界资源,一段时间只允许一位哲学家使用。为了表示互斥,用一个信号量表示一只筷子,五个信号量构成信号量数组。本文中算法用类C语言描述伪码算法。算法描述如下:n用五支筷子的信号量构成信号量数组:

Semaphore chopstick[5]={1,l,1,l,1};
Pi()
{
    do
    {
        p(chopstick[i]);//取左边的筷子
        p(chopstick[(i+1) % 5]);//取右边的筷子
        eat;                //进餐
        v(chopstick[i]);//放回左边的筷子
        v(chopstick[(i+1) % 5]);//放回右边的筷子
        think;
    }while(1);
}

思考:

  当哲学家饥饿时,总是先去拿他左边的筷子,执行wait(chopstick[I]),成功后,再去拿他右边的筷子,执行wait(chopstick[I+1]%5);成功后便可进餐。进餐毕,先放下他左边的筷子,然后再放下右边的筷子。当五个哲学家同时去取他左边的筷子,每人拿到一只筷子且不释放,即五个哲学家只得无限等待下去,引起死锁。

  这就很好的知道同步互斥问题很重要,然后分析下信号量。

  在分析之前先对信号量进行简介,看书上关于Operating Systems Internals and Design Principles 第5章同步互斥中对信号量实现的原理性描述:

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);如果相应的信号仍然没有发送,则进程被阻塞或睡眠,直到发送完为止。
  ucore中信号量参照上述原理描述,建立在开关中断机制和wait queue的基础上进行了具体实现。信号量的数据结构定义如下:

typedef struct {  
    int value;                           //信号量的当前值  
    wait_queue_t wait_queue;     //信号量对应的等待队列  
} semaphore_t;  

接下来进入代码的分析。

  lab7和之前的lab6的大体执行流程都与实验六相同,查看到proc.c文件,发现init_main中有问题
操作系统ucore lab7实验报告_第2张图片

  函数在开始执行调度之前多执行了一个check_sync函数

check_sync函数

void check_sync(void){

    int i;

    //check semaphore
    sem_init(&mutex, 1);
    for(i=0;i0);
        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");
    }

    //check condition variable
    monitor_init(&mt, N);
    for(i=0;iint 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");
    }
}

  根据注释可以看到,该函数分为了两个部分,第一部分是实现基于信号量的哲学家问题,第二部分是实现基于管程的哲学家问题。
  就先分析前一部分。首先实现初始化了一个互斥信号量,然后创建了对应5个哲学家行为的5个信号量,并创建5个内核线程代表5个哲学家,每个内核线程完成了基于信号量的哲学家吃饭睡觉思考行为实现。

philosopher_using_semaphore函数

然后分析philosopher_using_semaphore

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++/* 无限循环 */
        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 lab7实验报告_第3张图片

操作系统ucore lab7实验报告_第4张图片

操作系统ucore lab7实验报告_第5张图片

操作系统ucore lab7实验报告_第6张图片

操作系统ucore lab7实验报告_第7张图片

操作系统ucore lab7实验报告_第8张图片

操作系统ucore lab7实验报告_第9张图片

  核心是phi_take_forks_semaphi_put_forks_sema两个函数,
  然后up和down函数就分别调用了__up函数和__down函数,而这两个函数分别对应着信号量的V,P操作。

__up(semaphore_t *sem, uint32_t wait_state)

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

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);
        }
    }
    local_intr_restore(intr_flag);//开启中断返回
}

__down(semaphore_t *sem, uint32_t wait_state, timer_t *timer)

  具体实现信号量的P操作,首先关掉中断,然后判断当前信号量的value是否大于0。如果是>0,则表明可以获得信号量,故让value减一,并打开中断返回即可;如果不是>0,则表明无法获得信号量,故需要将当前的进程加入到等待队列中,并打开中断,然后运行调度器选择另外一个进程执行。如果被V操作唤醒,则把自身关联的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) {//当前信号量value大于0
        //直接让value减一
        sem->value --;
        //开中断返回
        local_intr_restore(intr_flag);
        return 0;
    }
    //当value小于等于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);//关中断
    //被V操作唤醒,从等待队列移除
    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具有有如下性质:

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

练习2

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

原理

  一个管程定义了一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据。
管程主要由这四个部分组成

  • 1、管程内部的共享变量;
  • 2、管程内部的条件变量;
  • 3、管程内部并发执行的进程;
  • 4、对局部于管程内部的共享数据设置初始值的语句。

  管程相当于一个隔离区,它把共享变量和对它进行操作的若干个过程围了起来,所有进程要访问临界资源时,都必须经过管程才能进入,而管程每次只允许一个进程进入管程,从而需要确保进程之间互斥。
  但在管程中仅仅有互斥操作是不够用的。进程可能需要等待某个条件C为真才能继续执行。
  所谓条件变量,即将等待队列和睡眠条件包装在一起,就形成了一种新的同步机制,称为条件变量。一个条件变量CV可理解为一个进程的等待队列,队列中的进程正等待某个条件C变为真。每个条件变量关联着一个断言Pc。当一个进程等待一个条件变量,该进程不算作占用了该管程,因而其它进程可以进入该管程执行,改变管程的状态,通知条件变量CV其关联的断言Pc在当前状态下为真。

因而条件变量两种操作如下:
- wait_cv: 被一个进程调用,以等待断言Pc被满足后该进程可恢复执行. 进程挂在该条件变量上等待时,不被认为是占用了管程。
- signal_cv:被一个进程调用,以指出断言Pc现在为真,从而可以唤醒等待断言Pc被满足的进程继续执行。

分析具体的代码。
  ucore中的管程机制是基于信号量和条件变量来实现的。管程的数据结构monitor_t如下:

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;

  管程中的条件变量cv通过执行wait_cv,会使得等待某个条件C为真的进程能够离开管程并睡眠,且让其他进程进入管程继续执行;而进入管程的某进程设置条件C为真并执行signal_cv时,能够让等待某个条件C为真的睡眠进程被唤醒,从而继续进入管程中执行。发出signal_cv的进程A会唤醒睡眠进程B,进程B执行会导致进程A睡眠,直到进程B离开管程,进程A才能继续执行,这个同步过程是通过信号量next完成的;而next_count表示了由于发出singal_cv而睡眠的进程个数。

条件变量condvar_t的数据结构如下:

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用于让发出wait_cv操作的等待某个条件C为真的进程睡眠,而让发出signal_cv操作的进程通过这个sem来唤醒睡眠的进程。count表示等在这个条件变量上的睡眠进程的个数。owner表示此条件变量的宿主是哪个管程。

  开始分析管程的实现。ucore设计实现了条件变量wait_cv操作和signal_cv操作对应的具体函数,即cond_wait函数和cond_signal函数,此外还有cond_init初始化函数。

操作系统ucore lab7实验报告_第10张图片

操作系统ucore lab7实验报告_第11张图片

  简单分析一下cond_wait函数的实现。可以看出如果进程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减一,表示等待此条件的睡眠进程个数少了一个,可继续执行了!

  对照着再来看cond_signal的实现。
  首先进程B判断cv.count,如果不大于0,则表示当前没有睡眠的进程,因此就没有被唤醒的对象了,直接函数返回即可;
  如果大于0,这表示当前有睡眠的进程A,因此需要唤醒等待在cv.sem上睡眠的进程A。由于只允许一个进程在管程中执行,所以一旦进程B唤醒了别人(进程A),那么自己就需要睡眠。故让monitor.next_count加一,且让自己(进程B)睡在信号量monitor.next上。如果睡醒了,这让monitor.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) 
   {//当前存在执行cond_wait而睡眠的进程  
        cvp->owner->next_count ++;//睡眠的进程总个数加一  
        up(&(cvp->sem));//唤醒等待在cv.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);
}

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  
    {
        //唤醒睡在monitor.mutex上的进程
        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);
}

  分析完毕后,就可以完成哲学家就餐问题。

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饥饿
    state_condvar[i]=HUNGRY; 
    // 试图拿叉子
    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
     //哲学家进餐结束   
    state_condvar[i]=THINKING;
    //看一下左边是否能拿叉子    
    phi_test_condvar(LEFT);
    //看一下右边是否能拿叉子  
    phi_test_condvar(RIGHT);
//--------leave routine in monitor--------------
     if(mtp->next_count>0)
        up(&(mtp->next));
     else
        up(&(mtp->mutex));
}

实验结果

通过make qemu得到结果如下图:
操作系统ucore lab7实验报告_第12张图片

make run-matrix
操作系统ucore lab7实验报告_第13张图片

对比实验指导书,实验成功!

实验收获

  通过本次实验,结合哲学家就餐问题,理解了什么是同步互斥,互斥是指某一资源同时只允许一个进程对其进行访问,具有唯一性和排它性,但互斥不用限制进程对资源的访问顺序,即访问可以是无序的。同步是指在进程间的执行必须严格按照规定的某种先后次序来运行,即访问是有序的,这种先后次序取决于要系统完成的任务需求。在进程写资源情况下,进程间要求满足互斥条件。在进程读资源情况下,可允许多个进程同时访问资源。针对开关中断的顺序以及多者互斥问题的理解,在代码的逻辑理解方面还是存在着一些问题。

你可能感兴趣的:(操作系统)