update lab7

This commit is contained in:
yuchen 2015-04-10 00:59:23 +08:00
parent 0bba4377cf
commit c9f2d5e9a8
13 changed files with 279 additions and 131 deletions

1
.gitignore vendored
View File

@ -4,4 +4,5 @@ assets
*.bak
*.org
*~
*.dot
autocover

View File

@ -158,10 +158,12 @@
* [实验内容](lab7/lab7_2_labs.md)
* [练习](lab7/lab7_2_1_exercises.md)
* [项目组成](lab7/lab7_2_2_files.md)
* [同步互斥的设计与实现](lab7/lab7_3_synchronization_implement.md)
* [同步互斥机制的设计与实现](lab7/lab7_3_synchronization_implement.md)
* [实验执行流程概述](lab7/lab7_3_1_experiment.md)
* [计时器的原理和实现](lab7/lab7_3_2_timer_implement.md)
* [同步互斥的底层支撑](lab7/lab7_3_2_synchronization_basic_support.md)
* [同步互斥机制的底层支撑](lab7/lab7_3_2_synchronization_basic_support.md)
* [计时器](lab7/lab7_3_2_1_timer.md)
* [屏蔽与使能中断](lab7/lab7_3_2_2_interrupt.md)
* [等待队列](lab7/lab7_3_2_3_waitqueue.md)
* [信号量](lab7/lab7_3_3_semaphore.md)
* [管程和条件变量](lab7/lab7_3_4_monitors.md)
* [实验报告要求](lab7/lab7_4_lab_requirement.md)

View File

@ -1,6 +1,6 @@
{
"plugins": [
"autocover"
"autocover","grivs"
],
"pluginsConfig": {
"autocover": {
@ -18,6 +18,8 @@
"background": {
"color": "#09F"
}
},
"grivs": {
}
}
}

View File

@ -1,6 +1,7 @@
## 实验目的
* 熟悉ucore中的进程同步机制了解操作系统为进程同步提供的底层支持
* 了解定时器timer机制的实现
* 在ucore中理解信号量semaphore机制的具体实现
* 理解管程机制在ucore内核中增加基于管程monitor的条件变量condition
variable的支持

View File

