Timeline
Timeline
2025-11-12
init
2026-05-21
add rwlock, rw_semaphore , rcu and per_cpu
This article introduces the concepts of concurrency and competition in Linux driver development and their common causes (multithreading, interrupts, preemption, and multi-core concurrency), and explains in detail the mechanisms used by the Linux kernel to address these issues, with a focus on the principles of integer atomic operations and related API functions.
Linux Driver Notes
| Table of Contents | Links |
|---|---|
| 1. Linux Driver Framework | |
| 2. Linux Driver Loading Logic | |
| 3. Character Device Basics | |
| 4. Concurrency and Competition | |
| 5. Advanced Character Device Progression | |
| 6. Interrupts | |
| 7. Platform Bus | |
| 8. Device Tree | |
| 9. Device Model | |
| 10. Hot Plug | |
| 11. pinctrl Subsystem | |
| 12. GPIO Subsystem | |
| 13. Input Subsystem | |
| 14. Single Bus | |
| 15. I2C | |
| 16. SPI | |
| 17. UART | |
| 18. PWM | |
| 19. RTC | |
| 20. Watchdog | |
| 21. CAN | |
| 22. Network Device | |
| 23. ADC | |
| 24. IIO | |
| 25. USB | |
| 26. LCD |
Concurrency and Race Condition Concepts
Below, concurrency and parallelism are collectively referred to as concurrency.
In a concurrent execution environment, multiple programs may access the same shared resource simultaneously. When multiple tasks attempt to operate on such resources at the same time, execution anomalies or data errors may occur, and such problems are called race conditions.
Common causes of race conditions include:
- Multithreaded access. Since Linux is a multitasking operating system, multiple threads may access the same shared resource simultaneously, which is the fundamental cause of race conditions.
- Interrupt accessWhen a process is accessing a shared resource and an interrupt preempts the executing process, competition may also occur between the process that issued the interrupt and the interrupted process.
- Preemptive AccessLinux 2.6 and later versions introduced a preemptive kernel, where high-priority tasks can interrupt low-priority tasks. If a task accessing a shared resource is interrupted and another task subsequently accesses the same resource, competition may arise.
- **Multi-core Concurrent Access (SMP)**In multi-core processor systems, different CPU cores may concurrently access the same shared resource, leading to inter-core competition.
The Linux kernel provides various mechanisms to address this issue, commonly used methods includeatomic operations、spin locks、mutex locks、semaphoresand so on.
Atomic Operations
Atomic operations treat a read-write operation on an integer variable as a whole, ensuring it is indivisible, thereby avoiding competition.
Atomic operations can be further subdivided into “integer atomic operations” and “bit atomic operations”, here we first explain integer atomic operations.
integer atomic operations
In the Linux kernel, useatomic_tandatomic64_tstructures to define atomic variables for 32-bit and 64-bit systems respectively
include/linux/types.h
1 | typedef struct { |
32-bit atomic_t
include/linux/atomic.h
| function prototype | function description | parameters | return value |
|---|---|---|---|
#define ATOMIC_INIT(i) | initialize atomic_t variable toi | i: initial integer value | none |
int atomic_read(const atomic_t *v) | Read the value of the atomic variable | v: atomic_t pointer | Returnsvthe current value of |
void atomic_set(atomic_t *v, int i) | Set the atomic variable toi | v: variable pointer;i: value to write | None |
void atomic_add(int i, atomic_t *v) | Atomicallyv += i | i: increment value;v: variable pointer | None |
void atomic_sub(int i, atomic_t *v) | Atomicallyv -= i | i: decrement value;v: variable pointer | None |
void atomic_inc(atomic_t *v) | Atomicallyv++ | v: variable pointer | None |
void atomic_dec(atomic_t *v) | Atomicallyv-- | v: variable pointer | None |
int atomic_inc_return(atomic_t *v) | Atomicallyv++and return the new value (add first, then return) | v: variable pointer | the value after increment |
int atomic_dec_return(atomic_t *v) | Atomicallyv--and return the new value (subtract first, then return) | v: variable pointer | the value after decrement |
int atomic_sub_and_test(int i, atomic_t *v) | atomv -= i, if the result is0return true | i: value to subtract;v: variable pointer | is 0 → return 1, not 0 → return 0 |
int atomic_dec_and_test(atomic_t *v) | atomv--, if the result is0return true | v: variable pointer | is 0 → return 1, not 0 → return 0 |
int atomic_inc_and_test(atomic_t *v) | atomv++, if the result is0return true | v: variable pointer | If 0 → return 1, otherwise 0 |
int atomic_add_negative(int i, atomic_t *v) | Atomicv += i, if result**< 0**Return true | i: Increment value;v: Variable pointer | Result < 0 → return 1, otherwise 0 |
64-bit atomic64_t
| Function prototype | Function description | Parameters | Return value |
|---|---|---|---|
#define ATOMIC64_INIT(i) | Initialize atomic64_t toi | i: Initial 64-bit integer | None |
long long atomic64_read(const atomic64_t *v) | Read the value of a 64-bit atomic variable | v: variable pointer | Returns the current 64-bit value |
void atomic64_set(atomic64_t *v, long long i) | Set atomic variable toi | v: variable pointer;i: 64-bit value to write | None |
void atomic64_add(long long i, atomic64_t *v) | Atomically executev += i | i: increment value;v: variable pointer | None |
void atomic64_sub(long long i, atomic64_t *v) | Atomically executev -= i | i: decrement value;v: variable pointer | None |
void atomic64_inc(atomic64_t *v) | Atomic executionv++ | v: Variable pointer | None |
void atomic64_dec(atomic64_t *v) | Atomic executionv-- | v: Variable pointer | None |
long long atomic64_add_return(long long i, atomic64_t *v) | Atomic executionv += iand return new value (add first, then return) | i、v | Return the value after addition |
long long atomic64_sub_return(long long i, atomic64_t *v) | Atomic executionv -= iand return new value (subtract first, then return) | i、v | Return the value after subtraction |
long long atomic64_inc_return(atomic64_t *v) | Atomicv++and return new value | v | Return the value after increment |
long long atomic64_dec_return(atomic64_t *v) | atomv--and return the new value | v | return the decremented value |
int atomic64_add_negative(long long i, atomic64_t *v) | atomv += i, if the result**< 0**returns true | i、v | result < 0 → 1, otherwise 0 |
int atomic64_add_and_test(long long i, atomic64_t *v) | atomv += i, if the result**== 0**returns true | i、v | result == 0 → 1, otherwise 0 |
int atomic64_sub_and_test(long long i, atomic64_t *v) | atomv -= i, if the result**== 0**returns true | i、v | result == 0 → 1, otherwise 0 |
int atomic64_inc_and_test(atomic64_t *v) | atomv++, if the result**== 0**returns true | v | is 0 → 1, otherwise 0 |
int atomic64_dec_and_test(atomic64_t *v) | atomv--, if the result**== 0**returns true | v | is 0 → 1, otherwise 0 |
long long atomic64_xchg(atomic64_t *v, long long i) | atomic swap: setvtoiand return the old value | v、i | returns the old value before the swap |
long long atomic64_cmpxchg(atomic64_t *v, long long old, long long new) | atomic compare-and-swap: if *v == old, write new | v、old、new | returns the old value before the swap (can be used to check success) |
incorrect usage
The following usage is incorrect:
1 | static atomic_t atomic_key = ATOMIC_INIT(1); |
The reason isatomic_read(&atomic_key)andatomic_dec(&atomic_key)These two operations are separate, and two processes can simultaneouslyatomic_read(&atomic_key)thenatomic_dec(&atomic_key)succeed. These two operations can be combined into one
1 | if (!atomic_dec_and_test(&atomic_key)) |
Example:
1 |
|
Test code:
1 |
|
Test:
1 | $ insmod atomic_t.ko |
atomic_cmpxchg
This function is relatively difficult to understand; its purpose is:
- If the current value of the atomic variable equals
old- Change it to
new- Return the value before modification (because
vthe value before modification equalsold, so the returned value equalsold)- If the current value ≠
old:
- the atomic variable will not be modified
- directly returns the “current value”
1 |
|
Parameters
| Parameter | Description |
|---|---|
v | Pointer to theatomic64_tvariable to operate on |
old | Expected old value (expected value) |
new | If the atomic variable’s value equalsold, replace it withnewReplace it |
Return Value
- Returnsthe original value of the atomic variable before the operation。
- If the original value equals
old, replace the original value with new, return value = old. - If the original value does not equal
old, replacement fails, return value ≠ old.
Example
1 |
|
Bit atomic operation
| Function prototype | Function description | Parameter description | Return value |
|---|---|---|---|
void set_bit(int nr, void *p) | Set thepbit of the addressnrposition / place / rank / digit / measure word for peopleto 1 | nr: bit number (0 indicates the least significant bit)p: starting address | none |
void clear_bit(int nr, void *p) | setpthenr-th bit of the addressto zero | same as above | none |
void change_bit(int nr, void *p) | togglenr-th bitinvert(0→1 or 1→0) | same as above | none |
int test_bit(int nr, void *p) | Readnrthe value of the bit | Same as above | Returns the value of the bit: 0 or 1 |
int test_and_set_bit(int nr, void *p) | Setnrthe bit to 1 and return the original value | Same as above | Returns the value of the bit before the operation (0 or 1) |
int test_and_clear_bit(int nr, void *p) | Clearnrthe bit to 0 and return the original value | Same as above | Returns the value of the bit before the operation |
int test_and_change_bit(int nr, void *p) | Togglenrthe bit (1↔0) and return the original value | Same as above | Returns the value of the bit before the operation |
Bit numbering starts fromp points to the starting address of the memorycounting from:
nr = 0→ the least significant bit (bit0) of the first bytenr = 7→ the most significant bit of the first bytenr = 8→ bit0 of the second byte- and so on
Bit numbering is an absolute bit index, not an offset within a byte.
Spinlock
A spinlock is a locking mechanism proposed to protect shared resources.When a thread attempts to acquire a spinlock and finds that the lock is already held by another thread, it does not enter a sleep state to wait, but instead continuously loops to try to acquire the lock until it succeeds. This process is called ‘spinning’, hence the name ‘spinlock’.
Definition
include/linux/spinlock_types.h
1 | typedef struct spinlock { |
offsetof is defined in
include/linux/stddef.hIts purpose is to calculate the offset from a struct to a specific field.
1
2
3
4
5The above spinlock field uses offsetof to calculate and set padding fields, ensuring that the two union members’
dep_mapaddresses are exactly the same, thus ensuring that regardless of whetherCONFIG_DEBUG_LOCK_ALLOC,spinlock_tthe size and alignment ofare consistentThere is another purpose:
spinlock_tisa generic lock type
- Do not want to expose
raw_spinlockthe internal structure of- But
lockdepneedsdep_mapThis design allows kernel code to be written like this:
1 lockdep_init_map(&lock->dep_map, ...);Instead of:
1 lock->rlock.dep_map // ❌ This is not desired
Spinlock-related API functions are defined in the kernel source codeinclude/linux/spinlock.hfile (the spinlock.h header includes spinlock_types.h, etc., so just include the spinlock.h header file)
API
| API / Macro | Return Type | Function Description | Blocking | Interrupt State | Return Value / Remarks |
|---|---|---|---|---|---|
DEFINE_SPINLOCK(name) | void | Statically define and initialize a spinlock | - | - | None |
spin_lock_init(spinlock_t *lock) | void | Dynamically initialize a spinlock (only initializes, does not allocate memory) | - | - | None |
spin_lock(spinlock_t *lock) | void | Acquire spinlock | Blocking (spinning) | Unchanged | None |
spin_lock_bh(spinlock_t *lock) | void | Acquire spinlock and disable softirq | Blocking | Disable softirq | None |
spin_lock_irq(spinlock_t *lock) | void | Acquire spinlock and disable local CPU interrupts | Blocking | Disable local interrupts | None |
spin_lock_irqsave(spinlock_t *lock, unsigned long flags) | void | Acquire spinlock and save local interrupt state | Blocking | Disable interrupts, save flags | None |
spin_trylock(spinlock_t *lock) | int | Try to acquire spinlock | Non-blocking | No change | Returns 1 on success, 0 on failure |
spin_trylock_bh(spinlock_t *lock) | int | Try to acquire spinlock and disable softirq | Non-blocking | Disable softirq | Returns 1 on success, 0 on failure |
spin_trylock_irq(spinlock_t *lock) | int | Try to acquire spinlock and disable interrupts | Non-blocking | Disable local interrupts | Returns 1 on success, 0 on failure |
spin_trylock_irqsave(spinlock_t *lock, unsigned long flags) | int | Try to acquire spinlock and save interrupt state | Non-blocking | Disable interrupts, save flags | Returns 1 on success, 0 on failure |
spin_unlock(spinlock_t *lock) | void | Release spinlock | - | - | None |
spin_unlock_bh(spinlock_t *lock) | void | Release spinlock and restore softirq state | - | Restore softirq | None |
spin_unlock_irq(spinlock_t *lock) | void | Release spinlock and restore interrupts | - | Restore interrupts | None |
spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) | void | Release spinlock and restore interrupt state | - | Restore interrupt state flags | None |
spin_is_locked(spinlock_t *lock) | int | Check if lock is held | Non-blocking | - | Returns 1 if locked, otherwise 0 |
spin_is_contended(spinlock_t *lock) | int | Check if lock is contended | Non-blocking | - | Returns 1 if there is contention, otherwise 0 |
Using a spin lock involves the following 3 steps:
- First, acquire the spin lock when accessing critical resources
- After acquiring the spin lock, enter the critical section; if unable to acquire it, wait in place.
- Release the spin lock when exiting the critical section.
Example
1 |
|
Test:
1 |
|
Test case:
1 | $ insmod spinlock_test.ko |
Notes
- BecauseA spin lock will “wait in place,” which continues to occupy the CPU and consumes CPU resources, so the lock duration must not be too long. That is, the code in the critical section should not be too extensive.
- In the critical section protected by the spin lock,functions that may cause thread sleep cannot be called, otherwise a deadlock may occur
- Spin locks are generallyUsed on multi-core SOCs
- Only one task can hold a mutex at a time; this is not a rule but a fact.
- Multiple unlocks are not allowed.
- They must be initialized through the API.
- A task holding a mutex cannot exit, because the mutex will remain locked, and potential contenders will wait forever (sleep).
- Locked memory regions cannot be freed.
- A held mutex must not be reinitialized.
- Because they involve rescheduling, mutexes cannot be used in atomic contexts such as tasklets and timers.
Spinlock deadlock
A spinlock deadlock refers to a situation where multiple tasks or processes wait for each other to release resources, causing none to proceed.
For example, suppose there are two processes, A and B. Process A holds a spinlock on resource 1 and also wants to acquire resource 2. Process B holds a spinlock on resource 2 and also wants to acquire resource 1. In this case, both processes are waiting for each other to release resources, causing a deadlock.
Another example: while process A holds a spinlock, an interrupt occurs, and the CPU switches to execute the interrupt handler, which also needs to acquire the same spinlock. Since the lock is already held by A, the interrupt handler can only spin and wait, causing the system to enter a deadlock state.
For example,When the interrupt handler also needs to acquire a spinlock, the driver must disable interrupts while holding the spinlock (spin_lock_irqsave, spin_lock_irqrestore) and enable interrupts when releasing the spinlock.。
Also, hold the spinlock for as short a time as possible; holding it for a long time may exhaust system resources and cause a deadlock.
Finally, alsoavoid a function that has acquired a spinlock from calling other functions that also attempt to acquire the same lock, otherwise it will also cause a deadlock.
No function that can cause sleep or blocking should be called within the critical section
Read-Write Lock
Definition
Usingspinlockto protect a critical section, multiple reads cannot be concurrent and can only bespin, to improve the overall system performance, the kernel defines a type of lock:
- Allowing multiple processor processes (or threads or interrupt contexts)to perform read operations concurrently(
SMP), which is safe and improves theSMPsystem’s performance. - During writing, ensure complete mutual exclusion of the critical section.
Read/Write Spinlockis introduced under the protection ofSMPthe shared data structure under the system, and its introduction is to increase the kernel’s concurrency capability**. As long as the kernel control pathdoes notmodify the data structure, the read/write spin lock allows multiple kernel control paths**to simultaneously read the same data structure.
If a kernel control path wants to write to this structure, it must first acquire the write lock of the read/write lock,the write lock grants exclusive access to this resource. The purpose of this design is that allowing concurrent reads of the data structure can improve system performance.
Example
1 | /* |
API
InitializationAPI**:
| Function | Description |
|---|---|
DEFINE_RWLOCK(rwlock_t lock) | Define and initialize a read/write lock |
void rwlock_init(rwlock_t *lock) | Initialize a read/write lock |
Read OperationAPI :
| Function | Description |
|---|---|
void read_lock(rwlock_t *lock) | Acquire read lock |
void read_unlock(rwlock_t *lock) | Release read lock |
void read_lock_irq(rwlock_t *lock) | Disable local interrupts and acquire read lock |
void read_unlock_irq(rwlock_t *lock) | Enable local interrupts and release read lock |
void read_lock_irqsave(rwlock_t *lock, unsigned long flags) | Save interrupt state, disable local interrupts, and acquire read lock |
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags) | Restore interrupt state to previous state, enable local interrupts, and release read lock |
void read_lock_bh(rwlock_t *lock) | Disable bottom halves and acquire read lock |
void read_unlock_bh(rwlock_t *lock) | Enable bottom halves and release read lock |
Write OperationAPI :
| Function | Description |
|---|---|
void write_lock(rwlock_t *lock) | Acquire write lock |
void write_unlock(rwlock_t *lock) | Release write lock |
void write_lock_irq(rwlock_t *lock) | Disable local interrupts and acquire write lock |
void write_unlock_irq(rwlock_t *lock) | Enable local interrupts and release write lock |
void write_lock_irqsave(rwlock_t *lock, unsigned long flags) | Save interrupt state, disable local interrupts, and acquire write lock |
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags) | Restore interrupt state to previous state, enable local interrupts, and release read lock |
void write_lock_bh(rwlock_t *lock) | Disable bottom halves and acquire read lock |
void write_unlock_bh(rwlock_t *lock) | Enable bottom halves and release read lock |
Semaphore
Spinlocks handle concurrency and competition through ‘busy waiting’, so the protected critical section cannot be too long to avoid wasting CPU resources. However, in some cases we inevitablyneed to protect certain resources for a long time. In such cases, semaphores can be used.
Semaphores cause the caller to sleep, so they are also called sleep locks.
Semaphores haveP operationandV operation. The P operation decrements the semaphore value by one. If the semaphore value is less than or equal to 0, it indicates that all resources are occupied; if another process needs to use it, that process will be added to the waiting queue. If the value is greater than 0, it means there are n available resources.
Definition
include/linux/semaphore
1 | /* SPDX-License-Identifier: GPL-2.0-only */ |
API
| API / Macro | Return Type | Function Description |
|---|---|---|
DEFINE_SEMAPHORE(name) | N/A | Define and initialize a semaphore with an initial value of 1 (mutex semaphore). |
sema_init(struct semaphore *sem, int val) | void | Initialize a semaphoresem, initial value isval。 |
down(struct semaphore *sem) | void | Acquire the semaphore. If count > 0, decrement by 1 and return immediately; if count is 0, block until it can be acquired. |
down_interruptible(struct semaphore *sem) | int | Acquire the semaphore. If interrupted by a signal, return non-zero; otherwise block until acquisition succeeds and return 0. |
down_killable(struct semaphore *sem) | int | Acquire the semaphore. If the process receives a fatal signal, return non-zero; otherwise block until acquisition succeeds and return 0. |
down_trylock(struct semaphore *sem) | int | Try to acquire the semaphore immediately. Return non-zero if successful, otherwise return 0 immediately without blocking. |
down_timeout(struct semaphore *sem, long jiffies) | int | Try to acquire the semaphore, blocking for at mostjiffiesclock ticks. Return value >0 indicates success, 0 indicates timeout, negative value indicates error or signal interruption. |
up(struct semaphore *sem) | void | Release the semaphore (increment count by 1) and wake up tasks in the waiting queue. |
Difference between down and down_interruptible:
| Feature | down(struct semaphore *sem) | down_interruptible(struct semaphore *sem) |
|---|---|---|
| Blocking behavior | If the semaphorecountis 0, the calling process willblock indefinitelyuntil the semaphore becomes available | If the semaphorecountis 0, the calling process will block, butcan be interrupted by a signal |
| Signal interruption | Does not respond to signals; an interrupt will not causedown()return | Responds to signals; if a signal is received while blocked, thenImmediately return non-zero |
| Return value | void(Always succeeds, unless kernel bug) | int, 0 indicates success, non-zero indicates interrupted by signal, acquisition failed |
| Usage scenario | No need to respond to interrupts, ensure semaphore acquisition | Need interruptible blocking, e.g., user process can respond to Ctrl+C or other signals |
Example
1 |
|
Test case:
1 |
|
1 | $ insmod semaphore_test.ko |
Note
- The semaphore value cannot be less than 0
- When accessing shared resources, the semaphore performs a “decrement” operation, and after access, an “increment” operation
- When the semaphore value is 0, threads wanting to access the shared resource must wait, when the semaphore is greater than 0, waiting threads can access
- becauseSemaphores can cause sleep, so they cannot be used in interrupt contexts.
- If the shared resource is held for a relatively long time, semaphores are generally used instead of spinlocks.
- When using both semaphores and spinlocks, acquire the semaphore first, then use the spinlock, because semaphores can cause sleep.
read-write semaphore
The difference from read-write locks is that read-write semaphores are sleep locks, while read-write locks are spinlocks. Choice:
Can the critical section potentially sleep?
├─ Yes → Use read-write semaphore (rw_semaphore) or mutex (mutex)
│ └─ Read operations far more than write operations? → Yes → read-write semaphore
│ └─ Read operations not significantly more than write operations? → Mutex is simpler and has better performance
│
└─ No → Is the critical section very short (a few integer operations)?
├─ Yes → A normal spinlock (spinlock_t) may be better than a read-write spinlock
└─ No → Reads far more than writes? → Yes → read-write spinlock (rwlock_t)
└─ No → normal spinlock
Functions that may cause sleep cannot be used in interrupt contexts:
Because the interrupt context does not belong to any process, it has no process control block (
task_struct), it fundamentally lacks the infrastructure required for ‘sleeping.’ When a process wants to enter a sleeping state, it actively marks itself as ‘sleeping,’ removes itself from the run queue, and tells the scheduler: ‘Please wake me up when the lock is available.’ This operation depends on the process context.
API
| API Function Definitions | Function Description |
|---|---|
DECLARE_RWSEM(name) | Declare a read-write semaphore named name and initialize it. |
void init_rwsem(struct rw_semaphore *sem); | Initialize the read-write semaphore sem. |
void down_read(struct rw_semaphore *sem); | Used by readers to acquire sem; if not acquired, the caller sleeps and waits. |
void up_read(struct rw_semaphore *sem); | Readers release sem. |
int down_read_trylock(struct rw_semaphore *sem); | Readers attempt to acquire sem; return 1 if acquired, 0 if not. Can be used in interrupt context. |
void down_write(struct rw_semaphore *sem); | Used by writers to acquire sem; if not acquired, the caller sleeps and waits. |
int down_write_trylock(struct rw_semaphore *sem); | Writers attempt to acquire sem; return 1 if acquired, 0 if not. Can be used in interrupt context. |
void up_write(struct rw_semaphore *sem); | Writers release sem. |
void downgrade_write(struct rw_semaphore *sem); | Downgrade a writer to a reader. |
Example
Initialization:
1 |
|
Using read-write lock:
1 | /* Reader: read configuration and copy to user space */ |
Mutex lock
Only one visitor can access the same resource at the same time, other visitors can access this resource only after it finishes. This is mutual exclusion.
Mutex lock is very similar to the case where the semaphore is 1, but mutexes are more concise and efficient. However, there are more things to be aware of.
Definition
include/linux/mutex.h
1 | struct mutex { |
API
| Function/Macro | Return Type | Description |
|---|---|---|
DEFINE_MUTEX(name) | struct mutex | Statically define and initialize a mutex, initial state is unlocked |
mutex_init(struct mutex *lock) | void | Initialize a mutex to unlocked state |
mutex_destroy(struct mutex *lock) | void | Destroy a mutex (only works under DEBUG_MUTEXES) |
mutex_lock(struct mutex *lock) | void | Acquire the mutex, block if locked until available |
mutex_lock_interruptible(struct mutex *lock) | int | Acquire the mutex, block if locked, interruptible by signals; returns 0 on success, -EINTR if interrupted |
mutex_lock_killable(struct mutex *lock) | int | Acquire the mutex, block if locked, interruptible by fatal signals; return value same as above |
mutex_lock_io(struct mutex *lock) | void | Acquire the mutex, allowed in I/O context (uncommon) |
mutex_lock_nested(struct mutex *lock, unsigned int subclass) | void | Nested lock acquisition for Lockdep analysis, effectively equivalent tomutex_lock() |
mutex_lock_interruptible_nested(struct mutex *lock, unsigned int subclass) | int | Interruptible nested lock acquisition for Lockdep analysis |
mutex_lock_killable_nested(struct mutex *lock, unsigned int subclass) | int | Fatal-signal-interruptible nested lock acquisition for Lockdep analysis |
mutex_lock_nest_lock(struct mutex *lock, struct lockdep_map *nest_lock) | void | Nested lock acquisition, used for Lockdep analysis |
mutex_trylock(struct mutex *lock) | int | Try to acquire mutex, non-blocking, returns 1 on success, 0 on failure |
mutex_trylock_recursive(struct mutex *lock) | enum mutex_trylock_recursive_enum | Try to acquire mutex, allows recursion, returns 0/1/recursion flag |
mutex_unlock(struct mutex *lock) | void | Release mutex, must be called by the thread holding the lock |
atomic_dec_and_mutex_lock(atomic_t *cnt, struct mutex *lock) | int | Decrement atomic counter by 1, acquire mutex if it reaches 0; otherwise return 0 |
mutex_is_locked(struct mutex *lock) | bool | Query whether the mutex is held, true if locked, false if not |
Similar to the interruptible series of functions for wait queues, it is recommended to usemutex_lock_interruptible(), which allows the driver to be interrupted by all signals, while formutex_lock_killable(), only signals that kill the process can interrupt the driver.
Callmutex_lock()with great caution; it should only be used when it is guaranteed that the mutex will be released under all circumstances. In user context, it is recommended to always usemutex_lock_interruptible()to acquire the mutex, becausemutex_lock()will not return even if a signal (such as Ctrl+C) is received.
Example
1 |
|
Test case:
1 |
|
1 | $ insmod mutex_test.ko |
Precautions
- Mutex locks cause sleep, somutex locks cannot be used in interrupts
- Only one thread can hold a mutex lock at a time, and only the holder can unlock it
- Recursive locking and unlocking are not allowed
RCU
RCU(Read-Copy-Update, read-copy-update) as a high-performance concurrency control mechanism, specifically designed to solve the “read-heavy, write-light” scenario. By separating the synchronization logic of read and write operations, it makesread operations almost overhead-free(no locking, no atomic instructions, no blocking), while write operations ensure safety through “copy-on-update” and “deferred release”.
RCU has become an indispensable basic component in the Linux kernel, widely used in core modules such as process scheduling, memory management, and file systems.
Applicable scenarios of RCUIt mainly has the following characteristics:
- Data structureMainly accessed through pointers;
- Read operations far outnumber update operations;
- Read-side code cannot tolerate the overhead of locks;
- Update operations are relatively infrequent;
The core idea of RCU can be summarized as**“Read directly, copy and modify when writing”**
Consider a real-life scenario: There is a bulletin board at home that records the family members’ schedules. When the mother needs to update the schedule, she does not prevent others from viewing the bulletin board. Instead, she first copies the current content onto a new piece of paper, makes modifications on the new paper, and then, at a moment when no one will notice, replaces the old paper on the bulletin board with the new one. After the replacement, she does not immediately destroy the old paper but waits for a period of time to ensure that everyone who might be looking at the old bulletin board has finished reading before discarding the old paper.
In this analogy,"the moment of replacement"corresponds to thePublishingoperation,"waiting to ensure"corresponds toGrace Period, while**"Discard Old Paper"corresponds toReclamation**. These three concepts form the core triangle of RCU
terminology
Read-Side Critical Section: The code segment where read operations access RCU-protected data must be marked with
rcu_read_lock()andrcu_read_unlock(). Within this interval, read operations can safely access old data (even if write operations have updated the data).Grace Period: The time period from when a write operation begins updating data until all read-side critical sections (for old data) have ended. The end of the grace period means “all read operations that might have accessed old data have completed”, at which point the old data can be safely freed.
Write-Side Operation: Consists of three steps:
- Copy: Create a copy of the data (or new data).
- Update: Atomically switch the pointer from old data to new data.
- Deferred Free: Wait for the grace period to end, then free the old data.
How RCU Works
Read-Side
Lock-free access in read-side operations is the core source of RCU’s performance advantage. The process is as follows:
- Via
rcu_read_lock()enter the read-side critical section (essentially disabling preemption, or tracking read-side state in Preemptible RCU). - Use
rcu_dereference(p)to obtain a pointer to the shared data (includes memory barriers to ensure atomicity and visibility of the pointer read). - Access the data pointed to by the pointer (read directly without locking).
- Through
rcu_read_unlock()exit the read-side critical section.
1 | rcu_read_lock(); |
Here,rcu_read_lock()andrcu_read_unlock()do not perform actual locking and unlocking operations like traditional locks. In non-preemptive kernels, they may be complete no-ops. Their real purpose is totell the compiler not to reorder instructions, and to mark the beginning and end of the critical section in preemptive kernels
rcu_dereference()is a critical macro that ensures read operations proceed in the expected order on processors with weak memory ordering, such as Alpha. It can be seen as a wrapper for memory barriers, ensuring that the pointer read occurs before fetching the actual data
The key characteristic of the read side iszero overhead- no atomic operations, no memory barriers (on most architectures), no lock contention. This makes the RCU read path extremely efficient, maintaining stable performance even under high concurrency read scenarios.
Write-Side
Write-side operations must ensure that ‘old data is freed only after all read operations complete,’ with the following process:
- Copy/create new data: For example, allocate new memory for a linked list node and initialize it.
- Atomically update the pointer: Use
rcu_assign_pointer(p, new_ptr)to switch the shared pointer from old data to new data (includes memory barriers to ensure the new pointer is visible to all CPUs). - Wait for grace period: Via
synchronize_rcu()(blocking wait) orcall_rcu()(asynchronous callback) to wait for the grace period to end. - Release old data: After the grace period ends, safely release the old data (e.g.,
kfree(old_ptr))。
Example:
1 | // Writer-side operation: update global_ptr |
Grace Period
How to ensure safe release?
The grace period is the core mechanism of RCU, and its role isto ensure that all read-side critical sections that started before the pointer update have ended. How does RCU detect the end of a grace period?
- Core idea: Each CPU maintains an “RCU state” (e.g.,
rcu_seq), recording whether there is an active read-side critical section. When a write operation requests a grace period, RCU waits for all CPUs to undergo a “context switch” (e.g., scheduling, interrupt return, etc.) — this means the read-side critical section on that CPU has ended (because read-side critical sections disable preemption and cannot be interrupted by scheduling; a context switch indicates the read-side has ended). - Implementation details: The Linux kernel uses
rcu_nodea tree structure to track the RCU state of each CPU,synchronize_rcu()and blocks until all CPUs have completed a “grace period acknowledgment” (rcu_qs())。
The grace period may last up to hundreds of milliseconds (depending on system load); the write side should not assume it ends quickly, avoiding logic that depends on the grace period duration.
The Linux kernel usescontext switches、user-mode executionandidle loopserve as markers for exiting read-side critical sections. After a CPU passes through one of these states, the kernel considers all RCU read-side critical sections on that CPU to have completed
The implementation of RCU heavily relies onmemory barriersto ensure sequential consistency in multi-core environments, preventing compilers and CPUs from reordering instructions for optimization
On the read side,
rcu_dereference()includes a read memory barrier, ensuring that operations after fetching the pointer are not reordered before the pointer fetch.On the update side,
rcu_assign_pointer()includes a write memory barrier, ensuring that the pointer update itself is not visible to other CPUs until all store operations before the pointer update are visible to them
This ordering guarantee is crucial for RCU correctness. Consider the data publication scenario: the updater must first initialize new data, then publish the pointer. Without memory barriers, the CPU or compiler might reorder these two steps, causing readers to see incompletely initialized data

RCU synchronization primitives
Read-Side
1 | void rcu_read_lock(void); |
The above two APIs are used to mark the beginning and end of read-side critical sections, ensuring that read operations within this interval can safely access old data.
- In Classic RCU (non-preemptible RCU),
rcu_read_lock()Disable preemption (preempt_disable()),rcu_read_unlock()Enable preemption (preempt_enable())。- In Preemptible RCU, by
rcu_read_lock_bh()disabling softirqs, or usingrcu_read_lock()with kernel preemption count.
can be used torcu_dereferencedereference
1 | typeof(p) rcu_dereference(p); |
safely access RCU-protected pointers on the read side, ensuring:
- Atomicity of pointer reads (preventing partial pointer loads due to compiler optimizations).
- Memory barriers (ensuring new data is visible to the read side, avoiding CPU cache inconsistencies).
Example:
1 | // Read-side operation: accessing RCU-protected pointers |
Write-Side
rcu_assign_pointer
1 | void rcu_assign_pointer(p, typeof(p) v); |
Write-side atomic update of RCU-protected pointers, ensuring:
- Atomicity of pointer updates.
- Memory barrier (ensures new data is fully initialized before being visible to readers).
synchronize_rcu()
Function: Blocks until the grace period ends; after return, old data can be safely freed.
Note: Cannot be used in atomic context (e.g., interrupt handlers) as it would cause scheduling failure.
Example:
1
2
3
4// Deleting an RCU-protected linked list node
list_del_rcu(node); // Remove node from linked list (RCU-safe version)
synchronize_rcu(); // Wait for grace period to end
kfree(node); // Free old node
synchronize_rcu()will block waiting for the grace period; atomic context (e.g., interrupts, softirqs) cannot schedule, causing kernel crash. In such cases, usecall_rcu()。
call_rcu()
Function: Asynchronously waits for the grace period; after it ends, calls the specified callback to free old data (non-blocking).
Prototype:
void call_rcu(struct rcu_head *head, void (*func)(struct rcu_head *head));Example:
1
2
3
4
5
6
7
8
9
10
11// Define callback function (free data)
void free_data(struct rcu_head *head) {
struct data *d = container_of(head, struct data, rcu);
kfree(d);
}
// Write-side operation: update data and asynchronously free old data
struct data *new_data = kmalloc(...);
struct data *old_data = rcu_dereference_protected(global_ptr, ...);
rcu_assign_pointer(global_ptr, new_data); // Update pointer
call_rcu(&old_data->rcu, free_data); // Call free_data after grace period
- Read side must use
rcu_dereference(): Direct pointer access is prohibited (e.g.,d = global_ptr), otherwise data inconsistency may occur due to compiler optimization or CPU reordering.- Write side must use
rcu_assign_pointer(): Direct assignment is prohibited (e.g.,global_ptr = new_d), ensuring the new pointer is visible to all CPUs.
RCU usage example
RCU-protected pointer update
Scenario: global pointerglobal_datapoints to a struct, read operations access frequently, write operations update occasionally.
1 |
|
RCU-protected linked list operations
RCU is commonly used to protect dynamic linked lists (e.g., process list, network connection table). The kernel provideslist_add_rcu()、list_del_rcu()and other RCU-safe linked list operation macros.
Scenario: Maintain an RCU-protected doubly linked list, with reader side traversing the list and writer side adding/removing nodes.
1 |
|
RCU best practices
Minimize the length of read-side critical sections
The longer the read-side critical section, the longer the grace period may be (writers wait longer), causing delayed release of old data and increasing memory pressure.
Principle: Enter the critical section only when accessing RCU data, and avoid time-consuming operations (such as I/O, complex calculations) within the critical section.
1 | // Bad example: critical section contains time-consuming operations |
RCU variants
The Linux kernel provides multiple RCU variants for different scenarios:
- Classic RCU(CONFIG_RCU_FANOUT): default variant, does not support read-side preemption, suitable for non-real-time kernels.
- Preemptible RCU(CONFIG_PREEMPT_RCU): supports read-side preemption, read-side critical sections can be scheduled, suitable for real-time kernels.
- Sleepable RCU(SRCU,CONFIG_SRCU): read side can block (e.g., calling
msleep()), must usesrcu_read_lock()/srcu_read_unlock(), suitable for scenarios where the read side needs to sleep.
PER_CPU
Example
net/ipv6/af_inet6.c
1 | static int __net_init ipv6_init_mibs(struct net *net) |
struct netinstruct netns_mib mibmember is defined as follows:
1 | struct netns_mib { |
And macroDEFINE_SNMP_STATDefined as follows:
1 |

