SX(9)

πŸ’– SX(9) β€” kernel shared/exclusive lock πŸ’–

okay so bestie, pull up a chair, because this is SX(9) from the Kernel Developer's Manual and it's literally the blueprint for shared/exclusive locks in the kernel. it's giving "reads all day, writes almost never" energy and i am obsessed. let's get into it 🫢


✨ NAME ✨

sx, sx_init, sx_init_flags, sx_destroy, sx_slock, sx_xlock, sx_slock_sig, sx_xlock_sig, sx_try_slock, sx_try_xlock, sx_sunlock, sx_xunlock, sx_unlock, sx_try_upgrade, sx_downgrade, sx_sleep, sx_xholder, sx_xlocked, sx_assert, SX_SYSINIT, SX_SYSINIT_FLAGS β€” that's the whole squad, and they all exist to serve one iconic purpose: the kernel shared/exclusive lock. πŸ’…


✨ SYNOPSIS ✨

first, the includes, because obviously:

#include <sys/param.h>
#include <sys/lock.h>
#include <sys/sx.h>

setting up and tearing down:

void
sx_init(struct sx *sx, const char *description);

void
sx_init_flags(struct sx *sx, const char *description, int opts);

void
sx_destroy(struct sx *sx);

getting your shared and exclusive locks, bestie:

void
sx_slock(struct sx *sx);

void
sx_xlock(struct sx *sx);

int
sx_slock_sig(struct sx *sx);

int
sx_xlock_sig(struct sx *sx);

int
sx_try_slock(struct sx *sx);

int
sx_try_xlock(struct sx *sx);

and letting them go (the healthy thing to do):

void
sx_sunlock(struct sx *sx);

void
sx_xunlock(struct sx *sx);

void
sx_unlock(struct sx *sx);

the glow-up and the downgrade, we love character development:

int
sx_try_upgrade(struct sx *sx);

void
sx_downgrade(struct sx *sx);

napping on a channel while holding the lock (iconic behaviour, more on that later):

int
sx_sleep(void *chan, struct sx *sx, int priority, const char *wmesg,
    int timo);

who's holding it, and do i hold it:

struct thread *
sx_xholder(struct sx *sx);

int
sx_xlocked(const struct sx *sx);

and for the paranoid girlies (affectionate), the invariants build options:

options INVARIANTS
options INVARIANT_SUPPORT

void
sx_assert(const struct sx *sx, int what);

plus the sysinit macros, which need a different header:

#include <sys/kernel.h>

SX_SYSINIT(name, struct sx *sx, const char *desc);

SX_SYSINIT_FLAGS(name, struct sx *sx, const char *desc, int flags);

✨ DESCRIPTION ✨

okay so the whole concept: shared/exclusive locks protect data that gets read wayyyyy more often than it gets written. that's the vibe. that's the entire point.

🚨 but listen. shared/exclusive locks do not implement priority propagation like mutexes and reader/writer locks do to prevent priority inversions. so bestie, use them prudently. no cap.