@ -13,8 +13,8 @@
#### 练习1: 理解内核级信号量的实现和基于内核级信号量的哲学家就餐问题(不需要编码)
完成练习0后建议大家比较一下可用kdiff3等文件比较软件个人完成的lab6和练习0完成后的刚修改的lab7之间的区别分析了解lab7采用信号量的执行过程。执行make
grade大部分测试用例应该通过。
完成练习0后建议大家比较一下可用meld等文件diff比较软件个人完成的lab6和练习0完成后的刚修改的lab7之间的区别分析了解lab7采用信号量的执行过程。执行`make
grade`,大部分测试用例应该通过。
请在实验报告中给出内核级信号量的设计描述,并说其大致执行流流程。
@ -24,20 +24,24 @@ grade大部分测试用例应该通过。
首先掌握管程机制,然后基于信号量实现完成条件变量实现,然后用管程机制实现哲学家就餐问题的解决方案(基于条件变量)。
执行make grade
执行:`make grade`
。如果所显示的应用程序检测都输出ok则基本正确。如果只是某程序过不去比如matrix.c则可执行
```
make run-matrix
命令来单独调试它。大致执行结果可看附录。(使用的是**qemu-1.0.1**)。
```
命令来单独调试它。大致执行结果可看附录。
请在实验报告中给出内核级条件变量的设计描述,并说其大致执行流流程。
请在实验报告中给出给用户态进程/线程提供条件变量机制的设计方案,并比较说明给内核级提供条件变量机制的异同。
请在实验报告中回答:能否不用基于信号量机制来完成条件变量?如果不能,请给出理由,如果能,请给出设计说明和具体实现。
#### 扩展练习 Challenge 实现 Linux 的 RCU
#### 扩展练习 Challenge  参考Linux的RCU机制在ucore中实现简化的RCU机制
在ucore
下实现下Linux的RCU同步互斥机制。可阅读相关Linux内核书籍或查询网上资料可了解RCU的细节然后大致实现在ucore中。下面是一些参考资料
下实现下Linux的RCU同步互斥机制。可阅读相关Linux内核书籍或查询网上资料可了解RCU的设计实现细节然后简化实现在ucore中。
要求有实验报告说明你的设计思路,并提供测试用例。下面是一些参考资料:
* [http://www.ibm.com/developerworks/cn/linux/l-rcu/](http://www.ibm.com/developerworks/cn/linux/l-rcu/)
* [http://www.diybl.com/course/6\_system/linux/Linuxjs/20081117/151814.html](http://www.diybl.com/course/6_system/linux/Linuxjs/20081117/151814.html)

View File

@ -51,9 +51,10 @@
简单说明如下:
* kern/schedule/{sched.h,sched.c}: 增加了定时器timer机制用于进程/线程的do_sleep功能。
* kern/sync/sync.h: 去除了lock实现这对于不抢占内核没用
* kern/sync/wait.[ch]:
了为wait结构和waitqueue结构以及在此之上的函数这是ucore中的信号量semophore机制和条件变量机制的基础在本次实验中你需要了解其实现。
义了等待队列wait_queue结构和等待entry的wait结构以及在此之上的函数这是ucore中的信号量semophore机制和条件变量机制的基础在本次实验中你需要了解其实现。
* kern/sync/sem.[ch]:定义并实现了ucore中内核级信号量相关的数据结构和函数本次试验中你需要了解其中的实现并基于此完成内核级条件变量的设计与实现。
* user/ libs/ {syscall.[ch],ulib.[ch]
}与kern/sync/syscall.c实现了进程sleep相关的系统调用的参数传递和调用关系。

View File

@ -2,11 +2,11 @@
互斥是指某一资源同时只允许一个进程对其进行访问,具有唯一性和排它性,但互斥不用限制进程对资源的访问顺序,即访问可以是无序的。同步是指在进程间的执行必须严格按照规定的某种先后次序来运行,即访问是有序的,这种先后次序取决于要系统完成的任务需求。在进程写资源情况下,进程间要求满足互斥条件。在进程读资源情况下,可允许多个进程同时访问资源。
实验七提供了多种同步互斥手段,包括中断控制、等待队列、信号量、管程机制包含条件变量设计并基于信号量实现了哲学家问题的执行过程。而练习是要求用管程机制实现哲学家问题的执行过程。在实现信号量机制和管程机制时需要让无法进入临界区的进程睡眠为此在ucore中设计了等待队列。当进程无法进入临界区即无法获得信号量可让进程进入等待队列这时的进程处于等待状态也可称为阻塞状态从而会让实验六中的调度器选择一个处于就绪状态即RUNNABLE
实验七设计实现了多种同步互斥手段,包括时钟中断管理、等待队列、信号量、管程机制(包含条件变量设计)等,并基于信号量实现了哲学家问题的执行过程。而本次实验的练习是要求用管程机制实现哲学家问题的执行过程。在实现信号量机制和管程机制时需要让无法进入临界区的进程睡眠为此在ucore中设计了等待队列wait_queue。当进程无法进入临界区即无法获得信号量可让进程进入等待队列这时的进程处于等待状态也可称为阻塞状态从而会让实验六中的调度器选择一个处于就绪状态即RUNNABLE
STATE的进程进行进程切换让新进程有机会占用CPU执行从而让整个系统的运行更加高效。
在实验七中的ucore初始化过程开始的执行流程都与实验六相同直到执行到创建第二个内核线程init\_main时修改了init\_main的具体执行内容即增加了check\_sync函数的调用而位于lab7_figs/kern/sync/check\_sync.c中的check\_sync函数可以理解为是实验七的起始执行点是实验七的总控函数。进一步分析此函数可以看到这个函数主要分为了两个部分第一部分是实现基于信号量的哲学家问题第二部分是实现基于管程的哲学家问题。
对于check\_sync函数的第一部分首先实现初始化了一个互斥信号量然后创建了对应5个哲学家行为的5个信号量并创建5个内核线程代表5个哲学家每个内核线程完成了基于信号量的哲学家吃饭睡觉思考行为实现。这部分是给学生作为练习参考用的。学生可以看看信号量是如何实现的已经如何利用信号量完成哲学家问题。
对于check\_sync函数的第二部分首先初始化了管程然后又创建了5个内核线程代表5个哲学家每个内核线程要完成基于管程的哲学家吃饭睡觉思考行为实现。这部分需要学生来具体完成。学生需要掌握如何用信号量来实现条件变量以及包含条件变量的管程如何能够确保哲学家能够正常思考和吃饭。
对于check\_sync函数的第二部分首先初始化了管程然后又创建了5个内核线程代表5个哲学家每个内核线程要完成基于管程的哲学家吃饭睡觉思考行为实现。这部分需要学生来具体完成。学生需要掌握如何用信号量来实现条件变量,以及包含条件变量的管程如何能够确保哲学家能够正常思考和吃饭。

View File

@ -1,16 +1,16 @@
### 计时器的原理和实现
### 定时器
在传统的操作系统中,时器是其中一个基础而重要的功能.它提供了基于时间事件的调度机制。在ucore 中timer 中断(irq0)给操作系统提供了有一定间隔的时间事件操作系统将其作为基本的调度和计时单位我们记两次时间中断之间的时间间隔为一个时间片timer splice
在传统的操作系统中,时器是其中一个基础而重要的功能.它提供了基于时间事件的调度机制。在ucore 中timer 中断(irq0)给操作系统提供了有一定间隔的时间事件操作系统将其作为基本的调度和计时单位我们记两次时间中断之间的时间间隔为一个时间片timer splice
基于此时间单位,操作系统得以向上提供基于时间点的事件,并实现基于时间长度的等待和唤醒机制。在每个时钟中断发生时,操作系统产生对应的时间事件。应用程序或者操作系统的其他组件可以以此来构建更复杂和高级的调度。
基于此时间单位,操作系统得以向上提供基于时间点的事件,并实现基于时间长度的睡眠等待和唤醒机制。在每个时钟中断发生时,操作系统产生对应的时间事件。应用程序或者操作系统的其他组件可以以此来构建更复杂和高级的进程管理和调度算法
* sched.h, sched.c 定义了有关timer的各种相关接口来使用 timer 服务,其中主要包括:
* typedef struct {……} timer\_t: 定义了 timer\_t 的基本结构,其可以用 sched.h 中的timer\_init函数对其进行初始化。
* void timer\_init(timer t \*timer, struct proc\_struct \*proc, int expires): 对某时器 进行初始化,让它在 expires 时间片之后唤醒 proc
* void timer\_init(timer t \*timer, struct proc\_struct \*proc, int expires): 对某时器 进行初始化,让它在 expires 时间片之后唤醒 proc
进程。
* void add\_timer(timer t \*timer): 向系统添加某个初始化过的timer\_t时器在 指定时间后被激活并将对应的进程唤醒至runnable如果当前进程处在等待状态
* void del\_timer(timer\_t \*time): 向系统删除(或者说取消)某一个计时器。该计时器在取消后不会被系统激活并唤醒进程。
* void run\_timer\_list(void): 更新当前系统时间点,遍历当前所有处在系统管理内的计时器,找出所有应该激活的计数器,并激活它们。该过程在且只在每次计时器中断时被调用。在ucore 中,其还会调用调度器事件处理程序。
* void add\_timer(timer t \*timer): 向系统添加某个初始化过的timer\_t时器在 指定时间后被激活并将对应的进程唤醒至runnable如果当前进程处在等待状态
* void del\_timer(timer\_t \*time): 向系统删除(或者说取消)某一个定时器。该定时器在取消后不会被系统激活并唤醒进程。
* void run\_timer\_list(void): 更新当前系统时间点,遍历当前所有处在系统管理内的定时器,找出所有应该激活的计数器,并激活它们。该过程在且只在每次定时器中断时被调用。在ucore 中,其还会调用调度器事件处理程序。
一个 timer\_t 在系统中的存活周期可以被描述如下:
@ -19,4 +19,4 @@ add\_timer加入系统管理列表中
2. 系统时间被不断累加,直到 run\_timer\_list 发现该 timer\_t到期。
3. run\_timer\_list更改对应的进程状态并从系统管理列表中移除该timer\_t。
尽管本次实验并不需要填充计时器相关的代码,但是作为系统重要的组件(同时计时器也是调度器的一个部分你应该了解其相关机制和在ucore中的实现方法。接下来的实验描述将会在一定程度上忽略计时器对调度带来的影响,即不考虑基于固定时间点的调度
尽管本次实验并不需要填充定时器相关的代码,但是作为系统重要的组件(同时定时器也是调度器的一个部分你应该了解其相关机制和在ucore中的实现方法和使用方法。且在trap_dispatch函数中修改之前对时钟中断的处理使得ucore能够利用定时器提供的功能完成调度和睡眠唤醒等操作

View File

@ -0,0 +1,24 @@
### 屏蔽与使能中断
根据操作系统原理的知识,我们知道如果没有在硬件级保证读内存-修改值-写回内存的原子性我们只能通过复杂的软件来实现同步互斥操作。但由于有开关中断和test\_and\_set\_bit等原子操作机器指令的存在使得我们在实现同步互斥原语上可以大大简化。
在ucore中提供的底层机制包括中断屏蔽/使能控制等。kern/sync.c中实现的开关中断的控制函数local\_intr\_save(x)和local\_intr\_restore(x)它们是基于kern/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);
……
```
由于目前ucore只实现了对单处理器的支持所以通过这种方式就可简单地支撑互斥操作了。在多处理器情况下这种方法是无法实现互斥的因为屏蔽了一个CPU的中断只能阻止本地CPU上的进程不会被中断或调度并不意味着其他CPU上执行的进程不能执行临界区的代码。所以开关中断只对单处理器下的互斥操作起作用。在本实验中开关中断机制是实现信号量等高层同步互斥原语的底层支撑基础之一。

View File

@ -0,0 +1,135 @@
## 等待队列
到目前为止,我们的实验中,用户进程或内核线程还没有睡眠的支持机制。在课程中提到用户进程或内核线程可以转入等待状态以等待某个特定事件(比如睡眠,等待子进程结束,等待信号量等当该事件发生时这些进程能够被再次唤醒。内核实现这一功能的一个底层支撑机制就是等待队列wait_queue等待队列和每一个事件睡眠结束、时钟到达、任务完成、资源可用等联系起来。需要等待事件的进程在转入休眠状态后插入到等待队列中。当事件发生之后内核遍历相应等待队列唤醒休眠的用户进程或内核线程并设置其状态为就绪状态PROC_RUNNABLE并将该进程从等待队列中清除。ucore在kern/sync/{ wait.h, wait.c
}中实现了等待项wait结构和等待队列wait
queue结构以及相关函数这是实现ucore中的信号量机制和条件变量机制的基础进入wait
queue的进程会被设为等待状态PROC_SLEEPING直到他们被唤醒。
### 数据结构定义
```
typedef struct {
struct proc_struct *proc; //等待进程的指针
uint32_t wakeup_flags; //进程被放入等待队列的原因标记
wait_queue_t *wait_queue; //指向此wait结构所属于的wait_queue
list_entry_t wait_link; //用来组织wait_queue中wait节点的连接
} wait_t;
typedef struct {
list_entry_t wait_head; //wait_queue的队头
} wait_queue_t;
le2wait(le, member) //实现wait_t中成员的指针向wait_t 指针的转化
```
### 相关函数说明
与wait和wait queue相关的函数主要分为两层底层函数是对wait queue的初始化、插入、删除和查找操作相关函数如下
```c
void wait_init(wait_t *wait, struct proc_struct *proc); //初始化wait结构
bool wait_in_queue(wait_t *wait); //wait是否在wait queue中
void wait_queue_init(wait_queue_t *queue); //初始化wait_queue结构
void wait_queue_add(wait_queue_t *queue, wait_t *wait); //把wait前插到wait queue中
void wait_queue_del(wait_queue_t *queue, wait_t *wait); //从wait queue中删除wait
wait_t *wait_queue_next(wait_queue_t *queue, wait_t *wait);//取得wait的后一个链接指针
wait_t *wait_queue_prev(wait_queue_t *queue, wait_t *wait);//取得wait的前一个链接指针
wait_t *wait_queue_first(wait_queue_t *queue); //取得wait queue的第一个wait
wait_t *wait_queue_last(wait_queue_t *queue); //取得wait queue的最后一个wait
bool wait_queue_empty(wait_queue_t *queue); //wait queue是否为空
```
高层函数基于底层函数实现了让进程进入等待队列--`wait_current_set`,以及从等待队列中唤醒进程--`wakeup_wait`,相关函数如下:
```
//让wait与进程关联且让当前进程关联的wait进入等待队列queue当前进程睡眠
void wait_current_set(wait_queue_t *queue, wait_t *wait, uint32_t wait_state);
//把与当前进程关联的wait从等待队列queue中删除
wait_current_del(queue, wait);
//唤醒与wait关联的进程
void wakeup_wait(wait_queue_t *queue, wait_t *wait, uint32_t wakeup_flags, bool del);
//唤醒等待队列上挂着的第一个wait所关联的进程
void wakeup_first(wait_queue_t *queue, uint32_t wakeup_flags, bool del);
//唤醒等待队列上所有的等待的进程
void wakeup_queue(wait_queue_t *queue, uint32_t wakeup_flags, bool del);
```
### 调用关系举例
如下图所示,对于唤醒进程的函数`wakeup_wait`可以看到它会被各种信号量的V操作函数up`调用,并且它会调用`wait_queue_del函数和wakup_proc`函数来完成唤醒进程的操作。
```dot
digraph "wakeup_wait" {
graph [bgcolor="#F7F5F3", fontname="Arial", fontsize="10", label="", rankdir="LR"];
node [shape="box", style="filled", color="blue", fontname="Arial", fontsize="10", fillcolor="white", label=""];
edge [color="#CC0044", fontname="Arial", fontsize="10", label=""];
graph [bgcolor="#F7F5F3"];
__N1 [color="red", label="wakeup_wait"];
__N2 [label="wait_queue_del"];
__N3 [label="wakeup_proc"];
__N4 [label="__up"];
__N5 [label="up"];
__N6 [label="phi_test_sema"];
__N7 [label="phi_take_forks_sema"];
__N8 [label="cond_signal"];
__N9 [label="phi_put_forks_sema"];
__N10 [label="cond_wait"];
__N11 [label="unlock_mm"];
__N12 [label="phi_take_forks_condvar"];
__N13 [label="phi_put_forks_condvar"];
__N14 [label="wakeup_first"];
__N15 [label="wakeup_queue"];
__N1 -> __N2;
__N1 -> __N3;
__N6 -> __N5;
__N7 -> __N5;
__N8 -> __N5;
__N9 -> __N5;
__N10 -> __N5;
__N11 -> __N5;
__N12 -> __N5;
__N13 -> __N5;
__N5 -> __N4;
__N4 -> __N1;
__N14 -> __N1;
__N15 -> __N1;
}
```
如下图所示,而对于让进程进入等待状态的函数`wait_current_set`可以看到它会被各种信号量的P操作函数down`调用,并且它会调用`wait_init`完成对等待项的初始化,并进一步调用`wait_queue_add`来把与要处于等待状态的进程所关联的等待项挂到与信号量绑定的等待队列中。
```dot
digraph "wait_current_set" {
graph [bgcolor="#F7F5F3", fontname="Arial", fontsize="10", label="", rankdir="LR"];
node [shape="box", style="filled", color="blue", fontname="Arial", fontsize="10", fillcolor="white", label=""];
edge [color="#CC0044", fontname="Arial", fontsize="10", label=""];
graph [bgcolor="#F7F5F3"];
__N1 [color="red", label="wait_current_set"];
__N3 [label="wait_init"];
__N4 [label="list_init"];
__N5 [label="wait_queue_add"];
__N6 [label="list_empty"];
__N7 [label="list_add_before"];
__N8 [label="__down"];
__N9 [label="down"];
__N10 [label="phi_take_forks_sema"];
__N11 [label="cond_signal"];
__N12 [label="phi_put_forks_sema"];
__N13 [label="cond_wait"];
__N14 [label="lock_mm"];
__N15 [label="phi_take_forks_condvar"];
__N16 [label="phi_put_forks_condvar"];
__N3 -> __N4;
__N1 -> __N3;
__N5 -> __N6;
__N5 -> __N7;
__N1 -> __N5;
__N10 -> __N9;
__N11 -> __N9;
__N12 -> __N9;
__N13 -> __N9;
__N14 -> __N9;
__N15 -> __N9;
__N16 -> __N9;
__N9 -> __N8;
__N8 -> __N1;
}
```

View File

@ -1,77 +1,7 @@
### 同步互斥的底层支撑
**开关中断**
由于有处理器调度的存在,且进程在访问某类资源暂时无法满足的情况下,进程会进入等待状态。这导致了多进程执行时序的不确定性和潜在执行结果的不确定性。为了确保执行结果的正确性,本试验需要设计更加完善的进程等待和互斥的底层支撑机制,确保能正确提供基于信号量和条件变量的同步互斥机制。
根据操作系统原理的知识,我们知道如果没有在硬件级保证读内存-修改值-写回内存的原子性,我们只能通过复杂的软件来实现同步互斥操作。但由于有开关中断和test\_and\_set\_bit等原子操作机器指令的存在使得我们在实现同步互斥原语上可以大大简化。在atomic.c文件中实现的test\_and\_set\_bit等原子操作
根据操作系统原理的知识,我们知道如果没有在硬件级保证读内存-修改值-写回内存的原子性,我们只能通过复杂的软件来实现同步互斥操作。但由于有定时器、屏蔽/使能中断、等待队列wait_queue支持test\_and\_set\_bit等原子操作机器指令在本次实验中没有用到的存在使得我们在实现进程等待、同步互斥上得到了极大的简化。下面将对定时器、屏蔽/使能中断和等待队列进行进一步讲解
在ucore中提供的底层机制包括中断开关控制和test\_and\_set相关原子操作机器指令。kern/sync.c中实现的开关中断的控制函数local\_intr\_save(x)和local\_intr\_restore(x)它们是基于kern/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);
……
```
由于目前ucore只实现了对单处理器的支持所以通过这种方式就可简单地支撑互斥操作了。在多处理器情况下这种方法是无法实现互斥的因为屏蔽了一个CPU的中断只能阻止本CPU上的进程不会被中断或调度并不意味着其他CPU上执行的进程不能执行临界区的代码。所以开关中断只对单处理器下的互斥操作起作用。在本实验中开关中断机制是实现信号量等高层同步互斥原语的底层支撑基础之一。
**等待队列**
到目前为止我们的实验中用户进程或内核线程还没有睡眠的支持机制。在课程中提到用户进程或内核线程可以转入休眠状态以等待某个特定事件当该事件发生时这些进程能够被再次唤醒。内核实现这一功能的一个底层支撑机制就是等待队列wait
queue等待队列和每一个事件睡眠结束、时钟到达、任务完成、资源可用等联系起来。需要等待事件的进程在转入休眠状态后插入到等待队列中。当事件发生之后内核遍历相应等待队列唤醒休眠的用户进程或内核线程并设置其状态为就绪状态runnable
state并将该进程从等待队列中清除。ucore在kern/sync/{ wait.h, wait.c
}中实现了wait结构和wait
queue结构以及相关函数这是实现ucore中的信号量机制和条件变量机制的基础进入wait
queue的进程会被设为睡眠状态直到他们被唤醒。
```
typedef struct {
struct proc_struct *proc; //等待进程的指针
uint32_t wakeup_flags; //进程被放入等待队列的原因标记
wait_queue_t *wait_queue; //指向此wait结构所属于的wait_queue
list_entry_t wait_link; //用来组织wait_queue中wait节点的连接
} wait_t;
typedef struct {
list_entry_t wait_head; //wait_queue的队头
} wait_queue_t;
le2wait(le, member) //实现wait_t中成员的指针向wait_t 指针的转化
```
与wait和wait queue相关的函数主要分为两层底层函数是对wait queue的初始化、插入、删除和查找操作相关函数如下
```
void wait_init(wait_t *wait, struct proc_struct *proc); //初始化wait结构
bool wait_in_queue(wait_t *wait); //wait是否在wait queue中
void wait_queue_init(wait_queue_t *queue); //初始化wait_queue结构
void wait_queue_add(wait_queue_t *queue, wait_t *wait); //把wait前插到wait queue中
void wait_queue_del(wait_queue_t *queue, wait_t *wait); //从wait queue中删除wait
wait_t *wait_queue_next(wait_queue_t *queue, wait_t *wait);//取得wait的后一个链接指针
wait_t *wait_queue_prev(wait_queue_t *queue, wait_t *wait);//取得wait的前一个链接指针
wait_t *wait_queue_first(wait_queue_t *queue); //取得wait queue的第一个wait
wait_t *wait_queue_last(wait_queue_t *queue); //取得wait queue的最后一个wait
bool wait_queue_empty(wait_queue_t *queue); //wait queue是否为空
```
高层函数基于底层函数实现了让进程进入等待队列,以及从等待队列中唤醒进程,相关函数如下:
```
//让wait与进程关联且让当前进程关联的wait进入等待队列queue当前进程睡眠
void wait_current_set(wait_queue_t *queue, wait_t *wait, uint32_t wait_state);
//把与当前进程关联的wait从等待队列queue中删除
wait_current_del(queue, wait);
//唤醒与wait关联的进程
void wakeup_wait(wait_queue_t *queue, wait_t *wait, uint32_t wakeup_flags, bool del);
//唤醒等待队列上挂着的第一个wait所关联的进程
void wakeup_first(wait_queue_t *queue, uint32_t wakeup_flags, bool del);
//唤醒等待队列上所有的等待的进程
void wakeup_queue(wait_queue_t *queue, uint32_t wakeup_flags, bool del);
```

View File

@ -5,7 +5,7 @@
CPU 的开销。参考教科书“Operating Systems Internals and Design
Principles”第五章“同步互斥”中对信号量实现的原理性描述
```
```c
struct semaphore {
int count;
queueType queue;
@ -30,12 +30,11 @@ if (s.count<= 0) {
基于上诉信号量实现可以认为,当多个(\>1进程可以进行互斥或同步合作时一个进程会由于无法满足信号量设置的某条件而在某一位置停止直到它接收到一个特定的信号表明条件满足了。为了发信号需要使用一个称作信号量的特殊变量。为通过信号量s传送信号信号量的V操作采用进程可执行原语semSignal(s)为通过信号量s接收信号信号量的P操作采用进程可执行原语semWait(s);如果相应的信号仍然没有发送,则进程被阻塞或睡眠,直到发送完为止。
ucore中信号量参照上述原理描述建立在开关中断机制和wait
queue的基础上进行了具体实现。信号量的数据结构定义如下
ucore中信号量参照上述原理描述建立在开关中断机制和wait_queue的基础上进行了具体实现。信号量的数据结构定义如下
```
```c
typedef struct {
int value; //信号量的当前值
int value; //信号量的当前值
wait_queue_t wait_queue; //信号量对应的等待队列
} semaphore_t;
```
@ -47,7 +46,7 @@ semaphore)结构包含了用于计数的整数值value和一个进程等
● \_\_down(semaphore\_t \*sem, uint32\_t wait\_state, timer\_t \*timer)具体实现信号量的P操作首先关掉中断然后判断当前信号量的value是否大于0。如果是\>0则表明可以获得信号量故让value减一并打开中断返回即可如果不是\>0则表明无法获得信号量故需要将当前的进程加入到等待队列中并打开中断然后运行调度器选择另外一个进程执行。如果被V操作唤醒则把自身关联的wait从等待队列中删除此过程需要先关中断完成后开中断。具体实现如下所示
```
```c
static __noinline uint32_t __down(semaphore_t *sem, uint32_t wait_state) {
bool intr_flag;
local_intr_save(intr_flag);
@ -77,7 +76,7 @@ static __noinline uint32_t __down(semaphore_t *sem, uint32_t wait_state) {
wait\_state)具体实现信号量的V操作首先关中断如果信号量对应的wait
queue中没有进程在等待直接把信号量的value加一然后开中断返回如果有进程在等待且进程等待的原因是semophore设置的则调用wakeup\_wait函数将waitqueue中等待的第一个wait删除且把此wait关联的进程唤醒最后开中断返回。具体实现如下所示
```
```c
static __noinline void __up(semaphore_t *sem, uint32_t wait_state) {
bool intr_flag;
local_intr_save(intr_flag);

View File

@ -1,5 +1,6 @@
### 管程和条件变量
#### 原理回顾
引入了管程是为了将对共享资源的所有访问及其所需要的同步操作集中并封装起来。Hansan为管程所下的定义“一个管程定义了一个数据结构和能为并发进程所执行在该数据结构上的一组操作这组操作能同步进程和改变管程中的数据”。有上述定义可知管程由四部分组成
* 管程内部的共享变量;
@ -9,61 +10,98 @@
局限在管程中的数据结构,只能被局限在管程的操作过程所访问,任何管程之外的操作过程都不能访问它;另一方面,局限在管程中的操作过程也主要访问管程内的数据结构。由此可见,管程相当于一个隔离区,它把共享变量和对它进行操作的若干个过程围了起来,所有进程要访问临界资源时,都必须经过管程才能进入,而管程每次只允许一个进程进入管程,从而需要确保进程之间互斥。
但在管程中仅仅有互斥操作是不够用的。进程可能需要等待某个条件C为真才能继续执行。如果采用[忙等](http://zh.wikipedia.org/w/index.php?title=%E5%BF%99%E7%AD%89%E5%BE%85&action=edit&redlink=1 "忙等待(页面不存在)")(busy
但在管程中仅仅有互斥操作是不够用的。进程可能需要等待某个条件Cond为真才能继续执行。如果采用[忙等](http://zh.wikipedia.org/w/index.php?title=%E5%BF%99%E7%AD%89%E5%BE%85&action=edit&redlink=1 "忙等待(页面不存在)")(busy
waiting)方式:
```
while not( C ) do {}
while not( Cond ) do {}
```
在单处理器情况下,将会导致所有其它进程都无法进入[临界区](http://zh.wikipedia.org/wiki/%E4%B8%B4%E7%95%8C%E5%8C%BA "临界区")使得该条件C为真该管程的执行将会发生[死锁](http://zh.wikipedia.org/wiki/%E6%AD%BB%E9%94%81 "死锁")。为此可引入条件变量Condition
Variables简称CV。一个条件变量CV可理解为一个进程的等待队列队列中的进程正等待某个条件C变为真。每个条件变量关联着一个[断言](http://zh.wikipedia.org/wiki/%E6%96%B7%E8%A8%80_(%E7%A8%8B%E5%BC%8F) "断言 (程序)")Pc。当一个进程等待一个条件变量该进程不算作占用了该管程因而其它进程可以进入该管程执行改变管程的状态通知条件变量CV其关联的断言Pc在当前状态下为真。因此对条件变量CV有两种主要操作
在单处理器情况下,将会导致所有其它进程都无法进入[临界区](http://zh.wikipedia.org/wiki/%E4%B8%B4%E7%95%8C%E5%8C%BA "临界区")使得该条件Cond为真,该管程的执行将会发生[死锁](http://zh.wikipedia.org/wiki/%E6%AD%BB%E9%94%81 "死锁")。为此可引入条件变量Condition
Variables简称CV。一个条件变量CV可理解为一个进程的等待队列队列中的进程正等待某个条件Cond变为真。每个条件变量关联着一个条件如果条件Cond不为真则进程需要等待如果条件Cond为真则进程可以进一步在管程中执行。需要注意当一个进程等待一个条件变量CV即等待Cond为真该进程需要退出管程这样才能让其它进程可以进入该管程执行并进行相关操作比如设置条件Cond为真改变条件变量的状态并唤醒等待在此条件变量CV上的进程。因此对条件变量CV有两种主要操作
* wait\_cv 被一个进程调用以等待断言Pc被满足后该进程可恢复执行.
进程挂在该条件变量上等待时,不被认为是占用了管程。
* signal\_cv被一个进程调用以指出断言Pc现在为真从而可以唤醒等待断言Pc被满足的进程继续执行。
#### "哲学家就餐"实例
有了互斥和信号量支持的管程就可用用了解决各种同步互斥问题。比如参考《OS
Concept》一书中的6.7.2小节“用管程解决哲学家就餐问题”就给出了这样的事例:
```c
monitor dp
{
enum {THINKING, HUNGRY, EATING} state[5];
condition self[5];
![image](../lab7_figs/image001.png)
void pickup(int i) {
state[i] = HUNGRY;
test(i);
if (state[i] != EATING)
self[i].wait_cv();
}
void putdown(int i) {
state[i] = THINKING;
test((i + 4) % 5);
test((i + 1) % 5);
}
void test(int i) {
if ((state[(i + 4) % 5] != EATING) &&
(state[i] == HUNGRY) &&
(state[(i + 1) % 5] != EATING)) {
state[i] = EATING;
self[i].signal_cv();
}
}
initialization code() {
for (int i = 0; i < 5; i++)
state[i] = THINKING;
}
}
```
#### 关键数据结构
虽然大部分教科书上说明管程适合在语言级实现比如java等高级语言没有提及在采用C语言的OS中如何实现。下面我们将要尝试在ucore中用C语言实现采用基于互斥和条件变量机制的管程基本原理。
ucore中的管程机制是基于信号量和条件变量来实现的。ucore中的管程的数据结构monitor\_t定义如下
```
```c
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
// the next semaphore is used to
// (1) procs which call cond_signal funciton should DOWN next sema after UP cv.sema
// OR (2) procs which call cond_wait funciton should UP next sema before DOWN cv.sema
semaphore_t next;
int next_count; // the number of of sleeped procs which cond_signal funciton
condvar_t *cv; // the condvars in monitor
} monitor_t;
```
管程中的成员变量mutex是一个二值信号量是实现每次只允许一个进程进入管程的关键元素确保了[互斥](http://zh.wikipedia.org/wiki/%E4%BA%92%E6%96%A5 "互斥")访问性质。管程中的条件变量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而睡眠的进程个数。
管程中的成员变量mutex是一个二值信号量是实现每次只允许一个进程进入管程的关键元素确保了[互斥](http://zh.wikipedia.org/wiki/%E4%BA%92%E6%96%A5 "互斥")访问性质。管程中的条件变量cv通过执行`wait_cv`会使得等待某个条件Cond为真的进程能够离开管程并睡眠且让其他进程进入管程继续执行而进入管程的某进程设置条件Cond为真并执行`signal_cv`时能够让等待某个条件Cond为真的睡眠进程被唤醒从而继续进入管程中执行。
注意管程中的成员变量信号量next和整型变量next\_count是配合进程对条件变量cv的操作而设置的这是由于发出`signal_cv`的进程A会唤醒由于`wait_cv`而睡眠的进程B由于管程中只允许一个进程运行所以进程B执行会导致唤醒进程B的进程A睡眠直到进程B离开管程进程A才能继续执行这个同步过程是通过信号量next完成的而next\_count表示了由于发出`singal_cv`而睡眠的进程个数。
管程中的条件变量的数据结构condvar\_t定义如下
```
```c
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
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表示此条件变量的宿主是哪个管程。
条件变量的定义中也包含了一系列的成员变量信号量sem用于让发出`wait_cv`操作的等待某个条件Cond为真的进程睡眠,而让发出`signal_cv`操作的进程通过这个sem来唤醒睡眠的进程。count表示等在这个条件变量上的睡眠进程的个数。owner表示此条件变量的宿主是哪个管程。
理解了数据结构的含义后我们就可以开始管程的实现了。ucore设计实现了条件变量wait\_cv操作和signal\_cv操作对应的具体函数即cond\_wait函数和cond\_signal函数此外还有cond\_init初始化函数可直接看源码。函数cond\_wait(condvar\_t
\*cvp, semaphore\_t \*mp)和cond\_signal (condvar\_t
\*cvp)的实现原理可参考《OS
Concept》一书中的6.7.3小节“用信号量实现管程”的内容:
** cond_wait的原理描述 **
#### 条件变量的signal和wait的设计
理解了数据结构的含义后我们就可以开始管程的设计实现了。ucore设计实现了条件变量`wait_cv`操作和`signal_cv`操作对应的具体函数,即`cond_wait`函数和`cond_signal`函数,此外还有`cond_init`初始化函数(可直接看源码)。函数`cond_wait(condvar_t *cvp, semaphore_t *mp)`和`cond_signal (condvar_t *cvp)`的实现原理参考了《OS Concept》一书中的6.7.3小节“用信号量实现管程”的内容。首先来看`wait_cv`的原理实现:
```
** wait_cv的原理描述 **
```c
cv.count++;
if(monitor.next_count > 0)
sem_signal(monitor.next);
@ -74,9 +112,22 @@ cv.count -- ;
```
** cond_signal的原理描述 **
对照着可分析出`cond_wait`函数的具体执行过程。可以看出如果进程A执行了`cond_wait`函数表示此进程等待某个条件Cond不为真需要睡眠。因此表示等待此条件的睡眠进程个数cv.count要加一。接下来会出现两种情况。
```
情况一如果monitor.next\_count如果大于0表示有大于等于1个进程执行cond\_signal函数且睡了就睡在了monitor.next信号量上假定这些进程挂在monitor.next信号量相关的等待队列因此需要唤醒等待队列中的一个进程B然后进程A睡在cv.sem上。如果进程A醒了则让cv.count减一表示等待此条件变量的睡眠进程个数少了一个可继续执行了
> 这里隐含这一个现象即某进程A在时间顺序上先执行了`cond_signal`而另一个进程B后执行了`cond_wait`这会导致进程A没有起到唤醒进程B的作用。
> 问题: 在cond\_wait有sem\_signal(mutex)但没有看到哪里有sem\_wait(mutex),这好像没有成对出现,是否是错误的?
> 答案其实在管程中的每一个函数的入口处会有wait(mutex),这样二者就配好对了。
情况二如果monitor.next\_count如果小于等于0表示目前没有进程执行cond\_signal函数且睡着了那需要唤醒的是由于互斥条件限制而无法进入管程的进程所以要唤醒睡在monitor.mutex上的进程。然后进程A睡在cv.sem上如果睡醒了则让cv.count减一表示等待此条件的睡眠进程个数少了一个可继续执行了
然后来看`signal_cv`的原理实现:
** signal_cv的原理描述 **
```c
if( cv.count > 0) {
monitor.next_count ++;
sem_signal(cv.sem);
@ -85,21 +136,19 @@ if( cv.count > 0) {
}
```
简单分析一下cond\_wait函数的实现。可以看出如果进程A执行了cond\_wait函数表示此进程等待某个条件C不为真需要睡眠。因此表示等待此条件的睡眠进程个数cv.count要加一。接下来会出现两种情况
对照着可分析出`cond_signal`函数的具体执行过程。首先进程B判断cv.count如果不大于0则表示当前没有执行cond\_wait而睡眠的进程因此就没有被唤醒的对象了直接函数返回即可如果大于0这表示当前有执行cond\_wait而睡眠的进程A因此需要唤醒等待在cv.sem上睡眠的进程A。由于只允许一个进程在管程中执行所以一旦进程B唤醒了别人进程A那么自己就需要睡眠。故让monitor.next\_count加一且让自己进程B睡在信号量monitor.next上。如果睡醒了这让monitor.next\_count减一
情况一如果monitor.next\_count如果大于0表示有大于等于1个进程执行cond\_signal函数且睡着了就睡在了monitor.next信号量上。假定这些进程形成S进程链表。因此需要唤醒S进程链表中的一个进程B。然后进程A睡在cv.sem上如果睡醒了则让cv.count减一表示等待此条件的睡眠进程个数少了一个可继续执行了这里隐含这一个现象即某进程A在时间顺序上先执行了signal\_cv而另一个进程B后执行了wait\_cv这会导致进程A没有起到唤醒进程B的作用。这里还隐藏这一个问题在cond\_wait有sem\_signal(mutex)但没有看到哪里有sem\_wait(mutex)这好像没有成对出现是否是错误的其实在管程中的每一个函数的入口处会有wait(mutex),这样二者就配好对了。
情况二如果monitor.next\_count如果小于等于0表示目前没有进程执行cond\_signal函数且睡着了那需要唤醒的是由于互斥条件限制而无法进入管程的进程所以要唤醒睡在monitor.mutex上的进程。然后进程A睡在cv.sem上如果睡醒了则让cv.count减一表示等待此条件的睡眠进程个数少了一个可继续执行了
对照着再来看cond\_signal的实现。首先进程B判断cv.count如果不大于0则表示当前没有执行cond\_wait而睡眠的进程因此就没有被唤醒的对象了直接函数返回即可如果大于0这表示当前有执行cond\_wait而睡眠的进程A因此需要唤醒等待在cv.sem上睡眠的进程A。由于只允许一个进程在管程中执行所以一旦进程B唤醒了别人进程A那么自己就需要睡眠。故让monitor.next\_count加一且让自己进程B睡在信号量monitor.next上。如果睡醒了这让monitor.next\_count减一。
#### 管程中函数的入口出口设计
为了让整个管程正常运行,还需在管程中的每个函数的入口和出口增加相关操作,即:
```
function (…)
function_in_monitor (…)
{
sem.wait(monitor.mutex);
sem.wait(monitor.mutex);
//-----------------------------
the real body of function;
//-----------------------------
if(monitor.next_count > 0)
sem_signal(monitor.next);
else