Lost Wakeup
检查条件与进入 SLEEPING 之间的窗口导致 wakeup 丢失。通过重叠的锁消除窗口。
6.1810 / 6.1810 · 第 14 课 · rtm
线程经常需要等待特定事件:磁盘读完成、管道有数据、子进程退出。xv6 使用 sleep/wakeup 原语实现协调,但必须小心避免"丢失唤醒"(lost wakeup)问题。
yield(): scheduler():
acquire(&p->lock); swtch();
p->state = RUNNABLE; release(&p->lock);
swtch();
p->state == RUNNABLE,直到原始核心完成保存寄存器并停止使用线程的栈yield()(acquire 关闭了中断),避免二次 swtch 覆盖已保存的寄存器P1 P2
acq(x) sched()
acq(x) // 自旋等待
// P2 自旋时中断已禁用
// 无时钟中断 → P2 不 yield → P1 不运行
// P1 不释放 x → 死锁!
解决方案:禁止在持有锁时放弃 CPU!(除了 p->lock)
线程经常需要等待特定事件/条件:
// pipe read 的错误方案
while (buffer is empty) {
// 自旋等待
}
// 浪费 CPU!
操作系统使用多种方案:barrier、semaphore、event queue、condition variable 等。xv6 使用 sleep & wakeup。
UART 是控制台设备硬件,每次发送一个字节需要很长时间(~1ms)。
uartwrite(buf):
for each char c:
while tx_busy:
sleep(&tx_chan, &tx_lock)
send c
tx_busy = 1
uartintr():
tx_busy = 0
wakeup(&tx_chan)
| 概念 | 说明 |
|---|---|
sleep(wait_chan, lock) | 让出 CPU,等待某事件 |
wakeup(wait_chan) | 唤醒所有在 sleep() 中等待同一 wait_chan 的进程 |
| wait_chan | "等待通道"——只是一个数字,链接 sleep 和 wakeup |
| condition | 程序员知道的条件(如 tx_busy == 0),sleep/wakeup 不知道 |
// 有 bug 的 sleep/wakeup
broken_sleep(chan)
acquire(&p->lock)
p->state = SLEEPING;
p->chan = chan;
swtch(...); // 不再可运行
wakeup(chan)
for each p:
acquire(&p->lock);
if p->state == SLEEPING && p->chan == chan:
p->state = RUNNABLE
uartwrite(buf):
for each char c:
while tx_busy:
release(&tx_lock); // 释放锁
broken_sleep(&tx_chan); // 然后睡眠
acquire(&tx_lock); // 重新获取
我们需要消除 uartwrite 检查条件与 sleep 标记进程为 SLEEPING 之间的窗口。使用锁防止 wakeup 在整个窗口期间运行。
新的 sleep 接口:
sleep(chan, lock)
调用者必须持有 lock
sleep 释放 lock,返回前重新获取
wakeup(chan)
调用者必须持有 lock
lock 必须防止等待的条件发生变化!lock 必须传给 sleep(),且必须在持有 lock 的情况下调用 wakeup()。
// sleep() 关键:在释放条件锁之前获取 p->lock
sleep(chan, lk):
acquire(&p->lock) // 先获取 p->lock
release(lk) // 再释放条件锁
p->state = SLEEPING
p->chan = chan
swtch() // 此时持有 p->lock
// 返回时:
acquire(lk) // 重新获取条件锁
release(&p->lock)
uartwrite() uartintr()
----------- ----------
acq tx_lock acq tx_lock
check tx_busy tx_busy = 0
sleep() wakeup()
acq p->lock acq p->lock(等待)
rel tx_lock ...
SLEEPING check SLEEPING / chan
swtch() rel p->lock
rel p->lock rel tx_lock
kill() 即使条件不为真也会唤醒进程条件:有数据可读(nread != nwrite)。条件锁:管道的 pi->lock。
如果 piperead 使用 broken_sleep(先释放锁再 sleep),多个读进程之间也会发生 lost wakeup。
线程不能释放自己的所有资源:
p->state = ZOMBIE(不能是 UNUSED,因为仍在使用内核栈)p->state == ZOMBIE 的子进程freeproc() 释放:trapframe、页表、...、p->state = UNUSEDwait() 不仅是为了应用方便,也是操作系统需要的。
wait_lock(不是每个子进程的 p->lock,因为父进程不知道在等哪个子进程)线程 X 不能直接销毁线程 Y:
kill() 只设置 p->killed 标志p->killed,调用 kexit()usertrap() 中检查:不在任何中间操作,不持有锁,安全退出kill() 将 SLEEPING 改为 RUNNABLE(像 wakeup)。sleep 循环可能检查 p->killed(如 piperead、consoleread),有些不检查(如 virtio_disk.c——因为可能在文件系统更新中间,不能直接退出)。
Q1:"Lost wakeup"是什么问题?
Q2:sleep() 为什么需要 lock 参数?
Q3:sleep() 在释放条件锁之前先获取 p->lock 的目的是什么?
Q4:为什么 kill() 不能直接销毁目标进程?
Q5:为什么 exit() 不直接将 p->state 设为 UNUSED?
检查条件与进入 SLEEPING 之间的窗口导致 wakeup 丢失。通过重叠的锁消除窗口。
sleep(chan, lock) 要求调用者持有条件锁,在释放条件锁前先获取 p->lock。
exit 设 ZOMBIE 后由父进程清理;kill 设 killed 标志由目标自行退出。
sleep/wakeup 灵活但底层;更高层抽象如 semaphore、condition variable 更简洁。