7.6 Code: Sleep and wakeup
sleep
函数中:- 之所以需要
lk
, 是因为lk
是用于保护作为sleep
和wakeup
的目标的互斥数据所必要的互斥锁. 为了避免漏掉wakeup
函数的唤醒操作,sleep
函数对互斥数据的查询操作和将当前线程置为睡眠的操作必须是原子性的, 这就要求使用互斥锁进行保护. - 之所以传入
lk
, 是因为当前进程睡眠之后必须释放互斥数据的锁, 否则当前进程将会在拥有互斥锁的情况下陷入睡眠, 而wakeup
要想唤醒当前进程就必须先获得互斥锁, 但要想获得互斥锁反过来必须先唤醒当前线程, 从而导致死锁. - 之所以获取
p->lock
之后就能够释放lk
, 是因为wakeup
如果想要唤醒当前线程同样需要先获取p->lock
, 因此原子性并不会遭到破坏; 同时p->lock
仅仅保护当前线程, 其粒度比保护互斥数据的lk
来得更小, 因此p->lock
是更好的选择.
- 之所以需要
即使两对不同的
sleep
和wakeup
函数不小心使用了同一个chan
值也无妨, 毕竟进程被唤醒只是一种暗示, 即数据或条件可能已经满足, 具体是否真的已满足还是得靠唤醒后所进行的检查来确定.
7.8 Code: Wait, exit, and kill
- 避免死锁的一个最简单也是最有效的办法就是使用统一的获取-释放顺序. 这是因为 (反过来) 导致死锁的最简单有效的办法就是按颠倒的顺序获取锁, 例如线程 A 获取锁 1 之后尝试获取锁 2, 而线程 B 获取锁 2 之后尝试获取锁 1, 马上就会导致死锁. xv6 中在
wait
和exit
函数中使用的就是统一的 parent-first-and-then-child 的获取顺序.
7.9 Real world
- xv6 的
scheduler
函数实现的是一种最简单的调度策略, 即 "round robin" 策略. round robin 策略的大意是按照顺序循环访问并尝试运行进程表中的每一个进程; 在当前进程返回调度器时并不回到表头, 而是继续从当前进程的下一个进程开始访问; 如果到达表尾, 则返回表头重新开始一轮访问. - xv6 的睡眠队列并没有实际形式, 而是在逻辑上以一个 64 位二进制模式
chan
进行标识, 并依靠唤醒者进程在进程表中进行轮询来唤醒睡眠在chan
上的其他进程. Linux 内核使用了显式的睡眠队列来组织进程, 每个队列具有各自独立的内部互斥锁. - xv6 所实现的
wakeup
函数将会一次性唤醒所有睡眠在chan
上的进程, 而所有这些进程将会在唤醒后竞争同一个互斥锁, 这是及其效率低下的. 这种现象被称为 "thundering herd" (惊群效应).