拓冰建站拓冰建站
首页 / 资讯中心 / 正文

MIT 6.S081 xv6 源码精读 (Sleep and wakeup):sleep、wakeup

xv6 源码精读sleep 与 wakeup —— 用锁撑起「等待/唤醒」上篇讲了「进程怎么被调度切换」这篇讲「进程怎么主动等一个事件、再被别人叫醒」——sleep/wakeup是 xv6 里最微妙的一对原语核心难点是「丢失唤醒lost wakeup」。为什么要 sleep/wakeup从信号量说起xv6 之前的章节里进程之间靠调度和锁「互不干扰」但进程之间还需要有意协作一个等事件、另一个触发事件。最经典的模型是信号量semaphorestructsemaphore{structspinlocklock;intcount;};V生产者count 1通知「有资源了」。P消费者等count ! 0再count - 1取走资源。最初的错误写法busy-wait烧 CPUvoidV(structsemaphore*s){acquire(s-lock);s-count1;release(s-lock);}voidP(structsemaphore*s){while(s-count0)// 一直空转轮询;acquire(s-lock);s-count-1;release(s-lock);}消费者在while里空转白白浪费 CPU。正确方向是用sleep释放 CPU、等V之后再由wakeup唤醒voidV(structsemaphore*s){acquire(s-lock);s-count1;wakeup(s);// 有资源了叫醒在 s 上等的人release(s-lock);}voidP(structsemaphore*s){while(s-count0)sleep(s);// 没资源就睡把 CPU 让出去acquire(s-lock);s-count-1;release(s-lock);}难点一丢失唤醒lost wakeup表面上 OK但有个时序陷阱。设想P在第 10 行的while发现count0正要执行第 11 行sleep(s)时另一个 CPU 上的V插进来它把count改成 1调wakeup(s)——此时P还没睡wakeup找不到睡眠者啥也不做。P接着睡下去永远等不到count明明是 1却睡死了。根因P「检查条件」和「真正睡下」这两步不是原子的中间的空档被V钻了空子。错误修复死锁版把锁移进P想让「检查 睡眠」变原子voidP(structsemaphore*s){acquire(s-lock);// 想靠这把锁挡住 Vwhile(s-count0)sleep(s);// 但 sleep 时仍然持有锁s-count-1;release(s-lock);}这下V拿不到锁、无法在中间插手——但死锁了P睡的时候还攥着s-lockV永远卡在acquire(s-lock)上count永远不会加。难点二正确解法 —— sleep 帮你在「睡下的同时」放锁xv6 的招数是改sleep的接口调用方必须把一个「条件锁」lk传给sleep由sleep在把进程标记为 SLEEPING 之后、真正让出 CPU 之前原子地释放lk醒来时再帮调用方把lk重新拿回来voidP(structsemaphore*s){acquire(s-lock);while(s-count0)sleep(s,s-lock);// 关键把条件锁交给 sleeps-count-1;release(s-lock);}P持有s-lock的事实挡住了V在「检查count」和「调sleep」之间插进来(V要先拿s-lock)。而sleep内部「释放lk 置进程为 SLEEPING 切走」必须是一个不可分割的动作——这正是后面源码要证明的事。kernle/proc.csleep556553// Atomically release lock and sleep on chan.554// Reacquires lock when awakened.555void556sleep(void*chan,structspinlock*lk)557{558structproc*pmyproc();559560// Must acquire p-lock in order to561// change p-state and then call sched.562// Once we hold p-lock, we can be563// guaranteed that we wont miss any wakeup564// (wakeup locks p-lock),565// so its okay to release lk.566if(lk!p-lock){// DOC: sleeplock0567acquire(p-lock);// DOC: sleeplock1568release(lk);569}570571// Go to sleep.572p-chanchan;573p-stateSLEEPING;574575sched();576577// Tidy up.578p-chan0;579580// Reacquire original lock.581if(lk!p-lock){582release(p-lock);583acquire(lk);584}585}(1) 拿进程myproc()proc.c:558structproc*pmyproc();拿到当前进程p。myproc()我们在上一篇讲过——它关中断取c-proc返回 per-process 指针取出后即可放心开中断使用。(2) 关键的「换锁」动作proc.c:566–569if(lk!p-lock){acquire(p-lock);release(lk);}这是整个机制的核心。调用方进来时持有lk条件锁。这里做两件事先拿p-lock后面要改p-state、p-chan按 xv6 不变量必须持p-lock回想 sched 篇讲的「状态切换必须持锁」。再释放lk此时同时持有lk和p-lock两个锁然后释放lk把「条件锁」让给可能的V。为什么这个顺序能防「丢失唤醒」看wakeup它也先拿lk它自己的acquire在循环外由调用方持有 / 或进入循环拿p-lock。关键点此时要么——V在P检查count0之前就拿到lk、count1、调wakeup——那P醒来时count已是 1要么V在P已经acquire(p-lock)之后才拿到lk于是wakeup会等待p-lock等到sleep把进程标记为 SLEEPING 之后才能改它——wakeup就能看到 SLEEPING 并唤醒。两者必居其一所以绝不会「睡眠者睡下之前 wakeup 已经跑完」。这正是 xv6 book 那句「要么 waker 在检查前让条件成立要么 waker 在 SLEEPING 后严格检查」的含义。(3) 标记睡眠 切走proc.c:572–575p-chanchan;// 记录「我在哪个通道上等」p-stateSLEEPING;// 状态改为睡眠——这一步必须还在持 p-lock 时做sched();// 让出 CPUsched 会带 p-lock 切走调度器再放锁p-chan是「等待通道」 —— xv6 通常用「等待的那个内核对象的地址」当通道比如sleep(pipe, pipe-lock)。必须在持p-lock的前提下把状态改成SLEEPING再sched()切走。绝不能在释放p-lock之后才标记 SLEEPING否则wakeup可能在「释放锁」和「还没标记」之间看到 RUNNING错过唤醒。(4) 醒来后重新拿锁proc.c:578–584p-chan0;// 清通道if(lk!p-lock){release(p-lock);// 现在是调度器切回来的语境先放进程锁acquire(lk);// 再拿回当初的条件锁返回给调用方}进程被wakeup改成 RUNNABLE、调度器切回来后从sched()返回继续执行这里。把lk还回来调用方P就能继续count - 1。kernel/proc.cwakeup590587// Wake up all processes sleeping on chan.588// Must be called without any p-lock.589void590wakeup(void*chan)591{592structproc*p;593594for(pproc;pproc[NPROC];p){595acquire(p-lock);596if(p-stateSLEEPINGp-chanchan){597p-stateRUNNABLE;598}599release(p-lock);600}601}遍历进程表对每一个进程先拿p-lock—— 既是因为要改p-state也是为了和sleep的锁形成「互斥检查」防止两者互相错过。只把chan匹配且SLEEPING的进程改成RUNNABLE。真正被选中运行是调度器的事回到 sched/scheduler 篇。注释强调wakeup调用时必须不持有任何p-lock但必须持有条件锁lk由调用方保证。这样wakeup在循环里拿p-lock时才不会被「正要睡的进程」挡住出竞态。注意的点「虚假唤醒」与「循环调用 sleep」多个进程可能在同一通道上睡比如多个进程读同一个 pipe。一次wakeup把它们全叫醒但只有一个能抢到锁、读完数据其余醒来看count0条件不成立 —— 这就是虚假唤醒spurious wakeup。所以sleep必须放在while循环里看P的写法while(count0) sleep(...)——醒来再检查不成立就接着睡。if(lk ! p-lock)这个特殊分支如果调用方已经持有自己的p-lock再调sleep典型例子是wait()它本来就在持p-lock的语境里那就不必再acquire(p-lock)也跳过最后的release(p-lock)acquire(lk)——因为lk和p-lock是同一把锁再拿会死锁。仓库里wakeup1proc.c:605就是这种「调用方已持p-lock」的变体专门给exit()用。和 kill 的配合trap.c里kill(pid)也会在p-state SLEEPING时直接置RUNNABLEproc.c:627 —— 本质上等同一次针对单个进程的 wakeup。小结什么是丢失唤醒睡眠者「检查条件」和「真正睡下」非原子唤醒者在这之间跑完导致睡眠者永久错过唤醒。为什么 sleep 要带锁参数让sleep在「标记 SLEEPING 切走」的同时原子地释放条件锁避免死锁又避免丢失唤醒。sleep 内部锁顺序先拿p-lock再释放lkwakeup 持lk后逐个拿p-lock。这个交叉持锁保证「要么 waker 先成立条件要么 waker 之后看到 SLEEPING」。为什么 SLEEPING 必须在持 p-lock 时标记否则 wakeup 可能在「释放锁之后、标记之前」窗口看到 RUNNING错过唤醒。为什么 sleep 必须放在 while 循环里虚假唤醒 多等待者醒来要重新检查条件。下一篇可以读exit/waitxv6 book 7.7那是sleep/wakeup在「父等子进程退出」场景的完整实战正好把本篇的wakeup1和p-lock用法串起来。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门