操作实验七——同步互斥
实验七:同步互斥
一、 实验目的
- 熟悉ucore中的进程同步机制,了解操作系统为进程同步提供的底层支持;
- 在ucore中理解信号量(semaphore)机制的具体实现;
- 理解管程机制,在ucore内核中增加基于管程(monitor)的条件变量(condition variable)的支持;
- 了解经典进程同步问题,并能使用同步机制解决进程同步问题。
二、 实验任务
实验六完成了用户进程的调度框架和具体的调度算法,可调度运行多个进程。如果多个进程需要协同操作或访问共享资源,则存在如何同步和有序竞争的问题。
本次实验,主要是熟悉ucore的进程同步机制—信号量(semaphore)机制,以及基于信号量的哲学家就餐问题解决方案。然后掌握管程的概念和原理,并参考信号量机制,实现基于管程的条件变量机制和基于条件变量来解决哲学家就餐问题。
在kern/sync/check_sync.c中提供了一个基于信号量的哲学家就餐问题解法。同时还需完成练习,即实现基于管程(主要是灵活运用条件变量和互斥信号量)的哲学家就餐问题解法。
三、 实验准备
(一) 底层支撑
1. 开关中断
根据操作系统原理的知识,我们知道如果没有在硬件级保证读内存-修改值-写回内存的原子性,我们只能通过复杂的软件来实现同步互斥操作。但由于有开关中断和原子操作机器指令的存在,使得我们在实现同步互斥原语上可以大大简化。
ucore中提供的底层机制包括中断开关控制和test_and_set相关原子操作机器指令。sync.c中实现的开关中断的控制函数local_intr_save(x)和local_intr_restore (x),它们是基于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);
2. 睡眠机制
在课程中提到用户进程或内核线程可以转入休眠状态以等待某个特定事件,当该事件发生时这些进程能够被再次唤醒。
ucore内核实现这一功能的一个底层支撑机制就是等待队列(wait queue),等待队列和每一个事件(睡眠结束、时钟到达、任务完成、资源可用等)联系起来。需要等待事件的进程在转入休眠状态后插入到等待队列中。当事件发生之后,内核遍历相应等待队列,唤醒休眠的用户进程或内核线程,并设置其状态为就绪状态,并将该进程从等待队列中清除。
ucore在kern/sync/{ wait.h, wait.c }中实现了wait结构和wait queue结构以及相关函数),这是实现ucore中的信号量机制和条件变量机制的基础,进入wait queue的进程会被设为睡眠状态,直到他们被唤醒。
(二) 数据结构
在理解ucore的同步互斥实现方式之前需要首先理解实现相关的数据结构;
1. 信号量
信号量是一种同步互斥机制的实现,普遍存在于现在的各种操作系统内核里。相对于spinlock 的应用对象,信号量的应用对象是在临界区中运行的时间较长的进程。等待信号量的进程需要睡眠来减少占用 CPU 的开销。
当多个(>1)进程可以进行互斥或同步合作时,一个进程会由于无法满足信号量设置的某条件而在某一位置停止,直到它接收到一个特定的信号(表明条件满足了)。
为了发信号,需要使用一个称作信号量的特殊变量。为通过信号量s传送信号,信号量的V操作采用进程可执行原语semSignal(s);为通过信号量s接收信号,信号量的P操作采用进程可执行原语semWait(s);如果相应的信号仍然没有发送,则进程被阻塞或睡眠,直到发送完为止。
信号量的数据结构定义如下:
- P操作函数down(semaphore_t *sem)
__down(semaphore_t *sem, uint32_t wait_state, timer_t *timer): - V操作函数 up(semaphore_t *sem)
__up(semaphore_t *sem, uint32_t wait_state):
2. 管程与条件变量
管程是为了将对共享资源的所有访问及其所需要的同步操作集中并封装起来。
定义:一个管程定义了一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据。
局限在管程中的数据结构,只能被局限在管程的操作过程所访问,任何管程之外的操作过程都不能访问它;另一方面,局限在管程中的操作过程也主要访问管程内的数据结构。所有进程要访问临界资源时,都必须经过管程才能进入,而管程每次只允许一个进程进入管程,从而需要确保进程之间互斥。
但是还需要保证进程间的同步(即进程一定的执行顺序),也就是进程可能需要等待某个条件C为真才能继续执行。为此,引入条件变量(简称CV)。**一个条件变量CV可理解为一个进程的等待队列,队列中的进程正等待某个条件C变为真。**每个条件变量关联着一个断言 "断言 (程序)"Pc。当一个进程等待一个条件变量,该进程不算作占用了该管程,因而其它进程可以进入该管程执行,改变管程的状态,通知条件变量CV其关联的断言Pc在当前状态下为真。
条件变量的数据结构condvar_t定义如下:
管程的数据结构monitor_t定义如下:
管程中的成员变量信号量next和整形变量next_count是配合进程对条件变量cv的操作而设置的,这是由于发出signal_cv的进程A会唤醒睡眠进程B,进程B执行会导致进程A睡眠,直到进程B离开管程,进程A才能继续执行,这个同步过程是通过信号量next完成的;而next_count表示了由于发出singal_cv而睡眠的进程个数。
(三) 相关函数改进
在实验指导书的练习0中提到了注意点如下:
注意:为了能够正确执行lab6的测试应用程序,可能需对已完成的实验1/2/3/4/5/6的代码进行进一步改进。
根据试验要求,我们需要对部分代码进行改进,这里需要改进的地方的代码仅有一处;
trap.c()函数
分析run_timer_list`()函数:
它的功能是更新当前系统时间点,遍历当前所有处在系统管理内的计时器,找出所有应该激活的计数器,并激活它们。该过程在且只在每次计时器中断时被调用。在ucore 中,其还会调用调度器事件处理程序。
这里涉及到了计时器的相关功能;
计时器是其中一个基础而重要的功能.它提供了基于时间事件的调度机制。在ucore 中,timer 中断(irq0)给操作系统提供了有一定间隔的时间事件,操作系统将其作为基本的调度和计时单位。
一个 timer_t 在系统中的存活周期可以被描述如下:
- timer_t 在某个位置被创建和初始化,并通过 add_timer加入系统管理列表中;
- 系统时间被不断累加,直到 run_timer_list 发现该 timer_t到期;
- run_timer_list更改对应的进程状态,并从系统管理列表中移除该timer_t。
四、 实验步骤
(一) 练习0:填写已有实验
lab7会依赖lab1lab6,我们需要把做的lab1lab6的代码填到lab7中缺失的位置上面。练习0 就是—个工具的利用。这里我使用的是linux的Meld工具。和lab6操作流程—样,我们只需要将已经完成的lab6与待完成的lab7分别导入进来,然后点击compare即可,详细细节不再赘述。需要修改的主要是以下六个文件:proc.c、default_pmm.c、pmm.c、swap_fifo.c 、vmm.c、trap.c、sche.c
练习0提到的对已做实验的改进已经在上面的准备部分完成。
(二) 练习1: 理解基于内核级信号量的实现和基于内核级信号量的哲学家就餐问题
哲学家就餐问题描述如下:
有五个哲学家,他们的生活方式是交替地进行思考和进餐。哲学家们公用一张圆桌,周围放有五把椅子,每人坐一把。在圆桌上有五个碗和五根筷子,当一个哲学家思考时,他不与其他人交谈,饥饿时便试图取用其左、右最靠近他的筷子,但他可能一根都拿不到。只有在他拿到两根筷子时,方能进餐,进餐完后,放下筷子又继续思考。
1. 信号量的具体实现
内核级信号量的数据结构在上面已经提到了,接下来详细的分析它的实现(主要是V操作与P操作对用的down和up);
1.1. P操作函数(__down函数)
首先关掉中断,然后判断当前信号量的value是否大于0。如果是>0,则表明可以获得信号量,故让value减一,并打开中断返回即可;如果不是>0,则表明无法获得信号量,故需要将当前的进程加入到等待队列中,并打开中断,然后运行调度器选择另外一个进程执行。如果被V操作唤醒,则把自身关联的wait从等待队列中删除(此过程需要先关中断,完成后开中断)。
1.2. V操作函数(__up函数)
首先关中断,如果信号量对应的wait queue中没有进程在等待,直接把信号量的value加一,然后开中断返回;如果有进程在等待且进程等待的原因是semophore设置的,则调用wakeup_wait
函数将waitqueue中等待的第一个wait删除,且把此wait关联的进程唤醒,最后开中断返回。
我们可以看出信号量的计数器value具有有如下性质:
value>0,表示共享资源的空闲数
vlaue<0,表示该信号量的等待队列里的进程数
value=0,表示等待队列为空
2. 基于内核信号量的哲学家就餐问题实现
上面分析了内核级信号量的具体实现,接下来分析哲学家就餐问题的实现方式;
解决哲学家就餐问题需要创建与之相对应的内核线程,而所有内核线程的创建都离不开 proc_init
函数,而proc_init
函数创建了0号idle
线程、1号init
线程,此时我们需要去寻找在lab4中讨论过的地方init
线程的init_main
函数(创建新的进程,在lab6中为用户进程),init_main
函数会调用check_sync
函数进行哲学家问题的检查,也就是我们的总入口:
而check_sync函数定义在kern/sync/check_sync.c中
,分析如下:
通过观察函数的注释,这个check_sync
函数被分为了两个部分,第—部分使用了信号量来解决哲学家就餐问题,第二部分则是使用管程的方法。因此,练习 1 中我们只需要关注前半段。
利用 kernel_thread
函数创建了—个哲学家就餐问题的内核线程(kernel_thread函数是lab3中学习过的函数,用于创建内核线程
),函数原型:
int kernel_thread(int (*fn)(void *), void *arg, uint32_t clone_flags)
简单的来说,这个函数需要传入三个参数:
- 第—个 fn 是—个函数,代表这个创建的内核线程中所需要执行的函数;
- 第二个 arg 是相关参数,这里传入的是哲学家编号 i;
- 第三部分是共享内存的标记位,内核线程之间内存是共享的,因此应该设置为 0。
接下来,让我们来分析需要创建的内核线程去执行的目标函数,即第一个参数;
philosopher_using_semaphore(kern/sync/check_sync.c)
分析:
传入参数 *arg,代表在上—个函数中“参数”部分定义的 (void *)i,是哲学家的编号;
iter++<TIMES
,表示循环 4 次,目的在于模拟多次试验情况;
从这个函数,哲学家需要思考—段时间,然后吃—段时间的饭,这里面的“—段时间”就是通过内核线程调用do_sleep,然后这个线程休眠指定的时间,从某种方面模拟了吃饭和思考的过程。
这里的do_sleep
实现就是使用了上面提到的计时器来实现的,利用了timer_init
定时器函数,去记录指定的时间(传入的参数time,宏定义为10),且在这个过程中,将进程的状态设置为睡眠,调用函数 add_timer 将绑定该进程的计时器加入计时器队列。
因此,哲学家最关键的两个函数为:phi_take_forks_sema(i) 和 phi_take_forks_sema(i)
;
它们分别实现了同时拿起两个叉子与放下两个叉子。
参数i表示当前哲学家的编号;
int state_sema[N]; 记录每个人状态的数组;
semaphore_t mutex; 表示临界区互斥信号量;
#define HUNGRY 1 哲学家想取得叉子 semaphore_t s[N]; 每个哲学家一个信号量
#define LEFT (i-1+N)%N i的左邻号码
#define RIGHT (i+1)%N i的右邻号码
#define THINKING 0 哲学家正在思考
down 和 up 操作对应着上面提到的P操作与V操作,它们是通过直接调用__down、__up函数实现的,这两个函数已经分析过,它们实现了“锁”的获得与释放机制。
上述的phi_take_forks_sema、put_put_forks_sema
函数中都是通过down、up函数来实现临界区的互斥,此外它们都调用了phi_test_sema
函数,该函数如下:
- 在试图获得筷子的时候,函数的传入参数为 i,即为哲学家编号,此时,他自已为 HUNGRY,而且试图检查旁边两位是否都在吃。如果都不在吃,那么可以获得 EATING 的状态。
- 在从吃的状态返回回到思考状态的时候,需要调用两次该函数,传入的参数为当前哲学家左边和右边的哲学家编号,因为他试图唤醒左右邻居,如果左右邻居满足条件,那么就可以将他们设置为EATING状态。
因此,通过分析代码实现,我们可以将哲学家就餐问题描述如下:
- 内核线程initproc执行init_main,init_main执行check_sync,check_sync内部测试哲学家就餐问题的解决方案。
- check_sync分为两部分,初始化解决哲学家就餐问题需要用到的信号量和条件变量。现在只关注信号量部分。首先调用sem_init初始化信号量,将信号量的value设置为1、wq初始化为空,然后调用kernel_thread创建了5个使用信号量的内核线程。
- 这5个使用信号量的内核线程的执行函数都是philosopher_using_semaphore。在philosopher_using_semaphore开头首先打印哲学家ID,然后进行4次循环,即进行4次思考和吃饭。首先哲学家思考10个ticks,这通过调用do_sleep来模拟。do_sleep首先将当前进程的state设置为SLEEPING,然后为进程创建一个expires为10 ticks的定时器,并添加到定时器列表中,最后调用schedule让出CPU给其他进程。
- 10个ticks过去后,思考完毕,哲学家调用phi_take_forks_sema尝试拿起两把叉子。为了保护state_sema和s被互斥访问,设置了互斥量mutex。因此, phi_take_forks_sema对mutex执行down操作,进入临界区,然后将自己的state_sema标记为HUNGRY,接着调用phi_test_sema来检查是否能拿到两只叉子。若能拿到,则将自己的state_sema标记为EATING,然后离开临界区,开始吃饭,时间同样为10个ticks;若不能拿到,则通过对s[i]执行down操作而堵塞。
- 10个ticks过去后,吃饭完毕,哲学家调用phi_put_forks_sema把两把叉子同时放回桌子。同样,为了保护state_sema和s被互斥访问,phi_put_forks_sema先对mutex执行down操作,进入临界区,然后将自己的state_sema标记为THINKING,接着两次调用phi_test_sema来检查左右邻居是否能进餐。若能就餐,则对s[i]执行up操作,这时会唤醒其他哲学家。等到哲学家离开临界区、进入下一次的思考后,会发生进程切换,使得其他哲学家可以就餐。
3. 问题简述
3.1. 内核级信号量的设计描述
实现了内核级信号量机制的函数均定义在 sem.c 中,上面也已经分析了具体的实现方法,因此对上述这些函数分析总结如下:
sem_init:对信号量进行初始化的函数,根据在原理课上学习到的内容,信号量包括了等待队列 和—个整型数值变量,该函数只需要将该变量设置为指定的初始值,并且将等待队列初始化即可;
__up:对应到了原理课中提及到的 V 操作,表示释放了—个该信号量对应的资源,如果有等待在了这个信号量上的进程,则将其唤醒执行;结合函数的具体实现可以看到其采用了禁用中断的方式 来保证操作的原子性;
__down:对应到了原理课中提及的P操作,表示请求—个该信号量对应的资源,同样采用了禁用中断的方式来保证原子性;
up, down:对__up、__down函数的简单封装;
3.2. 用户态进程/线程提供信号量机制的设计方案,说明给内核级提供信号量机制的异同
参考POSIX信号量实现机制:
用户态进程、线程的信号量机制依旧需要内核态的信号量机制支持,因此在内核部分沿用上面给出的内核态信号量实现。为了使用户态进程/线程可以调用内核态的信号量实现,需要添加相应的系统调用接口,主要包括以下几个:
- sem_open:打开或创建一个信号量并返回一个句柄以供后续调用使用,如果这个调用会创建信号量的话还会对所创建的信号量进行初始化。创建信号量时,将该信号量放置在内核态的一段共享内存中,可以供所有进程调用。
- sem_post和sem_wait:P操作和V操作接口。
- sem_getvalue:获取信号量当前的值。
- sem_close:删除调用进程与它之前打开的一个信号量之间的关联关系。
- sem_unlink:删除一个信号量名字并将其标记为在所有进程关闭该信号量时删除该信号量。
内核态信号量实现和用户态信号量实现的区别:
①内核态信号量可以直接调用内核的服务,而用户态信号量需要通过系统调用接口调用内核态的服务,涉及到栈切换等等。
②内核态信号量存储在内核态的内核栈上,而用户态信号量存储在内核中一段共享内存中。
(三) 练习2: 完成内核级条件变量和基于内核级条件变量的哲学家就餐问题
首先掌握管程机制,然后基于信号量实现完成条件变量实现,然后用管程机制实现哲学家就餐问题的解决方案(基于条件变量)。
1. 简单分析条件变量
上面已经初步分析了管程与条件变量的结构体了,下面接着分析它的主要操作init、wait、signal;
1.1. monitor_init
这里初始化一个管程mtp,该管程具有num_cv个条件变量;
1.2. monitor_init
管程wait操作:
先将因为条件不成立而睡眠的进程计数加1
分支1. 当管程的 next_count 大于0说明有进程睡在了signal 操作上,将其唤醒
分支2. 当管程的 next_count 小于0说明当前没有进程睡在 signal 操作数 只需要释放互斥体然后再将自身阻塞等待条件变量的条件为真被唤醒后,将条件不成立而睡眠的进程计数减1,因为现在成立了
如果进程 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减—,表示等待此条件的睡眠进程个数少了—个,可继续执行了!
1.3. monitor_signal
首先判断 cvp.count,如果不大于 0,则表示当前没有睡眠在这—个条件变量上的进程,因此就没有被唤醒的对象了,直接函数返回即可,什么也不需要操作。
如果大于0,这表示当前有睡眠在该条件变量上的进程,因此需要唤醒等待在cv.sem上睡眠的进程。而由于只允许—个进程在管程中执行,所以—旦进程 B 唤醒了别人(进程A),那么自已就需要睡眠。故让 monitor.next_count 加—,且让自已(进程B)睡在信号量 monitor.next(宿主管程的信号量) 上。如果睡醒了,这让 monitor.next_count 减—。
最后一句这说明上—句中的 down 的进程睡醒了,那么睡醒,就必然是另外—个进程唤醒了它,因为只能有—个进程在管程中被 signal,如果有进程调用了wait,那么必然需要 signal 另外—个进程。
从wait可以看出是调用wait的时候会判断next信号量上有没有进程睡着,因此这里唤醒因为signal而睡在next信号量上的进程的工作是通过wait实现的。
我们可以从下图可以看到这—调用过程:
2. 哲学家就餐问题基于条件变量的实现
这里哲学家就餐问题的实现位于check_sync函数的剩余部分:
int state_condvar[N]; //饥饿、思考中、就餐中三种状态
Kernel_thread创建进程,第—个 fn 是—个函数,代表这个创建的内核线程中所需要执行的函数,此时为使用条件变量的函数,我们对它进行分析;
该函数与我们的信号量实现的哲学家就餐问题的函数一致,但是拿起叉子与放下叉子的函数调用更换为条件变量实现的phi_put_forks_condvar、phi_take_forks_condvar
;
对这两个主要函数进行分析:
phi_take_forks_condvar:
首先分析 phi_take_forks_condvar 函数的实现,该函数表示指定的哲学家尝试获得自已所需要进餐的两把叉子,如果不能获得则阻塞,具体实现流程为:
①给管程上锁,进入管程;
②将哲学家的状态修改为 HUNGER;
③判断当前哲学家是否有足够的资源进行就餐(相邻的哲学家是否正在进餐);如果能够进餐,将自已的状态修改成EATING,然后释放锁,离开管程即可,退出时需要特别注意查看next信号量上有无睡着的进程,防止发出signal的进程一直睡眠;
④如果不能进餐,等待在自已对应的条件变量上,等待相邻的哲学家释放资源的时候将自已唤醒;
phi_put_forks_condvar:
phi_put_forks_condvar
函数则是释放当前哲学家占用的叉子,并且唤醒相邻的因为得不到资源而进入等待的哲学家:
①首先获取管程的锁;
②将自已的状态修改成 THINKING;
③检查相邻的哲学家是否在自已释放了叉子的占用之后满足了进餐的条件,如果满足,将其从等待中唤醒(使用cond_signal);
④释放锁,离开管程(退出时需要特别注意查看next信号量上有无睡着的进程);
上面两个函数的检查部分都是通过调用phi_test_condvar
函数,分析如下:
如果此时哲学家为饥饿状态,并且左右两个叉子都没被使用,那么唤醒该进程获得该条件变量,即哲学家可以吃东西现在是eating状态,获得了两个叉子。
由于限制了管程中在访问共享变量的时候处于RUNNABLE 的进程只有—个,因此对进程的访问是互斥的;并且由于每个哲学家只可能占有所有需要的资源(叉子)或者干脆不占用资源,因此不会出现部分占有资源的现象,从而避免了死锁的产生;根据上述分析,可知最终必定所有哲学将都能成功就餐。
3. 问题分析
3.1. 用户态进程/线程提供条件变量机制的设计方案
(参考自POSIX的条件变量接口)
-
方法一:
本实验中管程的实现中互斥访问的保证是完全基于信号量的,也就是如果按照上文中的说明使用syscall 实现了用户态的信号量的实现机制,那么就完全可以按照相同的逻辑在用户态实现管程机制和条件变量机制; -
方法二:
当然也可以仿照用户态实现条件变量的方式,将对访问管程的操作封装成系统调用;
用户态进程、线程的条件变量机制依旧需要内核态的条件变量机制支持,因此在内核部分沿用上面给出的内核态条件变量实现。为了使用内核态条件变量的服务,增加以下几个系统调用接口:cond_init:创建条件变量,需要先初始化,并将该条件变量放置在内核态的一段共享内存中,可以供所有进程调用。
cond_wait和cond_signal:wait和signal操作的接口。
cond_broadcast:唤醒所有的的等待进程。
cond_destroy:删除一个条件变量。
3.2. 内核态条件变量实现和用户态条件变量实现的区别
基本的实现逻辑相同;内核态条件变量存储在内核态的内核栈上,而用户态条件变量存储在内核中一段共享内存中。
内核态条件变量可以直接调用内核的服务,而用户态条件变量需要通过系统调用接口调用内核态的服务,涉及到栈切换等等。最终在用户态下实现管程和条件变量机制,需要使用到操作系统使用系统调用提供—定的 支持; 而在内核态下实现条件变量是不需要的;
4. 运行结果
这里出了与lab5、6相同的错误,相同的处理方式就是注释掉kern/process/pro.c中的:
这里的ucore出现的问题在lab5中分析过了,它就是页的回收没有实现完全,是ucore自己的一些小bug。
最终可以成功得到结果:
运行正确,基本实现无误。
五、 实验总结
这次试验算是比较简单,它主要就是在ucore中实现并掌握内核级信号量以及自己手工实现管程与内核级条件变量以及分别基于两者理解或者实现哲学家就餐问题。
书本上详细的介绍了锁(也就是这里的信号量)与条件量,只有管程是首次接触。管程理解起来十分简单,它就是将我们的临界区代码给“围”起来,也就是说使用一个数据结构将对共享资源的所有访问及其所需要的同步操作集中并封装起来,所有进程要访问临界资源时,都必须经过管程才能进入,而管程每次只允许一个进程进入管程,从而需要确保进程之间互斥。
这里信号量与条件变量的实现都及其依赖于队列,具体来说是ucore的睡眠机制依赖于队列实现,信息量的实现较为简单,与书上的锁实现类似,而条件变量稍微不同,因为它涉及到了管程的概念,也因此比书上的实现方式复杂的多,条件变量的实现是基于信号量来扩展的,比书上多加了next信号量,它在ucore中是更加符合同步的条件的,因此理解管程与条件变量就是本次实验的重点,上面也都具体分析了一遍。
总的来说经过几次实验之后对ucore的基本的同步互斥实现有了个清晰的认识。