同步互斥的设计与实现
实验执行流程概述
互斥是指某一资源同时只允许一个进程对其进行访问,具有唯一性和排它性,但互斥不用限制进程对资源的访问顺序,即访问可以是无序的。
同步是指在进程间的执行必须严格按照规定的某种先后次序来运行,即访问是有序的,这种先后次序取决于要系统完成的任务需求。在进程写资源情况下,进程间要求满足互斥条件。在进程读资源情况下,可允许多个进程同时访问资源。
在实验七中的ucore初始化过程,开始的执行流程都与实验六相同,直到执行到创建第二个内核线程init_main时,修改了init_main的具体执行内容,即增加了check_sync函数的调用,而位于lab7_figs/kern/sync/check_sync.c中的check_sync函数可以理解为是实验七的起始执行点,是实验七的总控函数。进一步分析此函数,可以看到这个函数主要分为了两个部分,第一部分是实现基于信号量的哲学家问题,第二部分是实现基于管程的哲学家问题。
# 该函数只是完成了初始化工作,并没有开始真正的往哲学家上调度。
229 void check_sync(void){
230
231 int i;
232
233 //check semaphore
# 将mutex.value设置为1
# 初始化作为临界区的信号量
234 sem_init(&mutex, 1);
# N是5
# 初始化N个哲学家
235 for(i=0;i<N;i++){
# 将第i个哲学家的信号量value值初始化为0。
236 sem_init(&s[i], 0);
# 每个哲学家对应一个内核线程(注意,此时还没开始调度到哲学家线程呢!!!)
237 int pid = kernel_thread(philosopher_using_semaphore, (void *)i, 0);
238 if (pid <= 0) {
239 panic("create No.%d philosopher_using_semaphore failed.\n");
240 }
# 指向每个哲学家自己的PCB
241 philosopher_proc_sema[i] = find_proc(pid);
242 set_proc_name(philosopher_proc_sema[i], "philosopher_sema_proc");
243 }
244
245 //check condition variable
246 monitor_init(&mt, N);
247 for(i=0;i<N;i++){
248 state_condvar[i]=THINKING;
249 int pid = kernel_thread(philosopher_using_condvar, (void *)i, 0);
250 if (pid <= 0) {
251 panic("create No.%d philosopher_using_condvar failed.\n");
252 }
253 philosopher_proc_condvar[i] = find_proc(pid);
254 set_proc_name(philosopher_proc_condvar[i], "philosopher_condvar_proc");
255 }
256 }
相关数据结构与宏
8 typedef struct {
9 int value;
10 wait_queue_t wait_queue;
11 } semaphore_t;
# 计算左邻右邻
7 #define N 5 /* 哲学家数目 */
8 #define LEFT (i-1+N)%N /* i的左邻号码 */
9 #define RIGHT (i+1)%N /* i的右邻号码 */
76 //---------- philosophers problem using semaphore ----------------------
77 int state_sema[N]; /* 记录每个人状态的数组 */
78 /* 信号量是一个特殊的整型变量 */
79 semaphore_t mutex; /* 临界区互斥 */
80 semaphore_t s[N]; /* 每个哲学家一个信号量 */
81
82 struct proc_struct *philosopher_proc_sema[N];
sem_init()
10 void
11 sem_init(semaphore_t *sem, int value) {
12 sem->value = value;
# 初始化队列,其实就是初始化链表
13 wait_queue_init(&(sem->wait_queue));
14 }
philosopher_using_semaphore()
# 一共有五个线程(对应五个哲学家),每个线程的执行体都是这个
112 int philosopher_using_semaphore(void * arg) /* i:哲学家号码,从0到N-1 */
113 {
114 int i, iter=0;
115 i=(int)arg;
116 cprintf("I am No.%d philosopher_sema\n",i);
117 while(iter++<TIMES)
118 { /* 无限循环 */
119 cprintf("Iter %d, No.%d philosopher_sema is thinking\n",iter,i); /* 哲学家正在思考 */
# 正在“思考”
120 do_sleep(SLEEP_TIME);
# 得到两只叉子,并进入eating状态,表示真正开吃
121 phi_take_forks_sema(i);
122 /* 需要两只叉子,或者阻塞 */
# 能走到这里,就说明阻塞状态解除,而能够解除阻塞状态,就说明一定进入了“开吃”
# 状态拿到了叉子。
123 cprintf("Iter %d, No.%d philosopher_sema is eating\n",iter,i); /* 进餐 */
# 开吃SLEEP_TIME时间
124 do_sleep(SLEEP_TIME);
# 把叉子放回去,同时唤醒旁边进入阻塞状态的哲学家,把叉子给他们(如果他们阻塞了的话)
125 phi_put_forks_sema(i);
126 /* 把两把叉子同时放回桌子 */
127 }
128 cprintf("No.%d philosopher_sema quit\n",i);
129 return 0;
130 }
do_sleep()
928 // do_sleep - set current process state to sleep and add timer with "time"
929 // - then call scheduler. if process run again, delete timer first.
930 int
931 do_sleep(unsigned int time) {
932 if (time == 0) {
933 return 0;
934 }
935 bool intr_flag;
936 local_intr_save(intr_flag);
937 timer_t __timer, *timer = timer_init(&__timer, current, time);
# 注意这里修改了线程状态,除非被唤醒,否则该状态的线程是不会被调度的!!!
938 current->state = PROC_SLEEPING;
939 current->wait_state = WT_TIMER;
940 add_timer(timer);
941 local_intr_restore(intr_flag);
942
# 触发调度,让出CPU,让别的线程去跑
# 等定时器到点儿了还会被调度回来
943 schedule();
# 能走到这里,说明当前线程已经被唤醒,并且定时器也到点儿了
944
# 这里重复删除定时器没问题,里面虽然没有检查,但是不影响。
945 del_timer(timer);
946 return 0;
947 }
phi_take_forks_sema()
94 void phi_take_forks_sema(int i) /* i:哲学家号码从0到N-1 */
95 {
96 down(&mutex); /* 进入临界区 */
97 state_sema[i]=HUNGRY; /* 记录下哲学家i饥饿的事实 */
# 如果资源充足,这里面会将当前哲学家的信号量加一,表示得到了叉子,
# 哲学家可以开吃了;否则哲学家的信号历量就始终是0。
98 phi_test_sema(i); /* 试图得到两只叉子 */
99 up(&mutex); /* 离开临界区 */
# 如果当前哲学家已经拿到了叉子,即当前哲学家的信号量是1,那么这里会让当前哲学家
# 自己的信号量减一,表示不用阻塞,可以往下进行;
# 如果当前哲学家没有拿到叉子,即当前哲学家的信号量是0,那么这里会让当前哲学家线程
# 阻塞,并将当前哲学家线程放到等待队列,调用schedule()开始调度,
# 先让别的线程去运行。
100 down(&s[i]); /* 如果得不到叉子就阻塞 */
101 }
phi_put_forks_sema()
103 void phi_put_forks_sema(int i) /* i:哲学家号码从0到N-1 */
104 {
105 down(&mutex); /* 进入临界区 */
106 state_sema[i]=THINKING; /* 哲学家进餐结束 */
107 phi_test_sema(LEFT); /* 看一下左邻居现在是否能进餐 */
108 phi_test_sema(RIGHT); /* 看一下右邻居现在是否能进餐 */
109 up(&mutex); /* 离开临界区 */
110 }
__down()
32 static __noinline uint32_t __down(semaphore_t *sem, uint32_t wait_state) {
33 bool intr_flag;
34 local_intr_save(intr_flag);
35 if (sem->value > 0) {
36 sem->value --;
37 local_intr_restore(intr_flag);
38 return 0;
39 }
40 wait_t __wait, *wait = &__wait;
41 wait_current_set(&(sem->wait_queue), wait, wait_state);
42 local_intr_restore(intr_flag);
43
# 将本哲学家的线程放入等待队列,然后先让其他哲学家的线程被调度
44 schedule();
# 当本哲学家再次走到这里,说明本哲学家已经被再次调度了,而从整个代码来看,能走到这里
# 只有一种情况,就是其他哲学家执行了phi_put_forks_sema()操作放下了叉子,并在这个
# 操作里通过phi_test_sema(LEFT)或phi_test_sema(RIGHT)修改了当前哲学家线程的
# 状态为eating继而通过up()操作将当前哲学家线程取出等待队列,并通过wakeup_wait()
# 将当前哲学家线程唤醒,这样当前哲学家线程才能走到这里。注意,当当前哲学家线程走到
# 这里的时候,尽管当前哲学家线程的信号量value还是0,但是当前哲学家的状态已经被更新
# 为eating,整体结果就和当初没被阻塞一样。
# 总的来说,即是如果当前哲学家线程因为拿不到叉子而陷入阻塞(自己让自己进入阻塞态),
# 就只能等着别的拿到叉子的哲学家线程放下叉子,并由这些哲学家线程去修改当前哲学家
# 线程的状态(开吃状态)继而唤醒当前哲学家线程(让别人唤醒自己,让别人把叉子给自己)。
45
46 local_intr_save(intr_flag);
47 wait_current_del(&(sem->wait_queue), wait);
48 local_intr_restore(intr_flag);
49
50 if (wait->wakeup_flags != wait_state) {
51 return wait->wakeup_flags;
52 }
53 return 0;
54 }
__up()
16 static __noinline void __up(semaphore_t *sem, uint32_t wait_state) {
17 bool intr_flag;
18 local_intr_save(intr_flag);
19 {
20 wait_t *wait;
21 if ((wait = wait_queue_first(&(sem->wait_queue))) == NULL) {
22 sem->value ++;
23 }
24 else {
25 assert(wait->proc->wait_state == wait_state);
26 wakeup_wait(&(sem->wait_queue), wait, wait_state, 1);
27 }
28 }
29 local_intr_restore(intr_flag);
30 }
phi_test_sema()
# 从整体看,哲学家线程不仅通过该函数修改自己的状态和信号量value值,还通过该函数去修改自己临近
# 哲学家线程的状态和value值,从而使陷入阻塞的邻近哲学家线程被重新调度并有机会开吃。
84 void phi_test_sema(i) /* i:哲学家号码从0到N-1 */
85 {
# 自己饥饿并且左邻和右邻都没在吃
86 if(state_sema[i]==HUNGRY&&state_sema[LEFT]!=EATING
87 &&state_sema[RIGHT]!=EATING)
88 {
# 自己开吃
89 state_sema[i]=EATING;
90 up(&s[i]);
91 }
92 }
计时器的原理和实现
在传统的操作系统中,计时器是其中一个基础而重要的功能,它提供了基于时间事件的调度机制。在ucore 中,timer 中断(irq0)给操作系统提供了有一定间隔的时间事件,操作系统将其作为基本的调度和计时单位(我们记两次时间中断之间的时间间隔为一个时间片,timer splice)。
基于此时间单位,操作系统得以向上提供基于时间点的事件,并实现基于时间长度的等待和唤醒机制。在每个时钟中断发生时,操作系统产生对应的时间事件。应用程序或者操作系统的其他组件可以以此来构建更复杂和高级的调度。
一个 timer_t 在系统中的存活周期可以被描述如下:
- timer_t 在某个位置被创建和初始化,并通过 add_timer加入系统管理列表中
- 系统时间被不断累加,直到 run_timer_list 发现该 timer_t到期。
- run_timer_list更改对应的进程状态,并从系统管理列表中移除该timer_t。
尽管本次实验并不需要填充计时器相关的代码,但是作为系统重要的组件(同时计时器也是调度器的一个部分),你应该了解其相关机制和在ucore中的实现方法。接下来的实验描述将会在一定程度上忽略计时器对调度带来的影响,即不考虑基于固定时间点的调度。
sched.h, sched.c 定义了有关timer的各种相关接口来使用 timer 服务,其中主要包括如下内容
typedef struct {……} timer_t
定义了 timer_t 的基本结构,其可以用 sched.h 中的timer_init函数对其进行初始化。
12 typedef struct {
13 unsigned int expires; //the expire time
14 struct proc_struct *proc; //the proc wait in this timer. If the expire time is end, then this proc will be scheduled
15 list_entry_t timer_link; //the timer list
16 } timer_t;
void timer_init()
初始化某计时器,让它在 expires 时间片之后唤醒 proc 进程。
21 // init a timer
22 static inline timer_t *
23 timer_init(timer_t *timer, struct proc_struct *proc, int expires) {
24 timer->expires = expires;
25 timer->proc = proc;
26 list_init(&(timer->timer_link));
27 return timer;
28 }
void add_timer()
向系统添加某个初始化过的timer_t,该计时器在指定时间后被激活,并将对应的进程唤醒至runnable(如果当前进程处在等待状态)。
102 // add timer to timer_list
103 void
104 add_timer(timer_t *timer) {
105 bool intr_flag;
106 local_intr_save(intr_flag);
107 {
108 assert(timer->expires > 0 && timer->proc != NULL);
109 assert(list_empty(&(timer->timer_link)));
110 list_entry_t *le = list_next(&timer_list);
# 从该循环可以看出定时器链表的原理:
# 整个定时器链表使用的“时间”是唯一的,定时器链表中每个节点上的时间都代表了一个
# 单独的时间“跨度”,而从定时器链表头开始累加每个节点的expires直到加完当前节点
# 的expires,表示从现在开始到当前节点的总定时时间!
# 每当插入一个新的定时器的时候,要好好计算当前时间和这个新的定时器的时间,把新的
# 定时器的时间从定时器链表里面的减掉才能把新的定时器加到定时器链表。
111 while (le != &timer_list) {
112 timer_t *next = le2timer(le, timer_link);
113 if (timer->expires < next->expires) {
114 next->expires -= timer->expires;
115 break;
116 }
117 timer->expires -= next->expires;
118 le = list_next(le);
119 }
120 list_add_before(le, &(timer->timer_link));
121 }
122 local_intr_restore(intr_flag);
123 }
void del_timer()
向系统删除(或者说取消)某一个计时器。该计时器在取消后不会被系统激活并唤醒进程。
125 // del timer from timer_list
126 void
127 del_timer(timer_t *timer) {
128 bool intr_flag;
129 local_intr_save(intr_flag);
130 {
# 根据定时器的原理,由于定时器链表中任何一个节点都代表了从当前开始到未来中间的某一
# 个时间段,所以当删除一个没到时间的定时器时,必须把该定时器涵盖的那个时间段加到
# 它后面的那个节点上,以保证整个定时器链表所涉及的时间是完整的。
131 if (!list_empty(&(timer->timer_link))) {
132 if (timer->expires != 0) {
133 list_entry_t *le = list_next(&(timer->timer_link));
134 if (le != &timer_list) {
135 timer_t *next = le2timer(le, timer_link);
136 next->expires += timer->expires;
137 }
138 }
139 list_del_init(&(timer->timer_link));
140 }
141 }
142 local_intr_restore(intr_flag);
143 }
void run_timer_list()
更新当前系统时间点,遍历当前所有处在系统管理内的计时器,找出所有应该激活的计数器,并激活它们。该过程在且只在每次计时器中断时被调用。在ucore 中,其还会调用调度器事件处理程序。
145 // call scheduler to update tick related info, and check the timer is expired? If expired, then wakup proc
146 void
147 run_timer_list(void) {
148 bool intr_flag;
149 local_intr_save(intr_flag);
150 {
151 list_entry_t *le = list_next(&timer_list);
152 if (le != &timer_list) {
153 timer_t *timer = le2timer(le, timer_link);
154 assert(timer->expires != 0);
155 timer->expires --;
# 注意这个循环很讲究,这里其实是将所有到点儿的定时器都触发。
# 基于定时器链表的原理,某一时刻可能有多个定时器到点儿,这些链表节点一定都是
# 相邻的,且expires肯定都是0,所以这里就是将所有到点儿的定时器全部触发。
156 while (timer->expires == 0) {
157 le = list_next(le);
158 struct proc_struct *proc = timer->proc;
159 if (proc->wait_state != 0) {
160 assert(proc->wait_state & WT_INTERRUPTED);
161 }
162 else {
163 warn("process %d's wait_state == 0.\n", proc->pid);
164 }
# 唤醒到点儿了的进程
165 wakeup_proc(proc);
166 del_timer(timer);
167 if (le == &timer_list) {
168 break;
169 }
170 timer = le2timer(le, timer_link);
171 }
172 }
173 sched_class_proc_tick(current);
174 }
175 local_intr_restore(intr_flag);
176 }