What You Will Learn
- Why a plain spinlock can be wasteful when a data structure is read far more often than it’s written
- How the Linux kernel’s reader-writer spinlock lets many readers proceed concurrently while writers get exclusive access
- The
rwlock_tAPI and how it differs from ordinaryspinlock_t - A modern kernel module example protecting a shared list with
rwlock_t - Where reader-writer locking helps — and where it can quietly hurt you
- Why current kernel guidance increasingly favors RCU or seqlocks over plain rwlocks in hot paths
Prerequisites
The Problem With a Plain Spinlock for Read-Heavy Data
Picture a kernel data structure — say, a linked list of active connections or registered devices — that dozens of code paths look up constantly, but only occasionally insert into or remove from. A single ordinary spinlock protecting that list forces every reader to fully serialize behind every other reader, even though none of them are actually changing anything. On a multi-core system under load, that serialization can become a real bottleneck: readers queue up waiting for a lock that, semantically, they didn’t even need to fight each other for.
How a Reader-Writer Spinlock Solves It
A reader-writer spinlock (or “rwlock”) splits locking into two distinct request types:
- Read lock — granted immediately to any thread, as long as no writer currently holds the lock. Multiple readers can hold a read lock at the same time.
- Write lock — exclusive. A writer must wait for every current reader (and any other writer) to release the lock before it proceeds, and while a writer holds the lock, new readers must wait too.
The rwlock_t API
The reader-writer spinlock is declared with the rwlock_t type from <linux/rwlock.h>. Its API mirrors ordinary spinlocks closely — you simply choose the read or write variant depending on what the critical section actually does.
| Spinlock equivalent | rwlock reader call | rwlock writer call |
|---|---|---|
spin_lock() | read_lock() | write_lock() |
spin_unlock() | read_unlock() | write_unlock() |
spin_lock_irqsave() | read_lock_irqsave() | write_lock_irqsave() |
DEFINE_SPINLOCK() | DEFINE_RWLOCK(my_lock) | |
A Modern Kernel Module Example
Here is a small, self-contained example protecting a simple in-memory registry (an array-backed table of device names) with an rwlock_t. Lookups take the read lock; registration takes the write lock. This builds cleanly against current kernel headers (6.1 through 6.12+).
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/rwlock.h>
#include <linux/string.h>
#define pr_fmt(fmt) "epreg: " fmt
#define MAX_ENTRIES 8
static char registry[MAX_ENTRIES][32];
static int registry_count;
static DEFINE_RWLOCK(registry_lock);
/* Many callers may look up a name concurrently */
static bool registry_lookup(const char *name)
{
int i;
bool found = false;
read_lock(®istry_lock);
for (i = 0; i < registry_count; i++) {
if (!strcmp(registry[i], name)) {
found = true;
break;
}
}
read_unlock(®istry_lock);
return found;
}
/* Registration is rare and must be exclusive */
static int registry_add(const char *name)
{
write_lock(®istry_lock);
if (registry_count >= MAX_ENTRIES) {
write_unlock(®istry_lock);
return -ENOSPC;
}
strscpy(registry[registry_count], name, sizeof(registry[0]));
registry_count++;
write_unlock(®istry_lock);
return 0;
}
static int __init epreg_init(void)
{
registry_add("uart0");
registry_add("spi1");
pr_info("lookup uart0: %s\n", registry_lookup("uart0") ? "found" : "missing");
return 0;
}
static void __exit epreg_exit(void)
{
pr_info("registry module unloaded, %d entries\n", registry_count);
}
module_init(epreg_init);
module_exit(epreg_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala reader-writer spinlock demo");
Every call into registry_lookup() only takes a read lock, so any number of CPUs can look up device names at the same time. Only registry_add() takes the exclusive write lock, and only for the brief moment it takes to append an entry.
Real-World Use Cases
- Routing and lookup tables — structures consulted on every packet or request but updated rarely.
- Configuration and policy tables — read constantly by the data path, updated only through explicit administrative action.
- Device or driver registries — as shown in the example, where lookups vastly outnumber registrations.
Common Mistakes
| Taking a read lock and then trying to write | Modifying data while only holding read_lock() breaks the whole guarantee, since other readers may be running concurrently. |
| Long critical sections under a write lock | Because writers block every reader, a slow write path can stall large amounts of otherwise-independent read traffic. |
| Forgetting the IRQ-safe variants | If a lock can be taken from interrupt context, you need read_lock_irqsave()/write_lock_irqsave(), not the plain versions, to avoid deadlocking against an interrupt on the same CPU. |
Best Practices
- Only reach for
rwlock_twhen reads genuinely dominate writes by a wide margin — otherwise a plain spinlock is simpler and just as fast. - Keep the writer’s critical section as short as possible, since it blocks all readers.
- Match irqsave/irqrestore variants consistently if the lock is ever touched from interrupt context.
- For very read-heavy, latency-critical paths in modern kernel code, evaluate RCU or seqlocks as an alternative — see the note below.
Performance Considerations
Reader-writer spinlocks help most when the critical section is non-trivial in size and reads dominate. If reads are extremely short (a few instructions), the bookkeeping overhead of tracking reader counts can erase the benefit compared to a plain spinlock. It’s also worth knowing that a steady stream of readers can, on some implementations, delay a waiting writer for a long time — a fairness consideration to keep in mind for update-sensitive paths.
It’s worth noting explicitly for current kernel work: in the modern kernel, many read-mostly data structures that once used rwlock_t have migrated to RCU (Read-Copy-Update) or seqlocks, which allow readers to proceed with zero locking overhead at all. Reader-writer spinlocks remain a perfectly valid and widely used tool, but when you’re designing new latency-critical code today, it’s worth comparing against RCU before defaulting to rwlock_t.
Security Considerations
Because a write lock excludes all readers, an attacker-influenced code path that can force frequent or long-held writes on a shared rwlock can be used as a denial-of-service vector, starving legitimate readers. Bounding writer critical-section time and validating any user-controlled sizes before entering the write-locked section are both important defensive habits.
Summary / Key Takeaways
- Reader-writer spinlocks let unlimited concurrent readers through while giving writers exclusive access.
- The API mirrors
spinlock_tclosely:read_lock()/read_unlock()andwrite_lock()/write_unlock(). - They shine when reads vastly outnumber writes and critical sections aren’t trivially short.
- Modern kernel code increasingly considers RCU or seqlocks as an alternative for the hottest read paths.
Conclusion
Reader-writer spinlocks are a natural next step once you understand plain spinlocks: same fundamental idea of mutual exclusion, but smarter about who actually needs to be excluded from whom. For read-heavy kernel and driver data structures, they can meaningfully cut down on unnecessary serialization — and understanding when to reach for rwlock_t, versus a plain spinlock, versus RCU, is a core skill for anyone writing concurrent kernel code today.
Frequently Asked Questions
<linux/rwlock.h> provides the rwlock_t type and its associated read_lock()/write_lock() API family.
Yes — any number of readers can hold the read lock concurrently, as long as no writer currently holds or is waiting to acquire the write lock in a way that blocks them.
Yes. A writer only acquires the exclusive write lock once every current reader has released the read lock.
No — only when reads genuinely dominate writes and the critical section is large enough to justify the extra reader-tracking overhead. For very short critical sections, a plain spinlock can be just as fast or faster.
RCU (Read-Copy-Update) and seqlocks are commonly used in current kernel code for read-mostly data where readers should incur zero locking overhead.
If the lock can ever be taken from interrupt context, yes — use read_lock_irqsave()/write_lock_irqsave() to prevent deadlocks against an interrupt on the same CPU.
Continue Your Free Linux Kernel Development Course
This lecture is part of EmbeddedPathashala’s free Linux kernel programming and device driver course.
Previous Lecture Next Lecture
2 Comments