ucoreOS_lab7 实验报告

全部的实验报告将会在 Github 同步更新,更多内容请移步至Github:https://github.com/AngelKitty/review_the_national_post-graduate_entrance_examination/blob/master/books_and_notes/professional_courses/operating_system/sources/ucore_os_lab/docs/lab_report/html

练习0:填写已有实验

lab7 会依赖 lab1~lab6 ,咱们须要把作的 lab1~lab6 的代码填到 lab7 中缺失的位置上面。练习 0 就是一个工具的利用。这里我使用的是 Linux 下的系统已预装好的 Meld Diff Viewer 工具。和 lab6 操做流程同样,咱们只须要将已经完成的 lab1~lab6 与待完成的 lab7 (因为 lab7 是基于 lab1~lab6 基础上完成的,因此这里只须要导入 lab6 )分别导入进来,而后点击 compare 就好了。linux

compare

而后软件就会自动分析两份代码的不一样,而后就一个个比较比较复制过去就好了,在软件里面是能够支持打开对比复制了,点击 Copy Right 便可。固然 bin 目录和 obj 目录下都是 make 生成的,就不用复制了,其余须要修改的地方主要有如下七个文件,经过对比复制完成便可:git

proc.c
default_pmm.c
pmm.c
swap_fifo.c
vmm.c
trap.c
sche.c

根据试验要求,咱们须要对部分代码进行改进,这里讲须要改进的地方只有一处:github

trap.c() 函数数组

修改的部分以下:数据结构

static void trap_dispatch(struct trapframe *tf) {
    ++ticks;
    /* 注销掉下面这一句 由于这一句被包含在了 run_timer_list()
        run_timer_list() 在以前的基础上 加入了对 timer 的支持 */
    // sched_class_proc_tick(current);
    run_timer_list();
}

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

在完成本练习以前,先说明下什么是哲学家就餐问题:并发

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

struct

在分析以前,咱们先对信号量有个了解,既然要理解信号量的实现方法,咱们能够先看看信号量的伪代码:函数

struct semaphore {
  int count;
  queueType queue;
};

void P(semaphore S){
  S.count--;
  if (S.count<0) {
    把进程置为睡眠态;
    将进程的PCB插入到S.queue的队尾;
    调度,让出CPU;
  }
}

void V(semaphore S){
    S.count++;
    if (S.count≤0) {
    唤醒在S.queue上等待的第一个进程;
  }
}

基于上诉信号量实现能够认为,当多个进程能够进行互斥或同步合做时,一个进程会因为没法知足信号量设置的某条件而在某一位置中止,直到它接收到一个特定的信号(代表条件知足了)。为了发信号,须要使用一个称做信号量的特殊变量。为经过信号量 s 传送信号,信号量经过 V、P 操做来修改传送信号量。工具

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

实验 7 的主要任务是实现基于信号量和管程去解决哲学家就餐问题,咱们知道,解决哲学家就餐问题须要建立与之相对应的内核线程,而全部内核线程的建立都离不开 pid 为 1 的那个内核线程——idle,此时咱们须要去寻找在实验 4 中讨论过的地方,如何建立并初始化 idle 这个内核线程。

在实验 7 中,具体的信号量数据结构被定义在(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 函数被分为了两个部分,第一部分使用了信号量来解决哲学家就餐问题,第二部分则是使用管程的方法。所以,练习 1 中咱们只须要关注前半段。

首先观察到利用 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)/* i:哲学家号码,从0到N-1 */
{
    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。

相关的图解以下:

time

目前看来,最关键的函数是 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 操做:(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);//将 wait_queue 中等待的第一个 wait 删除,并将该进程唤醒
        }
    }
    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 --;//直接让 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 函数查找下一个能够被运行调度的进程,此时,若是可以查到,那么唤醒,并将其中队列中删除并返回
    schedule();//运行调度器选择其余进程执行
 
    local_intr_save(intr_flag);//关中断
    wait_current_del(&(sem->wait_queue), wait);//被 V 操做唤醒,从等待队列移除
    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();
    }
}

