Even as they spoke there came a blare of trumpets. Then there was a crash and a flash of flame and smoke. The waters of the Deeping-stream poured out hissing and foaming: they were choked no longer, a gaping hole was blasted in the wall. A host of dark shapes poured in.
—The Two Towers, Chapter 7, Book III
Locking primitives are important userspace tools for concurrent and parallel programming. Two main types of locks exist. Spinlocks consume processor time as a process waits, but are ideally suited for low-latency and low-overhead appliations when critical section lengths are very short. Other locks allow processes to sleep while waiting, which can better utilize processor time but results the higher overhead of sleeping and waking up processes.
In this studio, you will:
Please complete the required exercises below, as well as any optional enrichment exercises that you wish to complete.
As you work through these exercises, please record your answers, and when finished email your results to dferry@email.wustl.edu with the phrase Locks in the subject line.
Make sure that the name of each person who worked on these exercises is listed in the first answer, and make sure you number each of your responses so it is easy to match your responses with each exercise.
top
program and pressing 1.
critical_section()
function simultaneously. This is undesirable, since critical sections typically
protect important shared data. First we will build a spin lock to protect access
to the critical_section()
function.
Create an empty lock
and
unlock
function. These functions should take a pointer to a
volatile integer. Insert these functions into the code around the critical
section.
Note: Recall that the volatile
specifier tells the compiler
that the value of a variable may change. In this case, the compiler interprets
a volatile int*
to mean that the value pointed at by the pointer
may change unexpectedly, not that the pointer itself may change.
lock
and unlock
functions we'll use GCC's built-in atomic instructions. If we were working in
C++ we could use C++ 11's atomic instructions. If we didn't have access to GCC,
or if speed was very critical, we could implement these with assembly
instructions.
The atomic built-in functions are documented here.
For the spin lock we will use the function __atomic_compare_exchange()
. The first three arguments determine the semantic meaning of this
function: ptr, expected, and desired. When called,
this function atomically compares the contents of ptr and expected, and
if they are equal, writes the value of desired into ptr. The last
three arguments specify special memory orderings, but we'll just opt for a
strong ordering for this studio. The last three parameters to
invoke this function are as so:
__atomic_compare_exchange( ptr, expected, desired, false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)
To implement the lock function, you should check for the unlocked state
and write the value of the locked state. However, it's possible that this
function will fail (for example, if another process already holds the lock).
Thus, your lock function should attempt to swap the state of the lock variable,
and continue retrying indefinitely until it succeeds. WARNING: The swap
function will overwrite the value of expected
when it fails!
Implement the unlock function with the same command. However, we only expect
the unlock function to be called when we already hold the lock. If the
call to __atomic_compare_exchange()
fails, then rather than
retrying the function, you should print out an error message and return.
lock()
and unlock()
, as well
as your lock state pound-defines.
Unlocked: | 1 |
Locked: | 0 |
At least one process is sleeping: | any negative number |
Since the futex is designed to implement a semaphore, this means that processes lock and unlock the futex by atomic increments and decrements. When a process claims the futex, it atomically decrements the integer by one. When a process releases the futex, it atomically increments the integer by one. If two processes never conflict, then the value of the futex integer will always be zero or one, and no process will ever have to sleep (and thus, you will never need to make a futex system call).
However, if multiple processes try to lock the futex simultaneously, they will
decrement the integer value to be negative. In this case, a process that gets
some value less than zero will want to put itself to sleep, and the kernel
must become involved. The semantics and the particulars of this process
are documented at the man pages man 2 futex
and
man 7 futex
.
ret_val = __atomic_sub_fetch( lock_ptr, 1, __ATOMIC_ACQ_REL );
__atomic_store_n( lock_ptr, -1, __ATOMIC_RELEASE );
syscall( SYS_futex, lock_ptr, FUTEX_WAIT, -1, NULL );
lock()
function
ret_val = __atomic_add_fetch( lock_ptr, 1, __ATOMIC_ACQ_REL );
unlock()
function
syscall( SYS_futex, lock_ptr, FUTEX_WAKE, INT_MAX );
trace-cmd record -e
sched_switch
. Take a screen shot showing both behaviors.