Β§28.15–28.17Different OS, Different Support … Summary

Part II OSTEP pp. 332–334 Β· ~5 min read

  • futex
  • two-phase lock

Solaris gave us park and unpark. Other kernels support sleeping locks their own way β€” and Linux’s way is worth reading closely, because it’s what actually runs beneath your pthread_mutex.

28.15 Different OS, Different Support

Linux provides the futex : each one is a physical memory word with a per-futex in-kernel queue. Two calls: futex_wait(address, expected) puts the caller to sleep β€” unless the value at address no longer equals expected, in which case it returns immediately (closing the wakeup/waiting race by construction); futex_wake(address) wakes one queued waiter. The real glibc lock built on them (Figure 28.10, from lowlevellock.h):

void mutex_lock (int *mutex) {
  int v;
  /* Bit 31 was clear, we got the mutex (the fastpath) */
  if (atomic_bit_test_set (mutex, 31) == 0)
    return;
  atomic_increment (mutex);
  while (1) {
      if (atomic_bit_test_set (mutex, 31) == 0) {
          atomic_decrement (mutex);
          return;
      }
      /* We have to wait. First make sure the futex value
         we are monitoring is truly negative (locked). */
      v = *mutex;
      if (v >= 0)
        continue;
      futex_wait (mutex, v);
  }
}

void mutex_unlock (int *mutex) {
  /* Adding 0x80000000 to counter results in 0 if and
     only if there are not other interested threads */
  if (atomic_add_zero (mutex, 0x80000000))
    return;

  /* There are other threads waiting for this mutex,
     wake one of them up. */
  futex_wake (mutex);
}

Two elegances. One integer carries everything: the high bit says held-or-free (set it and the integer goes negative β€” locked), and the remaining bits count waiters. And the common case is optimized: an uncontended acquire-release is one atomic bit-test-set in, one atomic add out β€” no kernel, no queue, no waiting machinery. Puzzle through the rest and become a master of Linux locking (or at least somebody who listens when a book tells you to do something).

28.16 Two-Phase Locks

The Linux lock echoes an old idea β€” Dahm locks, early 1960s β€” now called the two-phase lock . Insight: spinning is useful when the lock is about to be released. So phase one spins a while, hoping to grab the lock cheaply; failing that, phase two sleeps, waking only when the lock frees. The glibc code above is a one-spin variant; a generalization spins in a loop for a fixed time first. Another hybrid, like so many before β€” and whether it wins depends on hardware, thread counts, and workload details. Making a single general-purpose lock, good for all cases, remains quite a challenge.

28.17 Summary

Real locks today: some hardware support (a powerful atomic instruction) plus some OS support (park/unpark, futex) β€” highly tuned, endlessly refined. The full lineup, graded:

The whole chapter in one table: every lock design, graded on the rubric
mutual exclusionfair?1 CPU, contendedmany CPUs
disable interruptsβœ“ (1 CPU only)β€”fineβœ— broken
flag (loads/stores)βœ— FAILSβ€”β€”β€”
TAS spin lockβœ“βœ— can starvepainfulreasonable
ticket lockβœ“βœ“ progress for allstill spinsreasonable
TAS + yieldβœ“βœ— can starvebetterreasonable
queue + park/unparkβœ“βœ“ FIFO wakeupsgoodgood
futex (Linux)βœ“βœ“ kernel queuegoodgood β€” and the uncontended path is one atomic op
two-phaseβœ“inherits phase 2'sgoodgood β€” workload-dependent
Dotted-underlined cells have explanations β€” click one.

For more, read the Solaris or Linux lock code itself β€” a fascinating read β€” or David et al.’s comparison of locking strategies on modern multiprocessors.

Homework: x86.py

Build the chapter’s locks in toy assembly and torture them: flag.s (find where it breaks), test-and-set.s (does it always work? how much CPU does it waste?), peterson.s (prove mutual exclusion by controlling schedules with -P), ticket.s (watch spin time grow with threads), yield.s (count the instructions saved), and test-and-test-and-set.s (what does the extra test buy?). Get it at ostep-homework.

Check yourself

1.How do Linux's futex_wait and futex_wake work, and what's clever about the way glibc packs the lock into ONE integer?

2.The glibc mutex_lock 'fastpath' is a single atomic_bit_test_set. What case is being optimized, and why is that the right target?

3.Define the two-phase lock, and its rationale.

4.Assemble the chapter's verdict: what does a REAL modern lock consist of?

5.From the chapter's full lineup β€” interrupt-off, flag, TAS spin, ticket, TAS+yield, park queue, futex β€” which designs guarantee FAIRNESS (no starvation)?

5 questions