6.2 Code: Locks
Xv6 has two types of locks: spinlocks and sleep-locks.
We’ll start with spinlocks.
Xv6 represents a spinlock as a
struct spinlock
(kernel/spinlock.h:2).
The important field in the structure is
locked
,
a word that is zero when the lock is available
and non-zero when it is held.
Logically, xv6 should acquire a lock by executing code like
21 void
22 acquire(struct spinlock *lk) // does not work!
23 {
24 for(;;) {
25 if(lk->locked == 0) {
26 lk->locked = 1;
27 break;
28 }
29 }
30 }
Unfortunately, this implementation does not
guarantee mutual exclusion on a multiprocessor.
It could happen that two CPUs simultaneously
reach line 25, see that
lk->locked
is zero, and then both grab the lock by executing line 26.
At this point, two different CPUs hold the lock,
which violates the mutual exclusion property.
What we need is a way to
make lines 25 and 26 execute as an
atomic
(i.e., indivisible) step.
Because locks are widely used,
multi-core processors usually provide instructions that
implement an atomic version of
lines 25 and 26.
On the RISC-V this instruction is
amoswap r, a
.
amoswap
reads the value at the memory address a,
writes the contents of register r to that address,
and puts the value it read into r.
That is, it swaps the contents of the register and the memory address.
It performs this sequence atomically, using special
hardware to prevent any
other CPU from using the memory address between the read and the write.
Xv6’s
acquire
(kernel/spinlock.c:22)
uses the portable C library call
__sync_lock_test_and_set
,
which boils down to the
amoswap
instruction;
the return value is the old (swapped) contents of
lk->locked
.
The
acquire
function wraps the swap in a loop, retrying (spinning) until it has
acquired the lock.
Each iteration swaps one into
lk->locked
and checks the previous value;
if the previous value is zero, then we’ve acquired the
lock, and the swap will have set
lk->locked
to one.
If the previous value is one, then some other CPU
holds the lock, and the fact that we atomically swapped one into
lk->locked
didn’t change its value.
Once the lock is acquired,
acquire
records, for debugging, the CPU
that acquired the lock.
The
lk->cpu
field is protected by the lock
and must only be changed while holding the lock.
The function
release
(kernel/spinlock.c:47)
is the opposite of
acquire
:
it clears the
lk->cpu
field
and then releases the lock.
Conceptually, the release just requires assigning zero to
lk->locked
.
The C standard allows compilers to implement an assignment
with multiple store instructions,
so a C assignment might be non-atomic with respect
to concurrent code.
Instead,
release
uses the C library function
__sync_lock_release
that performs an atomic assignment.
This function also boils down to a RISC-V
amoswap
instruction.