Reader-Writer Locks in the Linux Kernel: rwlock, rwsem & Seqlock Explained-Free Linux Device Driver Training in Hyderabad

Reader-Writer Locks in the Linux Kernel: rwlock, rwsem & Seqlock Explained
Free Linux Kernel Development Course — Kernel Synchronization Series, Lecture 12
⏱ 18 min read
🎯 Intermediate
🐧 Kernel 6.12+

Most of the locking primitives you meet early in kernel work — the plain spinlock and the mutex — treat every thread of execution the same way: whoever wants the lock has to wait for exclusive access, whether it’s reading a value or changing it. That’s wasteful for data that is read constantly but written rarely, which describes a huge amount of kernel state: routing tables, process lists, filesystem metadata caches, and more. This lecture, part of our free Linux kernel development course, covers the family of locks the kernel provides for exactly this pattern — reader-writer spinlocks, reader-writer semaphores, and sequence locks — and explains where each one still earns its place in a modern 6.12+ kernel.

Topics Covered
rwlock_t rw_semaphore seqlock Writer Starvation RCU vs rwlock Fair Locking

What You Will Learn

  • Why a plain spinlock or mutex is the wrong tool for read-heavy shared data
  • How reader-writer spinlocks and reader-writer semaphores actually work
  • The writer-starvation problem, and how the kernel’s fair rwlock implementation addresses it today
  • What a sequence lock is and when it beats a reader-writer lock outright
  • Why RCU has quietly replaced rwlock_t in most of the paths that used to rely on it

Prerequisites

You should already be comfortable with basic spinlocks and mutexes, along with the general idea of a race condition and a critical section. If those terms feel shaky, go back to the earlier lectures in this free Linux kernel development course on kernel synchronization basics before continuing.

Why Ordinary Locks Fall Short for Read-Heavy Data

Picture a kernel data structure that a dozen CPUs read every few microseconds, but which is only modified once every few seconds — a network interface’s statistics block is a reasonable example. If you protect it with a plain spinlock, every one of those frequent reads still forces full mutual exclusion: only one CPU at a time can even look at the data, even though looking doesn’t change anything. On a busy multi-core system that serializes work that never needed to be serialized in the first place, and cache-line bouncing between cores makes it worse.

A reader-writer lock relaxes this: any number of readers can hold the lock at the same time, as long as no writer is active. A writer, on the other hand, still needs full exclusive access — no readers and no other writer may be present while it runs.

Reader-Writer Lock — Who Can Run Together
Lock Held By New Reader Allowed? New Writer Allowed?
No one Yes Yes
One or more readers Yes No, must wait
A writer No, must wait No, must wait

Reader-Writer Spinlocks: rwlock_t

The kernel’s reader-writer spinlock type is rwlock_t, declared with DEFINE_RWLOCK() and manipulated with a small family of functions. Like the ordinary spinlock, it busy-waits rather than sleeping, so it’s meant for short critical sections and is safe to use in atomic context (though as always, taking it in interrupt context needs the matching irq-safe variant).

rwlock_t stats_lock;
rwlock_init(&stats_lock);

/* A reader path */
read_lock(&stats_lock);
total = iface_stats.rx_packets + iface_stats.tx_packets;
read_unlock(&stats_lock);

/* A writer path */
write_lock(&stats_lock);
iface_stats.rx_packets++;
write_unlock(&stats_lock);

As with plain spinlocks, there are IRQ-safe and bottom-half-safe variants — read_lock_irqsave() / write_lock_irqsave() and the _bh() forms — for use when the lock might also be taken from interrupt or softirq context. One detail worth remembering: even the read-side IRQ variant disables kernel preemption on that CPU while held, exactly like a normal spinlock does.

The Writer-Starvation Problem

The classic weakness of a naive reader-writer spinlock is that a stream of overlapping readers can keep a waiting writer parked indefinitely. If reader A holds the lock and reader B arrives before A releases it, B is let straight in. If this keeps happening, a writer that arrived after A but before B can starve, because the implementation never stops admitting new readers just because someone is waiting to write.

