A Linux kernel module demonstrating how mutexes protect shared writable data in a device driver.
Unlike trivial counter examples, this module models a miniature driver by encapsulating device state inside a Driver Context. Three kernel threads concurrently access the same shared state, allowing you to observe critical sections, mutual exclusion, and lock contention exactly as they appear in real Linux drivers.
The objective of this project is not to teach multithreading itself, but to demonstrate how Linux mutexes synchronize concurrent access to shared driver resources.
After completing this project, you should understand:
- Why device drivers require synchronization
- What a Driver Context is
- What Shared Writable Data means
- Why race conditions occur
- What a Critical Section is
- How a mutex guarantees mutual exclusion
- Why even reading shared mutable data often requires locking
- How multiple kernel threads safely share one driver context
The entire module revolves around a single shared Driver Context.
Driver Context
+-------------------------------+
| struct mutex lock |
| tx_packets |
| rx_packets |
| device_enabled |
| device_name |
+---------------+---------------+
|
-----------------------------------------
| | |
| | |
▼ ▼ ▼
TX Thread RX Thread Stats Thread
| | |
+------------------+--------------------+
|
Linux Mutex
|
Critical Sections
|
Shared Writable Data
Every thread operates on the same driver context.
The mutex guarantees that only one thread at a time can access the shared driver state.
This module intentionally follows the same architectural pattern used by production Linux drivers.
Instead of creating several unrelated global variables, all driver state is grouped into a single structure.
Driver
│
▼
Driver Context (ctx)
│
-------------------------
│ │ │
▼ ▼ ▼
TX Thread RX Thread Stats Thread
This approach provides:
- better encapsulation
- easier maintenance
- cleaner synchronization
- scalable driver design
As drivers become larger, storing all state inside a context structure becomes essential.
A Driver Context is a structure that stores all runtime information belonging to a driver.
Think of it as the driver's private memory.
Instead of scattering variables throughout the module, everything is grouped together.
+----------------------------------+
| Driver Context |
|----------------------------------|
| mutex lock |
| tx_packets |
| rx_packets |
| device_enabled |
| device_name |
+----------------------------------+
The context represents the complete state of the simulated device.
Every worker thread receives access to exactly the same object.
ctx
▲
┌─────────┼─────────┐
│ │ │
│ │ │
TX RX Stats
This single object becomes the central resource of the driver.
Without a context structure, a driver quickly becomes difficult to maintain.
Poor design:
global_tx_packets
global_rx_packets
global_enabled
global_mutex
global_name
Better design:
struct driver_context
{
...
};
Advantages include:
- Improved organization
- Easier synchronization
- Better scalability
- Cleaner initialization
- Simpler cleanup
- Common production practice
Nearly every modern Linux driver maintains some form of private driver context.
Inside the Driver Context are several variables whose values change while the driver is running.
These variables are called Shared Writable Data.
Examples include:
- tx_packets
- rx_packets
- device_enabled
These variables are:
- shared by multiple threads
- modified during execution
- accessed repeatedly
Driver Context
│
▼
+-------------------------------+
| tx_packets |
| rx_packets |
| device_enabled |
+-------------------------------+
▲ ▲ ▲
│ │ │
TX RX Stats
Every thread can potentially access the same memory.
This immediately introduces the possibility of concurrent access.
Because multiple execution contexts reference the same object.
TX
│
│
▼
Driver Context
▲
│
RX
▲
│
Stats
There is only one copy of the data.
All threads operate on that single copy.
The values change during execution.
Example:
tx_packets
0
↓
1
↓
2
↓
3
↓
4
Similarly,
rx_packets
0
↓
1
↓
2
↓
3
Since these values continuously change, they require synchronization.
Whenever multiple threads modify the same variable simultaneously, the result becomes unpredictable.
Imagine two CPUs incrementing the same counter.
Current value
tx_packets = 5
CPU 1
Read 5
↓
Increment
↓
Write 6
CPU 2
Read 5
↓
Increment
↓
Write 6
Expected result
7
Actual result
6
One increment disappears.
This phenomenon is called a Race Condition.
A Critical Section is the portion of code that accesses shared writable data.
Only one thread should execute that region at a time.
mutex_lock()
Access Shared Data
mutex_unlock()
Everything between the lock and unlock belongs to the critical section.
Acquire Mutex
│
▼
+-----------------------------+
| Update Shared Driver State |
| Read Shared Driver State |
| Modify Shared Driver State |
+-----------------------------+
│
▼
Release Mutex
The mutex forms a protective boundary around the shared resource.
A common misconception is that only writes require locking.
Consider the statistics thread.
It only prints information.
However, while it is reading:
tx_packets
another thread may be updating that value.
The result could be inconsistent or partially updated state.
Therefore production drivers commonly protect both reads and writes of shared mutable data.
Shared Data
Read
▲
│
│
Write ◄────────┼────────► Read
│
▼
Update
All protected
by one mutex
The module revolves around four ideas.
Driver Context
│
▼
+-------------------------+
| Shared Writable Data |
+-------------------------+
▲
│
Protected By
│
Linux Mutex
│
▼
Critical Sections
▲
│
Accessed By Multiple Threads
Everything else in the program is built on top of these concepts.
Whenever reading Linux driver code, remember the following sequence.
Driver Context
│
▼
Shared Writable Data
│
▼
Critical Sections
│
▼
Linux Mutex
│
▼
Safe Concurrent Access
If you understand this flow, you understand the architectural foundation of synchronization in Linux device drivers.
- A Driver Context stores the complete runtime state of a driver.
- Shared Writable Data can be accessed by multiple execution contexts.
- Concurrent access without synchronization leads to race conditions.
- A Critical Section is the code that accesses shared mutable state.
- A Linux mutex ensures that only one thread executes a critical section at any given time.
- Grouping shared state inside a Driver Context and protecting it with a mutex is the standard synchronization pattern used throughout Linux kernel drivers.
The Linux kernel provides a family of mutex APIs that allow sleeping synchronization between execution contexts. Unlike spinlocks, mutexes are designed for longer critical sections where the owner may voluntarily sleep.
This driver demonstrates the complete mutex life cycle.
mutex_init()
│
▼
mutex_lock()
or
mutex_lock_interruptible()
│
▼
Critical Section
│
▼
mutex_unlock()
│
▼
mutex_destroy()
A mutex must be initialized before any thread attempts to acquire it.
Driver Context
+----------------------+
| struct mutex lock |
+----------------------+
│
▼
mutex_init(&ctx->lock);
Initialization places the mutex into an unlocked state, making it ready for concurrent use.
Purpose
- Initializes the mutex object.
- Prepares the synchronization primitive.
- Typically performed during driver initialization.
mutex_lock() acquires exclusive ownership of the mutex.
If another thread already owns the mutex, the calling thread sleeps until the mutex becomes available.
Thread
│
▼
mutex_lock()
│
▼
Critical Section
│
▼
mutex_unlock()
Characteristics:
- Sleeping lock
- Blocks until available
- Cannot be used in interrupt context
- Suitable for long critical sections
This function behaves similarly to mutex_lock() but allows the sleeping task to be interrupted by a signal.
mutex_lock_interruptible()
│
├──────── Success
│
▼
Critical Section
│
▼
mutex_unlock()
OR
Interrupted
│
▼
Return -EINTR
This API is useful when a thread should remain responsive while waiting for a mutex.
Advantages:
- Interruptible wait
- Better responsiveness
- Common in blocking kernel paths
Releases ownership of the mutex.
Acquire
↓
Critical Section
↓
Release
Once released, another waiting thread may immediately acquire the mutex.
Owner
TX Thread
↓
mutex_unlock()
↓
RX Thread acquires mutex
Destroys the mutex object.
Although many kernel mutexes require no explicit destruction, calling mutex_destroy() is considered good practice for dynamically allocated mutexes and helps debug kernels detect misuse.
Module Exit
│
▼
Stop Threads
│
▼
mutex_destroy()
│
▼
Free Driver Context
The module launches three independent kernel threads.
Driver Context
▲
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
TX Thread RX Thread Stats Thread
Each thread performs a different task while sharing the same driver state.
The TX thread simulates packet transmission.
Responsibilities:
- Acquire mutex
- Update transmit counter
- Simulate transmission delay
- Release mutex
TX Thread
Acquire Mutex
↓
Increment TX Counter
↓
Simulate Work
↓
Release Mutex
Because the TX counter belongs to shared driver state, every update occurs inside a critical section.
The RX thread simulates packet reception.
Instead of using mutex_lock(), it demonstrates the interruptible version.
RX Thread
Acquire Mutex
(Interruptible)
↓
Increment RX Counter
↓
Simulate Work
↓
Release Mutex
This highlights that Linux offers multiple ways to wait for a mutex depending on driver requirements.
The Statistics thread periodically reads the driver state.
Unlike the TX and RX threads, it performs no updates.
Instead, it reads:
- Device name
- Device status
- TX packets
- RX packets
Stats Thread
Acquire Mutex
↓
Read Shared State
↓
Display Statistics
↓
Release Mutex
Although this thread only performs reads, it still acquires the mutex.
Many beginners assume that only writers require synchronization.
Consider the following situation.
TX Thread
tx_packets++
│
│
▼
Stats Thread
Read tx_packets
If the statistics thread reads the value while another thread is modifying it, the resulting state may be inconsistent.
For this reason, Linux drivers commonly protect both reads and writes of shared mutable state.
All three threads operate independently.
Driver Context
▲
┌──────────┼──────────┐
│ │ │
TX RX Stats
│ │ │
└──────────┼──────────┘
▼
Same Mutex
Only one thread may enter the critical section at any instant.
A race condition occurs whenever multiple execution contexts access shared writable data without proper synchronization.
Imagine the TX counter currently equals:
tx_packets = 5
Both CPUs execute simultaneously.
CPU 1
Read 5
↓
Increment
↓
Write 6
CPU 2
Read 5
↓
Increment
↓
Write 6
Expected result:
7
Actual result:
6
One increment is lost.
The outcome depends entirely on execution timing.
This makes race conditions:
- difficult to reproduce
- difficult to debug
- highly nondeterministic
Instead of allowing simultaneous access, the mutex serializes execution.
CPU 1
Acquire Mutex
↓
Increment Counter
↓
Release Mutex
Only after CPU 1 finishes can another thread proceed.
CPU 2
Waiting...
↓
Acquire Mutex
↓
Increment Counter
↓
Release Mutex
Since updates occur sequentially, every increment is preserved.
5
↓
6
↓
7
No update is lost.
The synchronization model implemented by this driver can be summarized as follows.
Kernel Thread
│
▼
Acquire Mutex
│
▼
Access Shared Driver Context
│
▼
Update / Read Shared Data
│
▼
Release Mutex
Regardless of which thread executes, every access follows exactly the same synchronization pattern.
The worker threads themselves are not the primary focus of this project.
They simply generate concurrent access.
The real objective is to demonstrate how one mutex protects one shared driver context.
Three Kernel Threads
TX RX Stats
│ │ │
└─────┼─────┘
▼
Linux Kernel Mutex
▼
Driver Context (ctx)
▼
Shared Writable Driver State
Whenever multiple execution contexts share mutable state, Linux kernel drivers protect that state using synchronization primitives such as mutexes.
mutex_init()prepares a mutex for use.mutex_lock()acquires exclusive ownership and sleeps if necessary.mutex_lock_interruptible()allows the wait to be interrupted by signals.mutex_unlock()releases ownership so another thread may proceed.mutex_destroy()cleans up dynamically initialized mutexes.- TX, RX and Statistics threads all operate on the same Driver Context.
- The worker threads exist solely to generate concurrent access.
- A race condition occurs when shared writable data is accessed without synchronization.
- A mutex serializes access to shared state, guaranteeing mutual exclusion and preserving data consistency.
One of the primary goals of this demonstration is to visualize lock contention.
Lock contention occurs when multiple execution contexts attempt to acquire the same mutex simultaneously, but only one thread is allowed to own it.
Since a mutex provides mutual exclusion, every other thread must wait until the current owner releases the lock.
In this driver, all three worker threads operate on the same Driver Context.
Driver Context
+-------------------------+
| struct mutex lock |
| tx_packets |
| rx_packets |
| device_enabled |
| device_name |
+-----------+-------------+
|
---------------------------------------
| | |
▼ ▼ ▼
TX Thread RX Thread Stats Thread
Because all three threads access the same shared mutable state, they all require the same mutex.
Only one thread can own the mutex at any moment.
Suppose the TX thread acquires the mutex first.
ctx->lock
Owner
│
TX
│
-------------------------
Waiting : RX
Waiting : Stats
The TX thread enters the critical section while the RX and Statistics threads remain blocked.
This waiting state is known as lock contention.
Time ------------------------------------------------------------>
TX Thread
Acquire Lock
│
▼
Update TX Counter
│
▼
Sleep (1 second)
│
▼
Release Lock
---------------------------------------------------------------
RX Thread
Attempt Lock
│
▼
Waiting...
Waiting...
Waiting...
│
▼
Acquire Lock
│
▼
Release Lock
---------------------------------------------------------------
Stats Thread
Attempt Lock
│
▼
Waiting...
Waiting...
Waiting...
Waiting...
│
▼
Acquire Lock
│
▼
Release Lock
Although all three threads execute concurrently, only one thread executes the critical section at a time.
Not necessarily.
Some contention is completely normal.
Whenever multiple execution contexts protect the same shared resource, some waiting is expected.
The problem occurs when contention becomes excessive.
High contention means:
- Threads spend more time waiting.
- CPU resources are underutilized.
- Overall throughput decreases.
- Concurrency is reduced.
Thread A
Lock
Work
Unlock
---------------------
Thread B
Wait
Lock
Unlock
The waiting time is extremely small.
This is usually acceptable.
Thread A
Lock
Long Operation
Sleep
Unlock
------------------------
Thread B
Waiting...
------------------------
Thread C
Waiting...
------------------------
Thread D
Waiting...
Many threads remain blocked while one thread owns the mutex.
This significantly reduces concurrency.
One of the most important characteristics of a Linux mutex is that the owner is allowed to sleep while holding it.
Unlike spinlocks, mutexes are specifically designed for sleeping synchronization.
The TX worker intentionally demonstrates this behavior.
Acquire Mutex
↓
Update Shared Data
↓
Sleep
↓
Release Mutex
During the sleep period, the TX thread still owns the mutex.
No other thread can enter the critical section.
A mutex is a sleeping lock.
When a thread sleeps while holding the mutex:
- The scheduler suspends the thread.
- Waiting threads are also put to sleep.
- No CPU cycles are wasted spinning.
This makes mutexes ideal for longer operations.
| Feature | Mutex | Spinlock |
|---|---|---|
| Sleeping while holding lock | Yes | No |
| Waiting thread sleeps | Yes | No |
| Suitable for long operations | Yes | No |
| Suitable for interrupt context | No | Yes |
This distinction is fundamental in Linux kernel synchronization.
The purpose is educational.
Without the sleep, the mutex would only be held for a few microseconds.
Acquire Lock
↓
Increment Counter
↓
Release Lock
The RX and Statistics threads would almost never be observed waiting.
Contention would be nearly invisible.
Instead, the demo intentionally performs:
Acquire Lock
↓
Increment Counter
↓
Sleep (1 second)
↓
Release Lock
The mutex remains owned for one full second.
During that period, the other threads attempt to acquire the mutex and become blocked.
This makes lock contention easy to observe.
The sleep itself does not create contention.
Contention occurs because:
- one thread owns the mutex
- other threads attempt to acquire the same mutex
The longer the owner keeps the mutex, the greater the probability that other threads will be forced to wait.
This project intentionally prioritizes learning over performance.
The TX thread sleeps while holding the mutex.
Advantages:
- Easy to visualize synchronization.
- Easy to observe waiting threads.
- Easy to understand mutex ownership.
- Excellent teaching example.
Disadvantages:
- Long critical section.
- High lock contention.
- Poor scalability.
A production-quality driver follows a different philosophy.
The mutex should protect only the shared resource.
Everything else should execute outside the critical section.
Conceptually:
Acquire Mutex
↓
Update Shared Data
↓
Release Mutex
↓
Sleep
↓
Continue Processing
This minimizes waiting time.
Other threads can immediately acquire the mutex after the shared data has been updated.
A mutex should protect only the code that requires exclusive access.
Keep critical sections:
- small
- efficient
- predictable
Long critical sections increase contention and reduce concurrency.
The Statistics thread never modifies the counters.
It only reads them.
Even so, it still acquires the mutex.
Why?
Because another thread may update the shared data while the reader is accessing it.
Without synchronization, the reader could observe inconsistent state.
Production Linux drivers therefore commonly protect both readers and writers whenever they access shared mutable data.
The synchronization strategy of this module can be summarized as follows.
Driver Context
+-----------------------------+
| Shared Writable Data |
+-------------+---------------+
▲
│
Protected By
│
Linux Mutex
▲
│
----------------------------------------
| | |
▼ ▼ ▼
TX Thread RX Thread Stats Thread
Every thread follows exactly the same synchronization model.
Acquire mutex.
↓
Access shared driver state.
↓
Release mutex.
This project covers several concepts frequently discussed during Linux kernel and device driver interviews.
A mutex protects shared writable data from concurrent access.
A Driver Context stores the complete runtime state of a device driver.
The code that accesses shared mutable data.
Only one thread should execute it at a time.
Multiple threads attempting to acquire the same mutex simultaneously.
Yes.
Mutexes are sleeping locks.
No.
Sleeping while holding a spinlock is a serious kernel programming error.
It allows a sleeping task waiting for the mutex to be interrupted by a signal.
To reduce lock contention and improve concurrency.
Because shared mutable data may change while another thread is reading it.
It groups all driver state into a single structure, making synchronization simpler and improving maintainability.
- Lock contention occurs when multiple threads compete for the same mutex.
- Only one thread can execute a critical section protected by a mutex.
- Linux mutexes are sleeping locks.
- Sleeping while holding a mutex is legal but increases contention.
- This module intentionally sleeps while holding the mutex to make contention easy to observe.
- Production drivers generally keep critical sections as short as possible.
- Both reads and writes of shared mutable data should be synchronized.
- The worker threads are simply a mechanism to create concurrent access.
- The true purpose of this project is to demonstrate how a single Linux mutex protects a shared Driver Context, ensuring safe concurrent access to shared writable data.