Introduction
The Linux kernel is a massively parallel environment—thousands of threads execute simultaneously, handling everything from network packets to filesystem operations. But how does Linux prevent chaos when multiple threads access shared data?
The answer lies in its synchronization primitives: atomic operations, spinlocks, semaphores, and more. These tools ensure that critical sections of code execute safely, even on multi-core systems.
In this post, we'll explore:
- Atomic operations – The simplest way to avoid race conditions.
- Spinlocks – Lightweight locks for short critical sections.
- Semaphores – Sleep-capable locks for longer waits.
- RCU (Read-Copy-Update) – A lock-free synchronization marvel.
1. Atomic Operations: The Building Blocks
What Are They?
Atomic operations guarantee that a single variable is read, modified, and written back without interruption.
Linux provides two types:
- Atomic integers (
atomic_t
) – For counters and flags. - Atomic bitmaps – For manipulating individual bits.
Key Functions
1atomic_t counter = ATOMIC_INIT(0);23atomic_inc(&counter); // Thread-safe increment4if (atomic_dec_and_test(&counter)) {5 // Do something if counter reaches zero6}78set_bit(3, &bitmask); // Set bit 3 atomically
Why Use Them?
✔ No locks needed – Faster than spinlocks for simple operations. ✔ Guaranteed consistency – No race conditions on single variables.
2. Spinlocks: Hold Tight Until You Get the Lock
How They Work
A spinlock is a busy-waiting lock:
- If the lock is free, the thread acquires it.
- If not, the thread spins (repeatedly checks) until the lock is released.
Types of Spinlocks
Type | Use Case |
---|---|
spin_lock() | Basic lock (no interrupt handling). |
spin_lock_irq() | Disables interrupts (for IRQ handlers). |
spin_lock_irqsave() | Saves interrupt state before disabling. |
spin_lock_bh() | Disables "bottom halves" (softirqs). |
Example
1spinlock_t lock;2spin_lock_init(&lock);34spin_lock(&lock);5// Critical section (e.g., update shared list)6spin_unlock(&lock);
When to Use Spinlocks?
✔ Short critical sections (e.g., updating a pointer). ❌ Never in userland – Wastes CPU cycles if held too long.
3. Semaphores: Sleep Instead of Spin
How They Differ from Spinlocks
- If a semaphore is locked, the thread sleeps instead of spinning.
- Ideal for longer waits (e.g., waiting for I/O).
Types of Semaphores
Type | Description |
---|---|
Binary (Mutex) | init_MUTEX() – Locks like a mutex. |
Counting | sema_init() – Allows N threads at once. |
Reader-Writer | down_read() / down_write() – Optimized for reads. |
Example
1struct semaphore sem;2sema_init(&sem, 1); // Binary semaphore34down(&sem); // Sleep if locked5// Critical section6up(&sem); // Release
4. RCU: Lock-Free Magic for Readers
How It Works
- Readers access data without locks.
- Writers create a new copy, update it, and atomically switch pointers.
Key Functions
1rcu_read_lock();2// Read shared data (no locks!)3rcu_read_unlock();45// Writer updates:6new_data = kmalloc(...);7rcu_assign_pointer(shared_ptr, new_data);8synchronize_rcu(); // Wait for all readers to finish9kfree(old_data);
Why RCU?
✔ Zero overhead for readers – Perfect for read-heavy workloads (e.g., routing tables). ✔ Scalable – No lock contention.
5. Memory Barriers: Controlling Instruction Reordering
Modern CPUs reorder instructions for performance. But sometimes, order matters:
1a = 1;2b = 2; // CPU might execute this BEFORE `a = 1`!
Linux provides barriers to enforce ordering:
mb()
– Full memory barrier (no loads/stores reordered).rmb()
– Read barrier.wmb()
– Write barrier.
Real-World Use Cases
- Network Stack – RCU protects routing tables.
- Filesystems – Spinlocks guard inode caches.
- Device Drivers – Semaphores manage hardware access.
Key Takeaways
- Atomic ops for simple variables.
- Spinlocks for very short critical sections.
- Semaphores for sleepable waits.
- RCU for read-mostly data.
- Barriers when order matters.