Kernel developers took this seriously enough to change the underlying implementation. Modern kernels use a queued reader-writer lock (qrwlock), which enforces fairness: once a writer is queued, new readers are held back until that writer has had its turn. This doesn’t eliminate all latency concerns, but it does remove the “readers forever” starvation scenario that older, ticket-style implementations were vulnerable to.

Fair Queueing With qrwlock
Reader A (running)
→
Writer W (queued, waits)
→
Reader B (blocked behind W)

Reader B is held back even though the lock is technically shared, because Writer W is already queued.

Reader-Writer Semaphores: rw_semaphore

Where rwlock_t is a busy-waiting primitive, struct rw_semaphore is the sleeping equivalent, declared in <linux/rwsem.h>. It follows the same reader/writer semantics but blocks the calling task instead of spinning, which makes it appropriate for critical sections that may take a while or that need to sleep internally (for example, sections that might trigger memory reclaim or perform I/O).

struct rw_semaphore my_rwsem;
init_rwsem(&my_rwsem);

down_read(&my_rwsem);
/* read-only access */
up_read(&my_rwsem);

down_write(&my_rwsem);
/* exclusive read-write access */
up_write(&my_rwsem);

The kernel also provides down_read_trylock(), down_write_trylock(), and killable variants such as down_write_killable() for code paths that need to abort cleanly if a fatal signal arrives while waiting. One of the best-known real-world users of a reader-writer semaphore is the memory management subsystem’s mmap lock, which protects a process’s virtual memory area (VMA) layout — a great illustration of “read often, write occasionally,” since page faults read the VMA tree constantly while mmap()/munmap() calls that modify it are comparatively rare.

Property rwlock_t rw_semaphore
Waiting behaviour Busy-waits (spins) Sleeps
Safe in atomic/IRQ context Yes (with irq-safe variants) No
Best for Very short critical sections Longer sections, may block

Sequence Locks: Optimizing for Mostly-Write Data

Reader-writer locks assume reads dominate. Occasionally the opposite is true — a value is updated very frequently and read comparatively rarely, and you’d rather not pay any locking overhead at all on the write side. The sequence lock (seqlock_t) is built for exactly this. Instead of blocking readers, a seqlock lets a reader proceed optimistically and simply retry if it detects that a writer changed the data mid-read.

It works using a hidden sequence counter: the writer increments the counter before and after its update (making it odd during the write and even afterward), and a reader captures the counter, reads the data, then checks whether the counter changed or is odd. If either is true, the read wasn’t consistent and must be retried.

seqlock_t clock_seqlock = __SEQLOCK_UNLOCKED(clock_seqlock);

/* Writer side */
write_seqlock(&clock_seqlock);
system_time = read_hw_counter();
write_sequnlock(&clock_seqlock);

/* Reader side */
unsigned int seq;
u64 t;
do {
    seq = read_seqbegin(&clock_seqlock);
    t = system_time;
} while (read_seqretry(&clock_seqlock, seq));

The classic in-tree example of this pattern is the kernel’s internal timekeeping counter, which is updated on every timer tick and read constantly by time-related syscalls — an ideal fit for a write-cheap, retry-based lock.

Why RCU Has Replaced rwlock in Many Hot Paths

If you look through kernel source from ten years ago versus a 6.12-era tree, you’ll notice something: a lot of code that used to reach for rwlock_t now uses RCU (Read-Copy-Update) instead. RCU allows readers to proceed with essentially zero synchronization overhead and no blocking at all, while writers create a new version of the data and arrange for old versions to be freed only once no reader could still be using them. For read-dominated, latency-sensitive paths, RCU is generally faster than any lock-based scheme, reader-writer or otherwise. The Linux kernel’s official locking documentation is explicit about this direction, and it’s a large part of why you’ll now find rwlock_t used mostly in older subsystems or places where RCU’s deferred-reclaim model doesn’t fit cleanly.

That doesn’t make reader-writer locks obsolete — they’re still the right tool when the write side needs to block, or when the “old version stays valid until every reader is done” model that RCU relies on doesn’t apply to the data in question.