很容易发现他们的功能是关闭和打开中断。

break

分析完了 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

因为哲学家坐圆桌,所以可使用余数直接获取左右编号。

练习一的整体执行流程以下:

do_sleep

run_time_list

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

实现了内核级信号量机制的函数均定义在 sem.c 中,所以对上述这些函数分析总结以下:

  • sem_init:对信号量进行初始化的函数,根据在原理课上学习到的内容,信号量包括了等待队列和一个整型数值变量,该函数只须要将该变量设置为指定的初始值,而且将等待队列初始化便可;
  • __up:对应到了原理课中说起到的 V 操做,表示释放了一个该信号量对应的资源,若是有等待在了这个信号量上的进程,则将其唤醒执行;结合函数的具体实现能够看到其采用了禁用中断的方式来保证操做的原子性,函数中操做的具体流程为:
    • 查询等待队列是否为空,若是是空的话,给整型变量加 1;
    • 若是等待队列非空,取出其中的一个进程唤醒;
  • __down:一样对应到了原理课中说起的P操做,表示请求一个该信号量对应的资源,一样采用了禁用中断的方式来保证原子性,具体流程为:
    • 查询整型变量来了解是否存在多余的可分配的资源,是的话取出资源(整型变量减 1),以后当前进程即可以正常进行;
    • 若是没有可用的资源,整型变量不是正数,当前进程的资源需求得不到知足,所以将其状态改成 SLEEPING 态,而后将其挂到对应信号量的等待队列中,调用 schedule 函数来让出 CPU,在资源获得知足,从新被唤醒以后,将自身从等待队列上删除掉;
  • up, down:对 __up, __down 函数的简单封装;
  • try_down:不进入等待队列的 P 操做,即时是获取资源失败也不会堵塞当前进程;

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

将内核信号量机制迁移到用户态的最大麻烦在于,用于保证操做原子性的禁用中断机制、以及 CPU 提供的 Test and Set 指令机制都只能在用户态下运行,而使用软件方法的同步互斥又至关复杂,这就使得无法在用户态下直接实现信号量机制;因而,为了方便起见,能够将信号量机制的实现放在 OS 中来提供,而后使用系统调用的方法统一提供出若干个管理信号量的系统调用,分别以下所示:

  • 申请建立一个信号量的系统调用,能够指定初始值,返回一个信号量描述符(相似文件描述符);
  • 将指定信号量执行 P 操做;
  • 将指定信号量执行 V 操做;
  • 将指定信号量释放掉;

给内核级线程提供信号量机制和给用户态进程/线程提供信号量机制的异同点在于:

  • 相同点:
    • 提供信号量机制的代码实现逻辑是相同的;
  • 不一样点:
    • 因为实现原子操做的中断禁用、Test and Set 指令等均须要在内核态下运行,所以提供给用户态进程的信号量机制是经过系统调用来实现的,而内核级线程只须要直接调用相应的函数就能够了;

练习2: 完成内核级条件变量和基于内核级条件变量的哲学家就餐问题(须要编码)

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

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

管程主要由这四个部分组成:

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

管程至关于一个隔离区,它把共享变量和对它进行操做的若干个过程围了起来,全部进程要访问临界资源时,都必须通过管程才能进入,而管程每次只容许一个进程进入管程,从而须要确保进程之间互斥。

但在管程中仅仅有互斥操做是不够用的。进程可能须要等待某个条件 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; // 二值信号量 用来互斥访问管程
    //用于进程同步操做的信号量
    semaphore_t next;// 用于条件同步(进程同步操做的信号量),发出 signal 操做的进程等条件为真以前进入睡眠
    // 睡眠的进程数量
    int next_count;// 记录睡在 signal 操做的进程数
    // 条件变量cv
    condvar_t *cv;// 条件变量
} 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{
    // 用于条件同步 用于发出 wait 操做的进程等待条件为真以前进入睡眠
    semaphore_t sem;        //用于发出 wait_cv 操做的等待某个条件 C 为真的进程睡眠
    // 记录睡在 wait 操做的进程数(等待条件变量成真)
    int count;            //在这个条件变量上的睡眠进程的个数
    // 所属管程
    monitor_t * owner;      //此条件变量的宿主管程
} condvar_t;

