原子操作
原子操作(Atomic Operator)是指一次不存在任何中断或失效的操作
等待队列
前面的实验中已经实现了进程运行队列(就绪队列),而等待队列和它们类似:
1 2 3 4 5 6 7 8 9 10
| typedef struct { list_entry_t wait_head; } wait_queue_t;
typedef struct { struct proc_struct *proc; uint32_t wakeup_flags; wait_queue_t *wait_queue; list_entry_t wait_link; } wait_t;
|
等待队列结构底层操作:
- wait_init:初始化 wait 等待队列项,为 wait(等待队列结构体)绑定一个 proc(进程)
1 2 3 4 5 6
| void wait_init(wait_t *wait, struct proc_struct *proc) { wait->proc = proc; wait->wakeup_flags = WT_INTERRUPTED; list_init(&(wait->wait_link)); }
|
1 2 3 4
| void wait_queue_init(wait_queue_t *queue) { list_init(&(queue->wait_head)); }
|
- wait_queue_add:将 wait 节点项插入等待队列
1 2 3 4 5 6
| void wait_queue_add(wait_queue_t *queue, wait_t *wait) { assert(list_empty(&(wait->wait_link)) && wait->proc != NULL); wait->wait_queue = queue; list_add_before(&(queue->wait_head), &(wait->wait_link)); }
|
- wait_queue_del:将 wait 项从等待队列中移除
1 2 3 4 5
| void wait_queue_del(wait_queue_t *queue, wait_t *wait) { assert(!list_empty(&(wait->wait_link)) && wait->wait_queue == queue); list_del_init(&(wait->wait_link)); }
|
- wait_queue_next:获取等待队列中wait节点的下一项
1 2 3 4 5 6 7 8 9
| wait_t * wait_queue_next(wait_queue_t *queue, wait_t *wait) { assert(!list_empty(&(wait->wait_link)) && wait->wait_queue == queue); list_entry_t *le = list_next(&(wait->wait_link)); if (le != &(queue->wait_head)) { return le2wait(le, wait_link); } return NULL; }
|
- wait_queue_prev:获取等待队列中wait节点的前一项
1 2 3 4 5 6 7 8 9
| wait_t * wait_queue_prev(wait_queue_t *queue, wait_t *wait) { assert(!list_empty(&(wait->wait_link)) && wait->wait_queue == queue); list_entry_t *le = list_prev(&(wait->wait_link)); if (le != &(queue->wait_head)) { return le2wait(le, wait_link); } return NULL; }
|
- wait_queue_first:获取等待队列的第一项
1 2 3 4 5 6 7 8
| wait_t * wait_queue_first(wait_queue_t *queue) { list_entry_t *le = list_next(&(queue->wait_head)); if (le != &(queue->wait_head)) { return le2wait(le, wait_link); } return NULL; }
|
- wait_queue_last:获取等待队列的最后一项
1 2 3 4 5 6 7 8
| wait_t * wait_queue_last(wait_queue_t *queue) { list_entry_t *le = list_prev(&(queue->wait_head)); if (le != &(queue->wait_head)) { return le2wait(le, wait_link); } return NULL; }
|
- wait_queue_empty:检查等待队列是否为空
1 2 3 4 5 6 7 8 9
| bool wait_queue_empty(wait_queue_t *queue) { return list_empty(&(queue->wait_head)); }
static inline bool list_empty(list_entry_t *list) { return list->next == list; }
|
- wait_in_queue:检查 wait 项是否在等待队列中
1 2 3 4
| bool wait_in_queue(wait_t *wait) { return !list_empty(&(wait->wait_link)); }
|
等待队列休眠/唤醒等高层操作:
- wakeup_wait:将等待队列中的 wait 项对应的线程唤醒
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| void wakeup_wait(wait_queue_t *queue, wait_t *wait, uint32_t wakeup_flags, bool del) { if (del) { wait_queue_del(queue, wait); } wait->wakeup_flags = wakeup_flags; wakeup_proc(wait->proc); }
void wakeup_proc(struct proc_struct *proc) { assert(proc->state != PROC_ZOMBIE); bool intr_flag; local_intr_save(intr_flag); { if (proc->state != PROC_RUNNABLE) { proc->state = PROC_RUNNABLE; proc->wait_state = 0; if (proc != current) { sched_class_enqueue(proc); } } else { warn("wakeup runnable process.\n"); } } local_intr_restore(intr_flag); }
|
- wakeup_first:将等待队列中的第一项对应的线程唤醒
1 2 3 4 5 6 7 8
| void wakeup_first(wait_queue_t *queue, uint32_t wakeup_flags, bool del) { wait_t *wait; if ((wait = wait_queue_first(queue)) != NULL) { wakeup_wait(queue, wait, wakeup_flags, del); } }
|
- wakeup_queue:将等待队列中的所有项对应的线程全部唤醒
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void wakeup_queue(wait_queue_t *queue, uint32_t wakeup_flags, bool del) { wait_t *wait; if ((wait = wait_queue_first(queue)) != NULL) { if (del) { do { wakeup_wait(queue, wait, wakeup_flags, 1); } while ((wait = wait_queue_first(queue)) != NULL); } else { do { wakeup_wait(queue, wait, wakeup_flags, 0); } while ((wait = wait_queue_next(queue, wait)) != NULL); } } }
|
- wait_current_set:为当前进程绑定 wait,使其休眠并且插入等待队列
1 2 3 4 5 6 7 8
| void wait_current_set(wait_queue_t *queue, wait_t *wait, uint32_t wait_state) { assert(current != NULL); wait_init(wait, current); current->state = PROC_SLEEPING; current->wait_state = wait_state; wait_queue_add(queue, wait); }
|
- wait_current_del:将 wait 项(绑定有当前进程)从等待队列中删除(如果存在的话)
1 2 3 4 5 6 7 8
| #define wait_current_del(queue, wait) \ do { \ if (wait_in_queue(wait)) { \ wait_queue_del(queue, wait); \ } \ } while (0)
#endif
|
临界区
每个进程中访问临界资源的那段程序称为临界区,临界资源是一次仅允许一个进程使用的共享资源,每次只准许一个进程进入临界区,进入后不允许其他进程进入
相关区域的概念:
- 临界区(critical section):进程中访问临界资源的一段需要互斥执行的代码
- 进入区(entry section):检查可否进入临界区的一段代码,如果可以进入,则设置“正在访问临界区”标志
- 退出区(exit section):清除标志
- 剩余区(remainder section):代码中的其余部分
信号量
信号量是一个有整数值的对象,可以用两个函数来操作它
Linux中的信号量是一种睡眠锁,本质上是一个计数器,用于多进程对共享数据对象的读取,它和管道有所不同,它不以传送数据为主要目的,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享
- 信号量(Semaphore)是操作系统提供的一种协调共享资源访问的方法
- 软件同步是平等线程间的一种同步协商机制
- OS 是管理者,地位高于进程
- 用信号量表示系统资源的数量
- 信号量是一种抽象数据类型
- 由一个整数(sem)变量和两个原子操作(PV)组成
- 整数sem:
- sem >= 0:代表剩余可供并发进程使用的资源实体数
- sem < 0:代表正在使用的资源实体数
- P操作:
- sem —
- 如果 sem < 0,则该进程进入阻塞队列(等待队列)
- 如果 sem >= 0,则该进程继续执行
- V操作:
- sem ++
- 如果 sem < 0,则唤醒阻塞队列中的第一个等待信号量的进程
- 如果 sem > 0,则该进程继续执行
- 信号量是被保护的整数变量
- 初始化完成后,只能通过 P() 和 V() 操作修改
- 由操作系统来保证,PV操作是原子操作
PV操作
PV操作是一种实现进程互斥与同步的有效方法,PV操作与信号量的处理相关(P表示通过的意思,V表示释放的意思)
PV操作是典型的同步机制之一,用一个信号量与一个消息联系起来
- 当信号量的值为“0”时,表示期望的消息尚未产生
- 当信号量的值非“0”时,表示期望的消息已经存在
用PV操作实现进程同步时,调用P操作测试消息是否到达,调用V操作发送消息
ucore 中采用以下结构体来管理信号量
1 2 3 4
| typedef struct { int value; wait_queue_t wait_queue; } semaphore_t;
|
- value 是用于判断该信号量能否进入临界区的关键参数
- wait_queue 记录了该信号量所属的等待队列,便于之后的
wait_current_set
和 wait_current_del
把当前进程填入或取出该等待队列
进入临界区时,uCore会执行 down 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| 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); local_intr_restore(intr_flag);
if (wait->wakeup_flags != wait_state) { return wait->wakeup_flags; } return 0; }
|
- 当信号量的value值大于“0”时,说明还能容纳当前线程进入临界区
- 当信号量的value值等于“0”时。说明已经无法容纳更多的线程了,此时需要将当前线程阻塞在信号量的等待队列上,等待信号量的 up 操作将其唤醒
- 按照程序的逻辑,value值不可能小于“0”
退出临界区时,uCore会执行 up 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| void up(semaphore_t *sem) { __up(sem, WT_KSEM); }
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); }
|
- 等待队列为NULL,代表了资源实体充足,也就是说没有进程因为“互斥资源实体不足”而进入等待队列,自然没有必要唤醒
- 信号量增加,代表了剩余可供并发进程使用的资源实体数增加
PS:可以发现,ucore 对临界区的处理和 PV 操作有点不同,并没有刻意让 value 值为负,而是直接将当前进程添加入等待队列,退出临界区时,又从等待队列中唤醒该进程
与信号量有关的函数
- sem_init:初始化信息量(各个条目需要手动输入)
1 2 3 4 5
| void sem_init(semaphore_t *sem, int value) { sem->value = value; wait_queue_init(&(sem->wait_queue)); }
|
- 未进行初始化的信号量根本就没有对应的等待队列,所以需要调用
wait_queue_init
来初始化一个等待队列
- 因为信号量是分配到栈上的,所以不需要格外的“create”或者“destroy”操作
管程
管程(Monitor)是一种用于多线程互斥访问共享资源的程序结构(其实就是封装了一下PV操作),它为进程提供了一种“抽象”,使进程可以通过访问管程来间接访问共享资源
- 采用面向对象方法,简化了线程间的同步控制
- 任一时刻最多只有一个线程执行管程代码
- 正在管程中的线程可临时放弃管程的互斥访问,等待事件出现时恢复
管程的组成
- 一个锁:控制管程代码的互斥访问
- 0-n 个条件变量:用于管理共享数据的并发访问
引入管程机制的目的:
- 把分散在各进程中的临界区集中起来进行管理
- 防止进程有意或无意的违法同步操作(防止死锁)
- 便于用高级语言来书写程序,也便于程序正确性验证
在 ucore 中有如下结构体来管理管程:
1 2 3 4 5 6 7 8
| typedef struct monitor monitor_t;
typedef struct monitor{ semaphore_t mutex; semaphore_t next; int next_count; condvar_t *cv; } monitor_t;
|
- mutex:
- 管程中的成员变量 mutex 是一个二值信号量,是实现每次只允许一个进程进入管程的关键元素,确保了互斥访问性质
- cv:
- 管程中的条件变量 cv 通过执行 wait_cv,会使得等待某个条件 C 为真的进程能够离开管程并睡眠,且让其他进程进入管程继续执行
- 而进入管程的某进程设置条件 C 为真并执行 signal_cv 时,能够让等待某个条件 C 为真的睡眠进程被唤醒,从而继续进入管程中执行
- next,next_count:
- 管程中的成员变量信号量 next 和整形变量 next_count 是配合进程对条件变量 cv 的操作而设置的
- 这是由于发出signal_cv 的进程 A 会唤醒睡眠进程 B,进程 B 执行会导致进程 A 睡眠,直到进程 B 离开管程,进程 A 才能继续执行,这个同步过程是通过信号量 next 完成的
下面是与条件变量有关的结构体:
1 2 3 4 5
| typedef struct condvar{ semaphore_t sem; int count; monitor_t * owner; } condvar_t;
|
- sem:信号量 sem 用于让发出 wait_cv 操作的
- count:表示等在这个条件变量上的睡眠进程的个数
- owner:表示此条件变量的宿主是哪个管程
与管程有关的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void monitor_init (monitor_t * mtp, size_t num_cv) { int i; assert(num_cv>0); mtp->next_count = 0; mtp->cv = NULL; sem_init(&(mtp->mutex), 1); sem_init(&(mtp->next), 0); mtp->cv =(condvar_t *) kmalloc(sizeof(condvar_t)*num_cv); assert(mtp->cv!=NULL); for(i=0; i<num_cv; i++){ mtp->cv[i].count=0; sem_init(&(mtp->cv[i].sem),0); mtp->cv[i].owner=mtp; } }
|
- cond_signal:当某个线程准备离开临界区,准备释放对应的条件变量时,执行该函数(需要实现)
- cond_wait:当某个线程需要等待锁时,执行该函数(需要实现)
进程的交互关系
相互感知的程度 |
交互关系 |
进程间的影响 |
相互不感知(完全不了解其他进程的存在) |
独立 |
一个进程的操作对其他进程的结果无影响 |
间接感知(双方都与第三方交互,例如数据共享) |
通过共享进行协作 |
一个进程的结果依赖于共享资源的状态 |
直接感知(双方直接交互,例如通信) |
通过通信进行协作 |
一个进程的结果依赖于从其他进程获得的信息 |
进程之间可能出现三种关系:
- 互斥(mutual exclusion):一个进程占用资源,其他进程不能使用
- 死锁(deadlock):多个进程占用部分资源,形成循环等待
- 饥饿(starvation):其他进程可能轮流占用资源,一个进程一直得不到资源
CAS与锁
CAS
CPU拥有多个物理核心,利用超线程技术可以把这些物理核心分为更多的逻辑核心
这就产生了一些问题:
左边是我们预想的执行顺序,右边是可以产生的情况(从不同的寄存器中读取了“0”)
如果把 “i++” 设置为原子操作,那么 “i+2”,“i+3”,“i*3” …… 这些都要设置为原子操作,大大影响了效率,于是 CPU 就提供了一个抽象的底层指令 cas(Compare and Set)
- 更新内存地址“i”的时候,需要告诉CPU过去的值“0”,和想要更新的值“1”,CPU会先对比过去的值,然后再更新需要的值“1”,如果对比不通过,CPU就不作出相应
- 通过这种方式,CPU可以给更多指令添加原子操作
假设有两个线程:(“i”初始化为“0”)
线程A可以通过“cas(&i,0,1)”,然后“i”变为“1”,线程B就不能通过了,然后线程B就会采取如下操作:
锁
锁是一个抽象的数据结构:
- 使用一个二进制变量,用于表示锁定/解锁
- Lock::Acquire():锁被释放前一直等待,直到得到锁
- Lock::Release():释放锁,唤醒任何等待的进程
使用锁可以解决一些 cas 无法解决的问题
定时器
定时器(timer)可以帮助操作系统在 经过一段特定时间 后执行一些特殊操作(例如:唤醒执行线程),可以说,正是有了定时器,操作系统才有了时间这个概念
timer_t
结构体中存储了一个定时器所需要的相关数据,包括 倒计时时间 以及 所绑定的进程
1 2 3 4 5
| typedef struct { unsigned int expires; struct proc_struct *proc; list_entry_t timer_link; } timer_t;
|
以下便是 ucore 中和定时器有关的函数:
- timer_init:用于初始化并返回某个
timer
(各个参数都需要手动设置)
1 2 3 4 5 6 7
| static inline timer_t * timer_init(timer_t *timer, struct proc_struct *proc, int expires) { timer->expires = expires; timer->proc = proc; list_init(&(timer->timer_link)); return timer; }
|
- add_timer:用于将某个
timer
按照 expires
的大小添加进 timer链表
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| static list_entry_t timer_list;
void add_timer(timer_t *timer) { bool intr_flag; local_intr_save(intr_flag); { assert(timer->expires > 0 && timer->proc != NULL); assert(list_empty(&(timer->timer_link))); list_entry_t *le = list_next(&timer_list); while (le != &timer_list) { timer_t *next = le2timer(le, timer_link); if (timer->expires < next->expires) { next->expires -= timer->expires; break; } timer->expires -= next->expires; le = list_next(le); } list_add_before(le, &(timer->timer_link)); } local_intr_restore(intr_flag); }
|
- del_timer:用于将某个
timer
从 timer链表
中删除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void del_timer(timer_t *timer) { bool intr_flag; local_intr_save(intr_flag); { if (!list_empty(&(timer->timer_link))) { if (timer->expires != 0) { list_entry_t *le = list_next(&(timer->timer_link)); if (le != &timer_list) { timer_t *next = le2timer(le, timer_link); next->expires += timer->expires; } } list_del_init(&(timer->timer_link)); } } local_intr_restore(intr_flag); }
|
- run_timer_list:用于更新定时器的时间,并更新当前进程的运行时间片,如果当前定时器的剩余时间结束,则唤醒某个处于
WT_INTERRUPTED
等待状态的进程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #define WT_INTERRUPTED 0x80000000
void run_timer_list(void) { bool intr_flag; local_intr_save(intr_flag); { list_entry_t *le = list_next(&timer_list); if (le != &timer_list) { timer_t *timer = le2timer(le, timer_link); assert(timer->expires != 0); timer->expires --; while (timer->expires == 0) { le = list_next(le); struct proc_struct *proc = timer->proc; if (proc->wait_state != 0) { assert(proc->wait_state & WT_INTERRUPTED); } else { warn("process %d's wait_state == 0.\n", proc->pid); } wakeup_proc(proc); del_timer(timer); if (le == &timer_list) { break; } timer = le2timer(le, timer_link); } } sched_class_proc_tick(current); } local_intr_restore(intr_flag); }
|
定时器的检查机制:
内核会每隔一段时间会检查一次定时器(如果定时器的 expires
为“0”,内存就会执行某个进程),但是检查的频率可能不相同,对于 expires
越小的定时器,内核检查的频率越高(例如:如果 expires
为“一年”,可能内核就一个月检查一次,如果 expires
小于一个月,内核就每天检查一次)
- 处于性能考虑,每个新添加的 timer 都会按照其
expires
属性的大小排列,同时减去上一个 timer 的 expires
属性
- 在 run_timer_list 中,程序会遍历 timer 链表,找出所有连续的expires为“0”的 timer,所以按大小排序后,一次执行 run_timer_list 后可能会找到多个目标,提高了效率
- 按照 timer 的机制:在更新 timer_list 中的所有 timer 的 expires 时,只需递减链首的第一个 timer 的 expire,即可间接达到所有 timer 的 expires 减一的目的
1 2 3 4 5 6 7 8 9
| timer1->expires = 20; timer2->expires = 38; timer3->expires = 24; timer4->expires = 10; ---------------------------- timer1插入 >> timer1:20 timer2插入 >> timer1:20 <=> timer2:18(38) timer3插入 >> timer1:20 <=> timer3:4(24) <=> timer2:14(38) timer4插入 >> timer4:10 <=> timer1:10(20) <=> timer3:4(24) <=> timer2:14(38)
|
- 在目标 timer 遍历的时候会不断减去 “结点timer->expires” ,这样保证了 timer 链表中,原来的 timer->expires(没有进行过加减操作,是真正的 timer)是按从小到大排序的
- 这样也避免了 “结点timer->expires” 或者 “目标timer->expires” 被减到“0”的情况发生
案例:do_sleep(将当前进程状态设置为睡眠)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| int do_sleep(unsigned int time) { if (time == 0) { return 0; } bool intr_flag; local_intr_save(intr_flag); timer_t __timer, *timer = timer_init(&__timer, current, time);
current->state = PROC_SLEEPING; current->wait_state = WT_TIMER; add_timer(timer); local_intr_restore(intr_flag);
schedule();
del_timer(timer); return 0; }
|
这里我会结合前面实验中有关进程的调度的部分内容,详细描述一下这个过程:
- 内核的第一个进程
idleproc
(空闲进程)会执行 cpu_idle
函数,在这个函数中循环执行 schedule
用于空闲进程的调度,这个函数是永远不会停止的
- 其他的进程都会因为
schedule
而被调度,又会因为各种原因被中断,然后再次调度
- 当 “PROC_SLEEPING” 被设置时:
schedule
就已经不会再调度该进程了,如果再次执行schedule
,CPU就会放弃当前进程,转而去遍历整个进程链表,直到找出处于就绪状态的进程,并将其调度
- 当 “add_timer(timer)” 执行时:绑定有当前进程的 timer 被链入 timer 链表,然后CPU会周期性调用 run_timer_list 检查 timer->expires 是否为“0”
- 如果时间结束,就会调用 wakeup_proc 重新设置该进程为 “PROC_RUNNABLE” ,这样
schedule
就可以再次调度该进程了
练习0-把 lab6 的内容复制粘贴到 lab7
trap_dispatch 中有关时钟中断的部分需要更新:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| static void trap_dispatch(struct trapframe *tf) { char c;
int ret=0;
switch (tf->tf_trapno) { case T_PGFLT: if ((ret = pgfault_handler(tf)) != 0) { print_trapframe(tf); if (current == NULL) { panic("handle pgfault failed. ret=%d\n", ret); } else { if (trap_in_kernel(tf)) { panic("handle pgfault failed in kernel mode. ret=%d\n", ret); } cprintf("killed by kernel.\n"); panic("handle user mode pgfault failed. ret=%d\n", ret); do_exit(-E_KILLED); } } break; case T_SYSCALL: syscall(); break; case IRQ_OFFSET + IRQ_TIMER: ticks ++; assert(current != NULL); run_timer_list(); break; case IRQ_OFFSET + IRQ_COM1: c = cons_getc(); cprintf("serial [%03d] %c\n", c, c); break; case IRQ_OFFSET + IRQ_KBD: c = cons_getc(); cprintf("kbd [%03d] %c\n", c, c); break; case T_SWITCH_TOU: case T_SWITCH_TOK: panic("T_SWITCH_** ??\n"); break; case IRQ_OFFSET + IRQ_IDE1: case IRQ_OFFSET + IRQ_IDE2: break; default: print_trapframe(tf); if (current != NULL) { cprintf("unhandled trap.\n"); do_exit(-E_KILLED); } panic("unexpected trap in kernel.\n"); } }
|
练习1-理解内核级信号量的实现和基于内核级信号量的哲学家就餐问题
哲学家就餐问题:
- 五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替的进行思考和进餐,平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐,进餐完毕,放下筷子继续思考
ucore 中的案例:
1 2 3 4 5 6 7 8
| #define N 5 #define LEFT (i-1+N)%N #define RIGHT (i+1)%N #define THINKING 0 #define HUNGRY 1 #define EATING 2 #define TIMES 4 #define SLEEP_TIME 10
|
- philosopher_using_semaphore:实现主体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| int state_sema[N]; semaphore_t mutex; semaphore_t s[N];
struct proc_struct *philosopher_proc_sema[N];
int philosopher_using_semaphore(void * arg) { int i, iter=0; i=(int)arg; cprintf("I am No.%d philosopher_sema\n",i); while(iter++<TIMES) { cprintf("Iter %d, No.%d philosopher_sema is thinking\n",iter,i); do_sleep(SLEEP_TIME); phi_take_forks_sema(i); cprintf("Iter %d, No.%d philosopher_sema is eating\n",iter,i); do_sleep(SLEEP_TIME); phi_put_forks_sema(i); } cprintf("No.%d philosopher_sema quit\n",i); return 0; }
|
- phi_take_forks_sema:需要两只叉子,或者阻塞
1 2 3 4 5 6 7 8
| void phi_take_forks_sema(int i) { down(&mutex); state_sema[i]=HUNGRY; phi_test_sema(i); up(&mutex); down(&s[i]); }
|
- phi_put_forks_sema:把两把叉子同时放回桌子
1 2 3 4 5 6 7 8
| void phi_put_forks_sema(int i) { down(&mutex); state_sema[i]=THINKING; phi_test_sema(LEFT); phi_test_sema(RIGHT); up(&mutex); }
|
- phi_test_sema:哲学家尝试得到两只叉子,并且进餐
1 2 3 4 5 6 7 8
| void phi_test_sema(i) { if(state_sema[i]==HUNGRY && state_sema[LEFT]!=EATING && state_sema[RIGHT]!=EATING) { state_sema[i]=EATING; up(&s[i]); } }
|
完整过程:
- 哲学家会循环进行两件事情:
- phi_take_forks_sema:拿起两把叉子准备进食
- phi_put_forks_sema:放回两把叉子
- 本程序没有对叉子进行标记,而是通过检查相邻哲学家的进食状态,来判断自己是否可以进食
- 在哲学家执行 phi_put_forks_sema 时,会检查相邻的哲学家是否可以进食(因为自己进食以后会归还叉子,给相邻哲学家创造了进食机会)
练习2-完成内核级条件变量和基于内核级条件变量的哲学家就餐问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| typedef struct monitor{ semaphore_t mutex; semaphore_t next; int next_count; condvar_t *cv; } monitor_t;
typedef struct condvar{ semaphore_t sem; int count; monitor_t * owner; } condvar_t;
typedef struct { int value; wait_queue_t wait_queue; } semaphore_t;
|
monitor_init 函数会初始管程,而对信号量进行的 P(),V() 操作(up,down)将会被封装为控制管程的两个函数 - cond_signal 和 cond_wait:
- cond_signal:当某个线程准备离开临界区,准备释放对应的条件变量时,执行该函数
1 2 3 4 5 6 7 8 9 10 11
| 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)); 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:当某个线程需要等待锁时,执行该函数
1 2 3 4 5 6 7 8 9 10 11 12
| 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)); 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); }
|
下面是对哲学家就餐问题的改进:(基于管程)
- philosopher_using_condvar:程序主体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| struct proc_struct *philosopher_proc_condvar[N]; int state_condvar[N]; monitor_t mt, *mtp=&mt;
int philosopher_using_condvar(void * arg) { int i, iter=0; i=(int)arg; cprintf("I am No.%d philosopher_condvar\n",i); while(iter++<TIMES) { cprintf("Iter %d, No.%d philosopher_condvar is thinking\n",iter,i); do_sleep(SLEEP_TIME); phi_take_forks_condvar(i);
cprintf("Iter %d, No.%d philosopher_condvar is eating\n",iter,i); do_sleep(SLEEP_TIME); phi_put_forks_condvar(i); } cprintf("No.%d philosopher_condvar quit\n",i); return 0; }
|
- phi_take_forks_condvar:需要两只叉子,或者阻塞
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void phi_take_forks_condvar(int i) { down(&(mtp->mutex));
state_condvar[i]=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:把两把叉子同时放回桌子
1 2 3 4 5 6 7 8 9 10 11 12
| 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)); }
|
- phi_test_condvar:哲学家尝试得到两只叉子,并且进餐
1 2 3 4 5 6 7 8 9
| void phi_test_condvar (i) { if(state_condvar[i]==HUNGRY&&state_condvar[LEFT]!=EATING &&state_condvar[RIGHT]!=EATING) { cprintf("phi_test_condvar: state_condvar[%d] will eating\n",i); state_condvar[i] = EATING ; cprintf("phi_test_condvar: signal self_cv[%d] \n",i); cond_signal(&mtp->cv[i]) ; } }
|
- PS:为了让整个管程正常运行,还需在管程中的每个函数的入口和出口增加相关操作
1 2 3 4 5 6 7 8 9 10
| void monitorFunc() { down(&(mtp->mutex));
if(mtp->next_count>0) up(&(mtp->next)); else up(&(mtp->mutex)); }
|
这样做的好处有两个
- 只有一个进程在执行管程中的函数
- 避免由于执行了
cond_signal
函数而睡眠的进程无法被唤醒
针对 “避免由于执行了 cond_signal
函数而睡眠的进程无法被唤醒” 这个优点简单说一下:
- 管程中
wait
和 signal
函数的调用存在时间顺序
- 例如:当线程1先调用
signal
唤醒线程2并将自身线程挂起后,线程2在开始执行时将无法唤醒原先的在 signal
中挂起的线程1
- 也就是说,只要存在线程在管程中执行了
signal
,那么至少存在一个线程在管程中被挂起
- 此时,就只能在临界区外唤醒挂起的线程1,而这一步在代码中也得到了实现