Common Mistakes

  • Using rwlock_t for long critical sections. Since it spins, holding it too long wastes CPU cycles across every waiter.
  • Forgetting the IRQ-safe variant. If the same lock is ever taken from interrupt context, every other acquisition of it must use the _irqsave forms, or you risk deadlocking against yourself.
  • Ignoring writer starvation on older kernels. If you’re backporting to a pre-qrwlock kernel, be aware the fairness guarantees this lecture describes may not apply.
  • Retrying forever on a seqlock without a bound. A pathological write rate can, in theory, starve a seqlock reader; production code paths should account for this.

Best Practices

  • Default to a reader-writer semaphore unless you specifically need atomic-context safety — sleeping locks are gentler on the scheduler.
  • Keep write-side critical sections as short as possible; writers block every reader, so long write sections directly hurt read-side scalability.
  • Before reaching for rwlock_t in new code, check whether RCU actually fits your access pattern — it usually scales better.
  • Use lockdep during development; it catches most of the ordering and context mistakes around these locks automatically.

Performance and Security Considerations

From a performance angle, reader-writer locks still involve at least one atomic memory operation per acquisition, and the reader and writer counters typically live in the same cache line, so heavy concurrent access from many cores can produce cache-line contention even without any actual data conflict. From a security and correctness angle, always double check that every writer path holds the lock in write mode — a reader-mode acquisition guarding a code path that actually mutates shared state is a silent, hard-to-find race condition, and static checkers won’t always catch it if the access pattern is indirect.

Summary & Key Takeaways

  • Reader-writer spinlocks (rwlock_t) allow concurrent readers but exclusive writers, and spin rather than sleep.
  • Reader-writer semaphores (rw_semaphore) offer the same semantics but sleep, making them suitable for longer or blocking critical sections.
  • Modern kernels use a fair, queued rwlock implementation that prevents writer starvation.
  • Sequence locks favor cheap, lockless writes with optimistic, retry-based reads — ideal for mostly-write data like time counters.
  • RCU has replaced rwlock_t in many performance-critical paths where its deferred-reclaim model applies.

Conclusion

Reader-writer locking is one of those topics where understanding the trade-off matters more than memorizing the API. Once you can look at a piece of kernel data and ask “how often is this read versus written, and can the write side tolerate blocking?”, picking between rwlock_t, rw_semaphore, a seqlock, or RCU becomes a fairly mechanical decision. In the next lecture of this free Linux kernel development course, we’ll build on this by looking at how the kernel deals with cache effects and false sharing — a closely related performance topic that affects every one of the locks covered here.

Frequently Asked Questions

1. What is the difference between rwlock_t and rw_semaphore in the Linux kernel?

rwlock_t is a spinning lock suitable for very short, atomic-context critical sections, while rw_semaphore is a sleeping lock suitable for longer sections that may block, such as ones that touch user memory or trigger I/O.

2. Can a reader-writer spinlock cause a deadlock?

Yes, most commonly through incorrect nesting or by acquiring the read lock recursively while a writer is queued on a fair (qrwlock) implementation, which will not admit the second reader.

3. Why did the Linux kernel move many rwlock users to RCU?

RCU allows read-side access with effectively no locking overhead and no blocking, which scales better than any lock-based reader-writer scheme on read-dominated workloads.

4. Is writer starvation still a problem in current kernels?

No, the queued reader-writer lock (qrwlock) used in modern kernels enforces fairness so that a waiting writer cannot be starved by a continuous stream of readers.

5. When should I use a sequence lock instead of a reader-writer lock?

Use a sequence lock when writes are far more frequent than reads and the writer should never be blocked by readers, such as for a frequently updated counter or timestamp.

6. Are reader-writer locks safe to use in interrupt handlers?

Only the IRQ-safe variants of rwlock_t (the _irqsave/_irqrestore forms) are safe when the same lock might be taken from interrupt context; rw_semaphore should never be used in interrupt context because it can sleep.

7. What real kernel subsystem uses rw_semaphore as a good example?

The memory management subsystem’s virtual memory area lock is a well-known example, since page faults read VMA data constantly while mapping changes are relatively infrequent.

Continue the Free Linux Kernel Development Course

This lecture is part of EmbeddedPathashala’s free Linux kernel programming and device driver course. Explore more lectures on kernel synchronization, scheduling, and Linux device drivers.

Browse the Full Course

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *