Lab7

Lab7 report

练习0:填写已有实验

用meld对比修改了以下文件:

kdebug.c
trap.c
default_pmm.c
pmm.c
swap_fifo.c
vmm.c
proc.c
default_sched.c

其中需要对trap.c中以前实验完成的部分做如下改动:

static void
trap_dispatch(struct trapframe *tf) {
    ......
    ticks ++;
        assert(current != NULL);
        //default_sched_class.proc_tick(current->rq, current);
        run_timer_list();
        break;
    ......
}

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

分析

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

个人完成的lab6和练习0完成后的刚修改的lab7之间的区别及lab7采用信号量的执行过程

练习0完成后的刚修改的lab7和lab6对比基本没有大的变化,二者开始的执行流程基本相同,而差异是在创建第二个内核线程,即创建init_main时,在proc.c中可以看到init_main()函数,在开始执行调度之前多执行了一个check_sync函数,init_main()函数发生变化的部分如下所示:

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

check_sync函数如下所示:

void check_sync(void){
    int i;
    //check semaphore
    sem_init(&mutex, 1);
    for(i=0;i

根据注释可知,该函数分为两个部分,第一部分是实现基于信号量的哲学家问题,第二部分是实现基于管程的哲学家问题。
由于练习1要求分析基于信号量的哲学家问题,所以我们先只看前半部分。
先初始化了一个互斥信号量,然后创建了对应5个哲学家行为的5个信号量,并创建5个内核线程代表5个哲学家,每个内核线程需要完成基于信号量的哲学家吃饭睡觉思考行为的实现。
其中比较重要的函数是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++

以上即为lab7中关于哲学家就餐问题的信号量的执行过程。

内核级信号量的设计描述,及其大致执行流流程

根据经典的PV模型:

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 */;
}
}```
观察sem.h和sem.c文件。
sem.h中关于信号量的结构体定义为:

typedef struct {
int value;
wait_queue_t wait_queue;
} semaphore_t;

和PV模型中semaphore结构体的设计一模一样。
而在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)```函数,二者的具体实现描述如下:

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);//关联的wait从等待队列中删除
local_intr_restore(intr_flag);

if (wait->wakeup_flags != wait_state) {
    return wait->wakeup_flags;
}
return 0;

}

static __noinline void __up(semaphore_t *sem, uint32_t wait_state) {
bool intr_flag;
local_intr_save(intr_flag);
{
wait_t *wait;
//信号量对应的wait queue中没有进程在等待
if ((wait = wait_queue_first(&(sem->wait_queue))) == NULL) {
sem->value ++;//信号量的value加一
}
else {
assert(wait->proc->wait_state == wait_state);
//将waitqueue中等待的第一个wait删除
wakeup_wait(&(sem->wait_queue), wait, wait_state, 1);
}
}
local_intr_restore(intr_flag);
}

#### 给用户态进程/线程提供信号量机制的设计方案,及给内核级提供信号量机制的异同
用户态的进行/线程的信号量的数据结构和内核级的是一样的。 
对于用户态的线程/进程使用信号量机制,应该首先通过系统调用进行sem的初始化,设置sem.value以及sem.wait_queue,而在初始化之后,在使用这个信号量时,通过P操作与V操作,也是通过系统调用进入到内核中进行处理,简称是否等待或者释放资源。 
不同的地方在于,用户态使用信号量时,需要进行系统调用进入到内核态进行操作。

## 练习2: 完成内核级条件变量和基于内核级条件变量的哲学家就餐问题(需要编码)
### 实验思路
管程定义了一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据。 其相当于一个隔离区,把共享变量和对它进行操作的若干个过程围了起来,所有进程要访问临界资源时,都必须经过管程才能进入,而管程每次只允许一个进程进入管程,从而需要确保进程之间互斥。 
将等待队列和睡眠条件包装在一起,就形成了一种新的同步机制,称为条件变量。
在了解以上知识的基础上可以实现管程和条件变量的相关函数cond_signal和cond_wait。在解决哲学家就餐问题中即可以直接调用以上函数。
### 实现过程
#### monitor.c
##### cond_signal()函数