条件变量的定义中也包含了一系列的成员变量,信号量 sem 用于让发出 wait_cv 操做的等待某个条件 C 为真的进程睡眠,而让发出 signal_cv 操做的进程经过这个 sem 来唤醒睡眠的进程。count 表示等在这个条件变量上的睡眠进程的个数。owner 表示此条件变量的宿主是哪一个管程。

其实原本条件变量中须要有等待队列的成员,以表示有多少线程由于当前条件得不到知足而等待,但这里,直接采用了信号量替代,由于信号量数据结构中也含有等待队列。

咱们对管程进行初始化操做:

// 初始化管程
void monitor_init (monitor_t * mtp, size_t num_cv) {
    int i;
    assert(num_cv>0);
    mtp->next_count = 0; // 睡在 signal 进程数 初始化为 0
    mtp->cv = NULL;
    sem_init(&(mtp->mutex), 1); // 二值信号量 保护管程 使进程访问管程操做为互斥的
    sem_init(&(mtp->next), 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;
        sem_init(&(mtp->cv[i].sem),0);
        mtp->cv[i].owner=mtp;
    }
}

那么如今开始解决哲学家就餐问题,使用管程,它的实如今(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行。

// 拿刀叉
/*
* phi_take_forks_condvar() 函数实现思路:
  1. 获取管程的锁
  2. 将本身设置为饥饿状态
  3. 判断当前叉子是否足够就餐,如不能,等待其余人释放资源
  4. 释放管程的锁
*/ 
void phi_take_forks_condvar(int i) {
     down(&(mtp->mutex));   //保证互斥操做,P 操做进入临界区
//--------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);      //测试哲学家是否能拿到刀叉,若不能拿,则阻塞本身,等其它进程唤醒
      if (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));
}

这个地方的意思是,若是当前管程的等待数量在唤醒了一个线程以后,还有进程在等待,那么就会唤醒控制当前进程的信号量,让其余进程占有它,若是没有等待的了,那么直接释放互斥锁,这样就能够容许新的进程进入管程了。

// 放刀叉
/*
* phi_put_forks_condvar() 函数实现思路:
  1. 获取管程的锁
  2. 将本身设置为思考状态
  3. 判断左右邻居的哲学家是否能够从等待就餐的状态中恢复过来
*/ 
void phi_put_forks_condvar(int i) {
     down(&(mtp->mutex));// P 操做进入临界区
 
//--------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)// 有哲学家睡在 signal 操做,则将其唤醒
        up(&(mtp->next));
     else
        up(&(mtp->mutex));//离开临界区
}

和信号量的实现差很少,咱们在拿起筷子和放下的时候,主要都还要唤醒相邻位置上的哲学家,可是,具体的test操做中,实现有所不一样。test 函数被定义在(同文件,110——118行)

// 测试编号为i的哲学家是否能得到刀叉 若是能得到 则将状态改成正在吃 而且 尝试唤醒 由于wait操做睡眠的进程
// cond_signal 还会阻塞本身 等被唤醒的进程唤醒本身
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阻塞的进程->阻塞本身
                  |             |                  A
                  |             V                  |
                  ->不能拿->wait阻塞本身             |
                                                   |
哲学家->放刀叉->让左右两边试试拿刀叉->有哲学家睡在signal 唤醒他

如今看来,最主要的部分在于管程的 signal 和 wait 操做,ucore 操做系统中对于 signal 和 wait 操做的实现是有专门的函数的,它们是 cond_signal 和 cond_wait(kern/sync/monitor.c,26——72行,代码实现部分)

