Understanding Linux Kernel Locks: Spinlocks, Semaphores, RWLocks, and Mutexes Explained
This article explains the design, principles, and C implementations of Linux kernel synchronization primitives—including spinlocks, semaphores, read‑write locks, and mutexes—illustrated with real‑world analogies, code snippets, and usage constraints in interrupt and multi‑CPU contexts.
Spinlock
Spinlocks are simple busy‑wait locks used in the Linux kernel for short critical sections, especially in interrupt context. They use two counters – next (the ticket to acquire) and owner (the ticket currently holding the lock). A thread spins until next == owner.
typedef struct {
union {
unsigned int slock;
struct __raw_tickets {
unsigned short owner;
unsigned short next;
} tickets;
};
} arch_spinlock_t;
static inline void arch_spin_lock(arch_spinlock_t *lock) {
arch_spinlock_t old_lock;
old_lock.slock = lock->slock; /* save current state */
lock->tickets.next++; /* claim a ticket */
while (old_lock.tickets.next != old_lock.tickets.owner) { /* wait */
old_lock.tickets.owner = lock->tickets.owner; /* reload owner */
}
}
static inline void arch_spin_unlock(arch_spinlock_t *lock) {
lock->tickets.owner++; /* release */
}When a spinlock is held and an interrupt on the same CPU tries to acquire the same lock, the kernel disables preemption (or disables local interrupts) while the lock is held to avoid a dead‑spin. On multi‑CPU systems the dead‑spin only occurs if the interrupt is serviced on the same CPU; otherwise the lock is released quickly by the other CPU.
Semaphore
A semaphore is a counting object that can put a contending process to sleep, allowing the scheduler to run other tasks. It is typically used to coordinate access to a limited number of identical resources.
struct semaphore {
unsigned int count;
struct list_head wait_list;
};
struct semaphore_waiter {
struct list_head list;
struct task_struct *task;
};
void down(struct semaphore *sem) {
if (sem->count > 0) {
sem->count--; /* acquire */
return;
}
struct semaphore_waiter waiter;
waiter.task = current; /* record current task */
list_add_tail(&waiter.list, &sem->wait_list); /* enqueue */
schedule(); /* sleep */
}
void up(struct semaphore *sem) {
if (list_empty(&sem->wait_list)) {
sem->count++; /* release */
return;
}
struct semaphore_waiter waiter =
list_first_entry(&sem->wait_list, struct semaphore_waiter, list);
list_del(&waiter.list); /* dequeue */
wake_up_process(waiter.task); /* wake one waiter */
}The down operation decrements the count if a resource is available; otherwise the calling task is placed on the semaphore’s wait list and put to sleep via schedule(). The up operation increments the count or wakes the first waiting task.
Read‑Write Lock
A read‑write lock allows multiple concurrent readers but only a single writer. The implementation packs a writer flag in the high bit (bit 31) and a reader count in the low 31 bits of a single unsigned int.
typedef struct {
volatile unsigned int lock;
} arch_rwlock_t;
static inline void arch_read_lock(arch_rwlock_t *rw) {
unsigned int tmp;
do {
tmp = rw->lock;
tmp++; /* increment reader count */
} while (tmp & (1U << 31)); /* retry if writer flag set */
rw->lock = tmp;
}
static inline void arch_read_unlock(arch_rwlock_t *rw) {
rw->lock--;
}
static inline void arch_write_lock(arch_rwlock_t *rw) {
while (rw->lock) {} /* wait until no readers/writer */
rw->lock = 1U << 31; /* set writer flag */
}
static inline void arch_write_unlock(arch_rwlock_t *rw) {
rw->lock = 0;
}Because readers continuously increment the low‑order count, a steady stream of readers can starve a waiting writer (writer starvation).
Mutex
A mutex is a binary semaphore with the additional rule that only the lock owner may release it. It protects mutable global or static data.
struct mutex_waiter {
struct list_head list;
struct task_struct *task;
};
struct mutex {
long owner; /* task that holds the lock */
struct list_head wait_list; /* queue of sleepers */
};
void mutex_take(struct mutex *mutex) {
struct mutex_waiter waiter;
if (!mutex->owner) {
mutex->owner = (long)current; /* acquire */
return;
}
waiter.task = current;
list_add_tail(&waiter.list, &mutex->wait_list); /* enqueue */
schedule(); /* sleep */
}
int mutex_release(struct mutex *mutex) {
if (mutex->owner != (long)current) /* only owner may release */
return -1;
if (list_empty(&mutex->wait_list)) {
mutex->owner = 0; /* unlock */
return 0;
}
struct mutex_waiter *waiter =
list_first_entry(&mutex->wait_list, struct mutex_waiter, list);
list_del(&waiter->list);
mutex->owner = (long)waiter->task; /* transfer ownership */
wake_up_process(waiter->task); /* wake next waiter */
return 0;
}Interrupt‑context and bottom‑half considerations
When a spinlock protects data accessed from interrupt handlers, it must be combined with local‑interrupt disabling ( local_irq_disable() / local_irq_enable()) to avoid dead‑spins caused by an interrupt on the same CPU acquiring the same lock. For bottom‑half mechanisms (softirqs, tasklets) the same rule applies: a writer must prevent concurrent readers, but the kernel can often handle bottom‑half cases without disabling interrupts.
Link: https://www.cnblogs.com/heyongshen/p/16827111.html
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Liangxu Linux
Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