//唤醒睡在条件变量上的线程
void
cond_signal (condvar_t *cvp) {
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));//直接切换回等待条件变量的进程,等其执行完毕,这个线程才能执行 count--
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()函数

//使线程睡在条件变量上
void
cond_wait (condvar_t *cvp) {
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));//释放mutex,是其他进程得到monitor进入管程
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);
}

#### check_sync.c
#####  phi_take_forks_condvar()函数

//拿叉子
void phi_take_forks_condvar(int i) {
down(&(mtp->mutex));//进入管程的临界区
state_condvar[i]=HUNGRY;//改变状态为 HUNGRY
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]);
}
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));//进入管程的临界区
state_condvar[i]=THINKING;
phi_test_condvar(LEFT);//左边的得到叉子
phi_test_condvar(RIGHT);//右边的得到叉子
if(mtp->next_count>0)
up(&(mtp->next));
else
up(&(mtp->mutex));//离开临界区
}

### 实验结果
执行```make qemu```之后得到如下结果:

I am No.4 philosopher_condvar
Iter 1, No.4 philosopher_condvar is thinking
······
I am No.4 philosopher_sema
Iter 1, No.4 philosopher_sema is thinking
······
Iter 1, No.2 philosopher_condvar is eating
······
Iter 1, No.0 philosopher_sema is eating
······
No.2 philosopher_sema quit

### 思考题
#### 给出内核级条件变量的设计描述,并说其大致执行流流程。
条件变量的数据结构如下:

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;

typedef struct monitor{
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
condvar_t *cv; // the condvars in monitor
} monitor_t;

其主要的函数执行流程如下:

// 对条件变量进行初始化
//设置next_count为0,对mutex,next进行初始化
//sem_init(&(mtp->mutex), 1); //unlocked
//并分配num个condvar_t,设置cv的count为0,初始化cv的sem和owner。
void monitor_init (monitor_t *cvp, size_t num_cv);
//唤醒睡在条件变量上的线程
//如果cv的count>0,说明有proc在等待,那么需要唤醒等待在cv.sem上的proc,并使自己进行睡眠,同时monitor.next_count++,在被唤醒后执行monitor.next_count–;
//如果cv的count == 0,说明没有proc在等待cv.sem,直接返回函数。
void cond_signal (condvar_t *cvp);
//使线程睡在条件变量上
//先将cv.count++,如果monitor.next_count >0,说明有进程执行cond_signal()函数并且睡着了,此时唤醒此proc;否则的话,说明目前没有因为执行了cond_signal()函数的proc睡着,此时唤醒因为互斥条件mutex无法进入管程的proc。
//在这之后,使A在cv.sem上进行等待并进行调度,如果A睡醒了,则cv.count–。
void cond_wait (condvar_t *cvp);

#### 请在实验报告中给出给用户态进程/线程提供条件变量机制的设计方案,并比较说明给内核级提供条件变量机制的异同。
用户级别的的管程,通过初始化condvar,然后通过系统调用在用户级别实现管程的处理函数。
不同的地方在于,用户态进程需要系统调用。
#### 能否不用基于信号量机制来完成条件变量?如果不能,请给出理由,如果能,请给出设计说明和具体实现。
我认为是可以的,但是并不需要这样做。
因为条件变量有一些子操作与信号量类似,所以直接使用信号量会使得实现更加得简洁。
如果非要不基于信号量实现的话,则需要把signal和wait等原本需要调用PV操作的地方换成关于管程的实现就行。但是这样的实现代码上会有极多的冗余重复,并且没有太大的意义。

## 和实验参考答案的对比分析
在lab7_result中make grade之后发现得分还没有上100,但是对比monitor.c和check_sync.c函数,发现没有区别,即对于本次需要实现的实验内容,我的答案和实验参考答案没有区别。
我猜想参考答案没有满分的原因是,前几次实验的代码在“迁移”的过程中可能产生了错误或者没有完全“迁移”过来。
## 知识点的分析
考察了信号量(semaphore)机制和管程(monitor)机制。前者的考察是基于分析理解,后者则是需要具体实现。
我认为后者是基于前者的一种扩展,两者在不同的场景可以有相应的应用。
## 知识点的补充
对于知识点的考察挺全面的,没有需要补充的地方。

你可能感兴趣的:(Lab7)