// 管程signal操做
/*
分支1. 由于条件不成立而睡眠的进程计数小于等于0 时 说明 没有进程须要唤醒 则直接返回
分支2. 由于条件不成立而睡眠的进程计数大于0 说明有进程须要唤醒 就将其唤醒
同时设置 条件变量所属管程的 next_count 加1 以用来告诉 wait操做 有进程睡在了 signal操做上
而后本身将本身阻塞 等待条件同步 被唤醒 被唤醒后 睡在 signal 操做上的进程应该减小 故 next_count 应减 1
*/
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() 函数实现思路:
     1. 判断条件变量的等待队列是否为空
     2. 修改 next 变量上等待进程计数,跟下一个语句不能交换位置,为了获得互斥访问的效果,关键在于访问共享变量的时候,管程中是否只有一个进程处于 RUNNABLE 的状态
     3. 唤醒等待队列中的某一个进程
     4. 把本身等待在 next 条件变量上
     5. 当前进程被唤醒,恢复 next 上的等待进程计数
   */ 
   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 另一个进程,咱们能够从下图能够看到这一调用过程:

monitor

咱们来看 wait 函数:

// 管程wait操做
/*
先将 由于条件不成立而睡眠的进程计数加1
分支1. 当 管程的 next_count 大于 0 说明 有进程睡在了 signal 操做上 咱们将其唤醒
分支2. 当 管程的 next_count 小于 0 说明 当前没有进程睡在 signal 操做数 只须要释放互斥体
而后 再将 自身阻塞 等待 条件变量的条件为真 被唤醒后 将条件不成立而睡眠的进程计数减1 由于如今成立了
*/
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);
   /*
   * cond_wait() 函数实现思路:
     1. 修改等待在条件变量的等待队列上的进程计数
     2. 释放锁
     3. 将本身等待在条件变量上
     4. 被唤醒,修正等待队列上的进程计数
   */ 
    cvp->count++;                  //条件变量中睡眠的进程数量加 1
    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 减一,表示等待此条件的睡眠进程个数少了一个,可继续执行了!

关于条件变量机制的实现主要位于 monitor.c 文件中的 cond_signal, cond_wait 两个函数中,这两个函数的含义分别表示提醒等待在这个条件变量上的进程恢复执行,以及等待在这个条件变量上,直到有其余进行将其唤醒位置,所以对上述这些函数分析总结以下:

cond_signal:将指定条件变量上等待队列中的一个线程进行唤醒,而且将控制权转交给这个进程;具体执行流程为:

  • 判断当前的条件变量的等待队列上是否有正在等待的进程,若是没有则不须要进行任何操做;
  • 若是由正在等待的进程,则将其中的一个唤醒,这里的等待队列是使用了一个信号量来进行实现的,因为信号量中已经包括了对等待队列的操做,所以要进行唤醒只须要对信号量执行 up 操做便可;
  • 接下来当前进程为了将控制权转交给被唤醒的进程,将本身等待到了这个条件变量所述的管程的next信号量上,这样的话就能够切换到被唤醒的进程了;因为 next 信号量的实现,就带来了两个困惑:
    • 等待在 next 信号量上的进程是否可以被唤醒?因为每个 next 信号量上等待的进程的产生一定是由于存在了某个它须要唤醒的进程,而这个进程在结束 cond_wait 函数以后返回到管程的函数,还会检查 next 信号量上是否存在等待着的进程,有的话将其唤醒,所以每个 next 信号量上等待的进程最终一定会被唤醒;
    • 在等待在 next 信号量上的时候,管程的 mutex 锁并无被释放,是否可能存在该锁永远都被释放不了的状况?不会的。根据前一个问题得知全部 next 信号量上的等待进程必定会被唤醒,那么最后一个被唤醒的 next 进程就会将锁释放掉;
  • 接下来,当前进程被从 next 信号量上被唤醒的时候,首先将 next count 减一,而后离开 cond_signal 函数,回到管程中的函数,检查是否应该释放管程的锁(取决于如今是否还有 next 信号量上等待的进程,有的话将其唤醒,完成其在函数中的操做,而且将释放锁的操做延迟给这个进程来进行),根据上述描述,咱们能够知道在管程中可以运行的进程之间不会有互相有意料外的打断的过程(因为进程的切换时机都是固定好的,由当前的进程来唤醒另外某一个进程),所以实现了对共享变量访问的互斥性;

