您的位置:首页 > 其它

中科大信息安全操作系统课程lab7实验报告

2018-01-15 22:10 162 查看

操作系统lab7实验报告

PB15051157 茹思淞

练习0:填写已有实验

根据原有的实验代码,我们只需要进行普通的粘贴复制即可完成该练习,因此在这里不再赘述。

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

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

首先,要完成本练习就必须要理解什么是哲学家就餐问题:

哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子。

哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)。即使没有死锁,也有可能发生资源耗尽。例如,假设规定当哲学家等待另一只餐叉超过五分钟后就放下自己手里的那一只餐叉,并且再等五分钟后进行下一次尝试。这个策略消除了死锁(系统总会进入到下一个状态),但仍然有可能发生“活锁”。如果五位哲学家在完全相同的时刻进入餐厅,并同时拿起左边的餐叉,那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉。

在实际的计算机问题中,缺乏餐叉可以类比为缺乏共享资源。一种常用的计算机技术是资源加锁,用来保证在某个时刻,资源只能被一个程序或一段代码访问。当一个程序想要使用的资源已经被另一个程序锁定,它就等待资源解锁。当多个程序涉及到加锁的资源时,在某些情况下就有可能发生死锁。例如,某个程序需要访问两个文件,当两个这样的程序各锁了一个文件,那它们都在等待对方解锁另一个文件,而这永远不会发生。

—-来自百度百科

通过以上的详细描述,我们可以知道该问题是一个典型的操作系统并发控制的典型问题,如何正确地使用信号量和管程成为了解决该问题的关键之处。

下面我们来详细介绍内核级信号量的实现过程,在实验代码中我们很容易找到了在
\kern\sync
下的一系列代码,其中我们在
sem.h
找到了如下代码:

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

void sem_init(semaphore_t *sem, int value);
void up(semaphore_t *sem);
void down(semaphore_t *sem);
bool try_down(semaphore_t *sem);


该代码主要表示了内核级信号量的结构体以及相关的一些操作,主要包括了以下内容:

信号量的结构体
semaphore_t
主要包含了两个部分,当前信号量的值为
value
以及等待队列
wait_queue


信号量的相关操作:信号量的初始化
sem_init
,信号量增
up
,信号量减
down
以及
try_down
操作。

下面我们详细分析以上提到的三个主要操作
sem_init
,
up
,
down
,其运行的过程均在每一步均在注释中给出了详细的分析。

sem_init
信号量初始化

void
sem_init(semaphore_t *sem, int value) {
sem->value = value; //给信号量赋初值,代表可用的资源总数
wait_queue_init(&(sem->wait_queue));//初始化信号量的等待队列
}


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

void
up(semaphore_t *sem) {
__up(sem, WT_KSEM);//调用_up()函数
}

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);//重新开中断
}




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

void
down(semaphore_t *sem) {
uint32_t flags = __down(sem, WT_KSEM);
assert(flags == 0);
}

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 --;//分配资源给该进程,并且信号量值减一
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);//等待被up_操作唤醒,然后从等待队列中移除
local_intr_restore(intr_flag);

if (wait->wakeup_flags != wait_state) {
return wait->wakeup_flags;//非等待状态返回唤醒
}
return 0;
}


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

首先,用户态的进行/线程的信号量的数据结构和内核级的是一样的。

对于用户态的线程/进程使用信号量机制,应该首先通过系统调用进行sem的初始化,设置sem.value以及sem.wait_queue,而在初始化之后,在使用这个信号量时,通过P操作与V操作,也是通过系统调用进入到内核中进行处理,简称是否等待或者释放资源。

不同的地方在于,用户态使用信号量时,需要进行系统调用进入到内核态进行操作,并且对于用户进程,可能会涉及到多个内核临界区,因此需要多个内核信号量进行并发控制。

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

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

首先,理解该题中提到的管程机制:

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

管程相当于一个隔离区,它把共享变量和对它进行操作的若干个过程围了起来,所有进程要访问临界资源时,都必须经过管程才能进入,而管程每次只允许一个进程进入管程,从而需要确保进程之间互斥。

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

1、管程内部的共享变量;

2、管程内部的条件变量;

3、管程内部并发执行的进程;

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

所谓条件变量,即将等待队列和睡眠条件包装在一起,就形成了一种新的同步机制,称为条件变量。个条件变量CV可理解为一个进程的等待队列,队列中的进程正等待某个条件C变为真。

每个条件变量关联着一个断言
Pc
。当一个进程等待一个条件变量,该进程不算作占用了该管程,因而其它进程可以进入该管程执行,改变管程的状态,通知条件变量CV其关联的断言Pc在当前状态下为真。

因而条件变量两种操作如下:

- wait_cv: 被一个进程调用,以等待断言Pc被满足后该进程可恢复执行. 进程挂在该条件变量上等待时,不被认为是占用了管程。

- signal_cv被一个进程调用,以指出断言Pc现在为真,从而可以唤醒等待断言Pc被满足的进程继续执行。

—-来自于实验代码中的翻译及网络

即管程是一个类似于信号量的东西,主要是包括一个数据结构以及定义在这个数据结构上的相关的操作,其中最区别于信号量的地方即在于条件变量以及条件变量所对应的两个操作,
wait_cv
signal_cv


下面是一个管程的具体实例,在
monitor.h
中可以找到

*   monitor mt {
*     ----------------variable------------------
*     semaphore mutex;
*     semaphore next;
*     int next_count;
*     condvar {int count, sempahore sem}  cv
;
*     other variables in mt;
*     --------condvar wait/signal---------------
*     cond_wait (cv) {
*         cv.count ++;
*         if(mt.next_count>0)
*            signal(mt.next)
*         else
*            signal(mt.mutex);
*         wait(cv.sem);
*         cv.count --;
*      }
*
*      cond_signal(cv) {
*          if(cv.count>0) {
*             mt.next_count ++;
*             signal(cv.sem);
*             wait(mt.next);
*             mt.next_count--;
*          }
*       }
*     --------routines in monitor---------------
*     routineA_in_mt () {
*        wait(mt.mutex);
*        ...
*        real body of routineA
*        ...
*        if(next_count>0)
*            signal(mt.next);
*        else
*            signal(mt.mutex);
*     }


下面分析如何基于信号量完成条件变量的实现,打开
monitor.h
中可以看到需要我们填写的伪代码部分,以及题目给出的代码提示,由此,我们可以简单地写出代码:

条件变量cond_signal的实现:

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的实现:

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


下面用管程机制实现哲学家就餐问题,在
check_sync.c
我们看到了两部分需要我们进行如下的补完:

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) {//哲学家没有拿到叉子
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));
}

//---------- philosophers using monitor (condition variable) ----------------------
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;
}


最终得到的实验结果基本无问题

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

同样,在这个问题的关键之处和上面的基本一致。

对于用户态进程来说,其所使用的条件变量机制的具体数据结构和内核级的基本一致,主要的不同之处在于对于用户态进程可能会涉及到多个的内核临界区,因此用户级的条件变量应该是由若干个的内核级条件变量所组成。相应的
wait
signal
操作也要区别具体到每一个内核级的条件变量

实验心得

在本次的实验中,通过对于信号量以及管程的编程,加深了我们在课堂上对于进程同步/并发的理解,主要掌握到的关键点如下:

内核级信号量在实际操作系统中的c语言实现,p/v操作的等价实现。

管程的c语言实现,管程条件变量的具体实现。

哲学家就餐问题的解决思路,以及利用信号量和管程的不同实现方法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息