you make one with either sx_init() or sx_init_flags(), where sx is a pointer to space for a struct sx, and description is a pointer to a null-terminated character string that describes the shared/exclusive lock (yes, a whole string, be descriptive, it's cute).

the opts argument to sx_init_flags() is a set of optional flags that alter the behavior of sx, and it contains one or more of these:

  • SX_NOADAPTIVE β€” disable adaptive spinning (instead of sleeping) for lock operations while an exclusive lock holder is executing on another CPU. adaptive spinning is the default unless the kernel was compiled with options NO_ADAPTIVE_SX. πŸ’€
  • SX_DUPOK β€” witness should not log messages about duplicate locks being acquired. keep it quiet, keep it cute.
  • SX_NOWITNESS β€” tells witness(4) to ignore this lock entirely. she's not seeing it.
  • SX_NOPROFILE β€” do not profile this lock.
  • SX_RECURSE β€” allow threads to recursively acquire exclusive locks for sx. πŸ”
  • SX_QUIET β€” do not log any operations for this lock via ktr(4). shhh. πŸ™ˆ
  • SX_NEW β€” if the kernel was compiled with options INVARIANTS, sx_init() will assert that the sx has not been initialized multiple times without intervening calls to sx_destroy() β€” unless this option is specified. so if you're re-initializing, say so. periodt.

shared/exclusive locks get destroyed with sx_destroy().

⚠️ the lock sx must not be locked by any thread when it is destroyed. like, nobody. not even one. this is not a drill.

acquiring and releasing, the whole choreography:

  • threads acquire and release a shared lock by calling sx_slock(), sx_slock_sig() or sx_try_slock() and sx_sunlock() or sx_unlock().
  • threads acquire and release an exclusive lock by calling sx_xlock(), sx_xlock_sig() or sx_try_xlock() and sx_xunlock() or sx_unlock().
  • a thread can attempt to upgrade a currently held shared lock to an exclusive lock by calling sx_try_upgrade().
  • a thread that has an exclusive lock can downgrade it to a shared lock by calling sx_downgrade().

the return values, because numbers matter:

  • sx_try_slock() and sx_try_xlock() return 0 if the shared/exclusive lock cannot be acquired immediately; otherwise the lock will be acquired and a non-zero value will be returned. slay.
  • sx_try_upgrade() returns 0 if the shared lock cannot be upgraded to an exclusive lock immediately; otherwise the exclusive lock will be acquired and a non-zero value will be returned.
  • sx_slock_sig() and sx_xlock_sig() do the same as their normal versions but perform an interruptible sleep. they return a non-zero value if the sleep has been interrupted by a signal or an interrupt, otherwise 0.

a thread can atomically release a shared/exclusive lock while waiting for an event by calling sx_sleep(). for more details on the parameters to this function, go read sleep(9) β€” i'm not retyping her whole biography here. 🫢

the assertions, when compiled with options INVARIANTS and options INVARIANT_SUPPORT:

sx_assert() tests sx for the assertions specified in what, and panics if they are not met. 😭 one of the following assertions must be specified:

  • SA_LOCKED β€” assert that the current thread has either a shared or an exclusive lock on the sx lock pointed to by the first argument.
  • SA_SLOCKED β€” assert that the current thread has a shared lock on the sx lock pointed to by the first argument.
  • SA_XLOCKED β€” assert that the current thread has an exclusive lock on the sx lock pointed to by the first argument.
  • SA_UNLOCKED β€” assert that the current thread has no lock on the sx lock pointed to by the first argument.

in addition, one of the following optional assertions may be included with either an SA_LOCKED, SA_SLOCKED, or SA_XLOCKED assertion:

  • SA_RECURSED β€” assert that the current thread has a recursed lock on sx.
  • SA_NOTRECURSED β€” assert that the current thread does not have a recursed lock on sx.

the little helpers:

  • sx_xholder() will return a pointer to the thread which currently holds an exclusive lock on sx. if no thread holds an exclusive lock on sx, then NULL is returned instead. nobody home. 🏠
  • sx_xlocked() will return non-zero if the current thread holds the exclusive lock; otherwise, it will return zero.

for ease of programming, sx_unlock() is provided as a macro frontend to the respective functions, sx_sunlock() and sx_xunlock(). algorithms that are aware of what state the lock is in should use either of the two specific functions for a minor performance benefit. iykyk. πŸ’…

the sysinit macros:

  • the SX_SYSINIT() macro is used to generate a call to the sx_sysinit() routine at system startup in order to initialize a given sx lock. the parameters are the same as sx_init() but with an additional argument, name, that is used in generating unique variable names for the related structures associated with the lock and the sysinit routine.
  • the SX_SYSINIT_FLAGS() macro can similarly be used to initialize a given sx lock using sx_init_flags().
πŸ’€ DEADLOCK ALERT: a thread may not hold both a shared lock and an exclusive lock on the same lock simultaneously. attempting to do so will result in deadlock. not "might". will. do not test this.

✨ CONTEXT ✨

a thread may hold a shared or exclusive lock on an sx lock while sleeping. πŸŒ™

as a result, an sx lock may not be acquired while holding a mutex. because otherwise, if one thread slept while holding an sx lock while another thread blocked on the same sx lock after acquiring a mutex, then the second thread would effectively end up sleeping while holding a mutex, which is not allowed. that's the whole explanation, it's giving domino effect, it's giving chaos. πŸ™…β€β™€οΈ


✨ SEE ALSO ✨

lock(9), locking(9), mutex(9), panic(9), rwlock(9), sema(9)

go read them, bestie. expand your horizons. πŸ¦‹


✨ BUGS ✨

okay so here's the messy part, and we're being honest about it:

  • a kernel without WITNESS cannot assert whether the current thread does or does not hold a shared lock.
  • SA_LOCKED and SA_SLOCKED can only assert that any thread holds a shared lock. they cannot ensure that the current thread holds a shared lock.
  • further, SA_UNLOCKED can only assert that the current thread does not hold an exclusive lock.

so like. she's trying her best but she has limits. 😭


and that's the whole page, babe. this little gem came out of the Debian build of the kernel developer's manual, dated November 11, 2017 β€” yeah she's an elder but she still ate. πŸŽ€ go forth, lock responsibly, and never hold a shared and an exclusive at the same time. mwah. πŸ’–βœ¨πŸ‘‘

πŸ“¦ where this page comes from