7.5 Sleep and wakeup

Scheduling and locks help conceal the actions of one thread from another, but we also need abstractions that help threads intentionally interact. For example, the reader of a pipe in xv6 may need to wait for a writing process to produce data; a parent’s call to wait may need to wait for a child to exit; and a process reading the disk needs to wait for the disk hardware to finish the read. The xv6 kernel uses a mechanism called sleep and wakeup in these situations (and many others). Sleep allows a kernel thread to wait for a specific event; another thread can call wakeup to indicate that threads waiting for a specified event should resume. Sleep and wakeup are often called sequence coordination or conditional synchronization mechanisms.

Sleep and wakeup provide a relatively low-level synchronization interface. To motivate the way they work in xv6, we’ll use them to build a higher-level synchronization mechanism called a semaphore [2] that coordinates producers and consumers (xv6 does not use semaphores). A semaphore maintains a count and provides two operations. The “V” operation (for the producer) increments the count. The “P” operation (for the consumer) waits until the count is non-zero, and then decrements it and returns. If there were only one producer thread and one consumer thread, and they executed on different CPUs, and the compiler didn’t optimize too aggressively, this implementation would be correct:


              100
              
              
              
              struct semaphore {


              101
              
              
              
                struct spinlock lock;


              102
              
              
              
                int count;


              103
              
              
              
              };


              104
              
              
              
            


              105
              
              
              
              void


              106
              
              
              
              V(struct semaphore *s)


              107
              
              
              
              {


              108
              
              
              
                 acquire(&s->lock);


              109
              
              
              
                 s->count += 1;


              110
              
              
              
                 release(&s->lock);


              111
              
              
              
              }


              112
              
              
              
            


              113
              
              
              
              void


              114
              
              
              
              P(struct semaphore *s)


              115
              
              
              
              {


              116
              
              
              
                 while(s->count == 0)


              117
              
              
              
                   ;


              118
              
              
              
                 acquire(&s->lock);


              119
              
              
              
                 s->count -= 1;


              120
              
              
              
                 release(&s->lock);


              121
              
              
              
              }

The implementation above is expensive. If the producer acts rarely, the consumer will spend most of its time spinning in the while loop hoping for a non-zero count. The consumer’s CPU could probably find more productive work than busy waiting by repeatedly polling s->count. Avoiding busy waiting requires a way for the consumer to yield the CPU and resume only after V increments the count.

Here’s a step in that direction, though as we will see it is not enough. Let’s imagine a pair of calls, sleep and wakeup, that work as follows. sleep(chan) waits for an event designated by the value of chan, called the wait channel. sleep puts the calling process to sleep, releasing the CPU for other work. wakeup(chan) wakes all processes that are in calls to sleep with the same chan (if any), causing their sleep calls to return. If no processes are waiting on chan, wakeup does nothing. We can change the semaphore implementation to use sleep and wakeup (changes highlighted in yellow):


              200
              
              
              
              void


              201
              
              
              
              V(struct semaphore *s)


              202
              
              
              
              {


              203
              
              
              
                 acquire(&s->lock);


              204
              
              
              
                 s->count += 1;


              205
              
              
              
                 wakeup(s);


              206
              
              
              
                 release(&s->lock);


              207
              
              
              
              }


              208
              
              
              
            


              209
              
              
              
              void


              210
              
              
              
              P(struct semaphore *s)


              211
              
              
              
              {


              212
              
              
              
                while(s->count == 0)    


              213
              
              
              
                  sleep(s);  


              214
              
              
              
                acquire(&s->lock);


              215
              
              
              
                s->count -= 1;


              216
              
              
              
                release(&s->lock);


              217
              
              
              
              }

P now gives up the CPU instead of spinning, which is nice. However, it turns out not to be straightforward to design sleep and wakeup with this interface without suffering from what is known as the lost wake-up problem. Suppose that P finds that s->count == 0 on line 212. While P is between lines 212 and 213, V runs on another CPU: it changes s->count to be nonzero and calls wakeup, which finds no processes sleeping and thus does nothing. Now P continues executing at line 213: it calls sleep and goes to sleep. This causes a problem: P is asleep waiting for a V call that has already happened. Unless we get lucky and the producer calls V again, the consumer will wait forever even though the count is non-zero.

The root of this problem is that the invariant that P sleeps only when s->count == 0 is violated by V running at just the wrong moment. An incorrect way to protect the invariant would be to move the lock acquisition (highlighted in yellow below) in P so that its check of the count and its call to sleep are atomic:


              300
              
              
              
              void


              301
              
              
              
              V(struct semaphore *s)


              302
              
              
              
              {


              303
              
              
              
                acquire(&s->lock);


              304
              
              
              
                s->count += 1;


              305
              
              
              
                wakeup(s);


              306
              
              
              
                release(&s->lock);


              307
              
              
              
              }


              308
              
              
              
            


              309
              
              
              
              void


              310
              
              
              
              P(struct semaphore *s)


              311
              
              
              
              {


              312
              
              
              
                acquire(&s->lock);


              313
              
              
              
                while(s->count == 0)    


              314
              
              
              
                  sleep(s);             


              315
              
              
              
                s->count -= 1;


              316
              
              
              
                release(&s->lock);


              317
              
              
              
              }

One might hope that this version of P would avoid the lost wakeup because the lock prevents V from executing between lines 313 and 314. It does that, but it also deadlocks: P holds the lock while it sleeps, so V will block forever waiting for the lock.

We’ll fix the preceding scheme by changing sleep’s interface: the caller must pass the condition lock to sleep so it can release the lock after the calling process is marked as asleep and waiting on the sleep channel. The lock will force a concurrent V to wait until P has finished putting itself to sleep, so that the wakeup will find the sleeping consumer and wake it up. Once the consumer is awake again sleep reacquires the lock before returning. Our new correct sleep/wakeup scheme is usable as follows (change highlighted in yellow):


              400
              
              
              
              void


              401
              
              
              
              V(struct semaphore *s)


              402
              
              
              
              {


              403
              
              
              
                acquire(&s->lock);


              404
              
              
              
                s->count += 1;


              405
              
              
              
                wakeup(s);


              406
              
              
              
                release(&s->lock);


              407
              
              
              
              }


              408
              
              
              
            


              409
              
              
              
              void


              410
              
              
              
              P(struct semaphore *s)


              411
              
              
              
              {


              412
              
              
              
                acquire(&s->lock);


              413
              
              
              
                while(s->count == 0)


              414
              
              
              
                   sleep(s, &s->lock);


              415
              
              
              
                s->count -= 1;


              416
              
              
              
                release(&s->lock);


              417
              
              
              
              }

The fact that P holds s->lock prevents V from trying to wake it up between P’s check of s->count and its call to sleep. However, sleep must release s->lock and put the consuming process to sleep in a way that’s atomic from the point of view of wakeup, in order to avoid lost wakeups.