cond_wait:该函数的功能为将当前进程等待在指定信号量上,其操做过程为将等待队列的计数加1,而后释放管程的锁或者唤醒一个next上的进程来释放锁(不然会形成管程被锁死没法继续访问,同时这个操做不能和前面的等待队列计数加1的操做互换顺序,要不不能保证共享变量访问的互斥性),而后把本身等在条件变量的等待队列上,直到有signal信号将其唤醒,正常退出函数;

关于使用条件变量来完成哲学家就餐问题的实现中,总共有两个关键函数,以及使用到了 N(哲学家数量)个条件变量,在管程中,还包括了一个限制管程访问的锁还有 N 个用于描述哲学家状态的变量(总共有 EATING, THINKING, HUNGER)三种状态;

首先分析 phi_take_forks_condvar 函数的实现,该函数表示指定的哲学家尝试得到本身所须要进餐的两把叉子,若是不能得到则阻塞,具体实现流程为:

  • 给管程上锁;
  • 将哲学家的状态修改成 HUNGER;
  • 判断当前哲学家是否有足够的资源进行就餐(相邻的哲学家是否正在进餐);
  • 若是可以进餐,将本身的状态修改为 EATING,而后释放锁,离开管程便可;
  • 若是不能进餐,等待在本身对应的条件变量上,等待相邻的哲学家释放资源的时候将本身唤醒;

而 phi_put_forks_condvar 函数则是释放当前哲学家占用的叉子,而且唤醒相邻的由于得不到资源而进入等待的哲学家:

  • 首先获取管程的锁;
  • 将本身的状态修改为 THINKING;
  • 检查相邻的哲学家是否在本身释放了叉子的占用以后知足了进餐的条件,若是知足,将其从等待中唤醒(使用 cond_signal);
  • 释放锁,离开管程;
  • 因为限制了管程中在访问共享变量的时候处于 RUNNABLE 的进程只有一个,所以对进程的访问是互斥的;而且因为每一个哲学家只可能占有全部须要的资源(叉子)或者干脆不占用资源,所以不会出现部分占有资源的现象,从而避免了死锁的产生;
  • 根据上述分析,可知最终一定全部哲学将都能成功就餐;

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

发如今本实验中管程的实现中互斥访问的保证是彻底基于信号量的,也就是若是按照上文中的说明使用 syscall 实现了用户态的信号量的实现机制,那么就彻底能够按照相同的逻辑在用户态实现管程机制和条件变量机制;

固然也能够仿照用户态实现条件变量的方式,将对访问管程的操做封装成系统调用;

异同点为:

  • 相同点:基本的实现逻辑相同;
  • 不一样点:最终在用户态下实现管程和条件变量机制,须要使用到操做系统使用系统调用提供必定的支持; 而在内核态下实现条件变量是不须要的;

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

可以基于信号量来完成条件变量机制;事实上在本实验中就是这么完成的,只须要将使用信号量来实现条件变量和管程中使用的锁和等待队列便可。

最终的实验结果以下图所示:

make_grade

若是 make grade 没法满分,尝试注释掉 tools/grade.sh 的 221 行到 233 行(在前面加上“#”)。

这里咱们选用古老的编辑器 Vim,具体操做过程以下:

  • 一、首先按 esc 进入命令行模式下,按下 :221 跳转至 221 行;
  • 二、按下 Ctrl + v,进入列(也叫区块)模式;
  • 三、在行首使用上下键选择须要注释的多行(221~233 行);
  • 四、按下键盘(大写)“I”键,进入插入模式;
  • 五、而后输入注释符(“//”、“#”等);
  • 六、最后按下“Esc”键。

扩展练习

Challenge 1 :在ucore中实现简化的死锁和重入探测机制

待完成。。。

Challenge 2 :参考Linux的RCU机制,在ucore中实现简化的RCU机制

待完成。。。

参考资料

相关文章
相关标签/搜索