[Linux Kernel] Spinlock spinlock mechanism

Spinlock

Note:

When using spin locks, you should avoid holding locks for a long time, otherwise other threads or processes may not be able to access shared resources. Therefore, it is recommended to shorten the lock holding time as much as possible to improve the concurrent performance of the system.

The spinlock mechanism in Linux is a technique for synchronizing access to shared resources by multiple threads or processes. When a thread or process acquires a spin lock, other threads or processes will be blocked until the thread or process that owns the lock releases the lock.

The characteristic of the spin lock is that when the lock is already occupied, the waiting thread or process will not be suspended, but will continue to try to acquire the lock until it succeeds. Compared with the traditional mutex, this method can reduce the overhead of thread or process context switching and improve the efficiency of the system.

In the Linux kernel, a spinlock is represented by a spinlock_t structure, which contains an integer value and a lock flag. When the lock is not occupied, the lock flag bit is 0; when the lock is occupied, the lock flag bit is 1, and the integer value records the number of the CPU currently occupying the lock.

Here is a simple example that demonstrates how to use a spinlock to protect a shared variable:

#include <linux/spinlock.h>

static DEFINE_SPINLOCK(my_lock);
static int shared_var = 0;

void my_function(void)
{<!-- -->
    unsigned long flags;
    spin_lock_irqsave( & amp;my_lock, flags); // acquire spin lock
    shared_var ++ ; // modify shared variable
    spin_unlock_irqrestore( & amp;my_lock, flags); // release the spin lock
}

In the above example, a spin lock my_lock is defined using the DEFINE_SPINLOCK macro, and a shared variable shared_var is defined. When calling the my_function function, first acquire the spin lock my_lock, then modify the shared variable shared_var, and finally release the spin lock.

It should be noted that when using spin locks, you should avoid holding locks for a long time, otherwise other threads or processes may not be able to access shared resources. Therefore, it is recommended to shorten the lock holding time as much as possible to improve the concurrent performance of the system.

spin_lock_irqsave

Use the spin_lock_irqsave() function to ensure that the interrupt is disabled when the spin lock is acquired, so as to prevent possible interrupt competition problems.

When an interrupt handler is running, if another interrupt request comes and needs to access the same shared resource, then the interrupt contention problem occurs. In this case, it is necessary to use a spin lock to protect shared resources, and at the same time prohibit other interrupt requests when acquiring the lock, so as to avoid the occurrence of interrupt competition problems.

The spin_lock_irqsave() function can disable interrupts when the spin lock is acquired, and save the interrupt status of the current CPU in the flags parameter, so that the interrupt status can be restored after the lock is released. In this way, it can be ensured that it will not be interrupted during the acquisition of the spin lock, thus ensuring the safety of shared resources.

When using the spin_lock_irqsave() function, you should avoid holding the lock for a long time, otherwise it may cause other threads or processes to be unable to access shared resources. Therefore, it is recommended to shorten the lock holding time as much as possible to improve the concurrent performance of the system.

Disable interruption reasons

Disabling interrupts is to avoid interrupt contention problems when acquiring spin locks. When an interrupt handler is running, if another interrupt request comes and needs to access the same shared resource, then the interrupt contention problem occurs. In this case, if you do not use spin locks to protect shared resources, and do not disable interrupts, deadlocks may occur.

Interrupt races can cause deadlocks for the following reasons:

  1. The interrupt handler is interrupted before acquiring the lock, and at this time another interrupt request comes and tries to acquire the same lock. Since the lock is already occupied, the interrupt request will always wait, resulting in a deadlock.

  2. The interrupt handler acquires the lock and is interrupted, while another interrupt request comes in and tries to acquire the same lock. Since the interrupt handler holds the lock, this interrupt request will wait forever, causing a deadlock.

Therefore, interrupts should be disabled when acquiring spin locks to avoid interrupt competition and ensure the security of shared resources. At the same time, you need to be careful not to hold the lock for a long time, otherwise other threads or processes may not be able to access shared resources, thereby reducing the concurrent performance of the system.

Source code files and functions related to the spinlock mechanism in the kernel

In the Linux kernel, the implementation of the spinlock mechanism involves the following files and functions:

  1. The include/linux/spinlock.h header file defines the spinlock_t structure and related macros and functions.

  2. The kernel/locking/spinlock.c file implements functions related to the spinlock_t structure, including spin_lock(), spin_unlock(), spin_lock_irqsave() and other functions.

  3. The kernel/locking/rwsem-spinlock.c file implements the related functions of reading and writing spin locks, including read_lock(), write_lock(), read_unlock(), write_unlock() and other functions.

These functions are mainly used to acquire and release spin locks, and ensure that interrupts are disabled while acquiring spin locks. At the same time, there are some other functions, such as spin_trylock(), spin_is_locked() and other functions, which are used to check the state of the lock and try to acquire the lock.

Spinlock macro definition explanation

#define __lockfunc __section(".spinlock.text")

__lockfunc is a macro that places the following function definition into the .spinlock.text section. .spinlock.text is a code segment specially used to store spinlock-related code in the Linux kernel.

Since spinlocks are a key mechanism for securing shared resources, their implementation needs to be very efficient and reliable. Placing the spinlock-related code in an independent code segment can make this part of the code more readable, maintainable and portable, and will not be interfered by other parts of the code. At the same time, this can also facilitate optimization and debugging related to spinlocks.

In the Linux kernel, spinlock-related code is usually placed in the `.spinlock.text` section. `.spinlock.text` is a code segment dedicated to storing spinlock-related code, and its specific location and size may vary with different architectures, compilers, and kernel versions.

Generally, `.spinlock.text` will be placed after the kernel code section, followed by other code sections (such as `.text`, `.rodata`, `.data`, etc.). When the kernel boots, the code in `.spinlock.text` is loaded into memory and executed as needed. Since spin locks are an important mechanism for protecting shared resources, code related to spin locks is usually executed frequently. For better performance, the code in `.spinlock.text` may be placed in a memory page corresponding to the CPU cache to reduce cache invalidation.
#define raw_spin_trylock(lock) __cond_lock(lock, _raw_spin_trylock(lock))
#define raw_spin_lock(lock) _raw_spin_lock(lock)

raw_spin_trylock(lock) is a macro that tries to acquire a raw spinlock pointing to lock. Acquires the lock and returns 1 if the lock is not currently occupied; otherwise, returns 0.
raw_spin_lock(lock) is a macro that acquires the raw spinlock pointing to lock. If the lock is already taken, the calling thread will be blocked until the lock becomes available.

`__cond_lock(lock, _raw_spin_trylock(lock))` is a macro that calls the `_raw_spin_trylock(lock)` function to try to acquire a spinlock under certain conditions. Specifically, this macro uses the condition variable mechanism in the Linux kernel. When the spin lock is busy, the thread will wait on the condition variable until the spin lock is available. This macro returns 1 if the spinlock is available and the thread can acquire it; otherwise, the macro returns 0.

`_raw_spin_lock(lock)` is a function that acquires a raw spinlock pointing to `lock`. This function uses the atomic operation mechanism provided by the hardware to ensure that the operation is atomic, indivisible, and will not be interrupted by other threads. While a spinlock is busy, the calling thread is blocked until the spinlock becomes available. If the spinlock is already taken, the function will keep spinning until the spinlock is available and successfully acquired.
#define raw_spin_lock_irqsave_nested(lock, flags, subclass) \
raw_spin_lock_irqsave(lock, flags)
\t
The `raw_spin_lock_irqsave_nested()` macro is typically used to acquire a spinlock in interrupt context. In the Linux kernel, since an interrupt handler can interrupt a running process or thread at any time, it is necessary to use spin locks to protect shared resources from race conditions.

Specifically, when the interrupt handler needs to access a shared resource, it will call the `raw_spin_lock_irqsave_nested()` macro to acquire the spin lock of the resource, and save the interrupt status of the current CPU to the `flags` variable. In this way, during the execution of the code in the critical section, other interrupts cannot interrupt the current interrupt handler, thereby ensuring data consistency and reliability. When the interrupt handler finishes accessing the shared resource, it calls the `raw_spin_unlock_irqrestore()` function to release the spin lock and restore the interrupt state.

It should be noted that since the `raw_spin_lock_irqsave_nested()` macro will shield the interrupt of the current CPU, the use of spin locks in the interrupt handler should be reduced as much as possible to avoid problems such as long interrupt response time or deadlock.

The raw_spin_lock_irqsave_nested() macro has one more subclass parameter than the raw_spin_lock_irqsave() function. This parameter is mainly used to mark when analyzing spin lock performance problems, so as to determine which spin locks are frequently accessed or have competition. This is very helpful for performance optimization and debugging of the Linux kernel.
#define raw_spin_trylock(lock) __cond_lock(lock, _raw_spin_trylock(lock))
#define raw_spin_lock_irq(lock) _raw_spin_lock_irq(lock)
#define raw_spin_lock_bh(lock) _raw_spin_lock_bh(lock)
#define raw_spin_unlock(lock) _raw_spin_unlock(lock)
#define raw_spin_unlock_irq(lock) _raw_spin_unlock_irq(lock)
#define raw_spin_lock(lock) _raw_spin_lock(lock)

These macros are used to implement spin locks, and the specific explanations are as follows:

1. `raw_spin_trylock(lock)`

This macro is used to try to acquire a spin lock. If the lock is not currently held, the lock will be acquired immediately and true will be returned, otherwise false will be returned directly.

Usage scenario: This macro can be used when trying to acquire a lock without blocking the thread.

2. `raw_spin_lock_irq(lock)`

This macro is used to acquire a spinlock and disable interrupts. This operation ensures that it will not be interrupted by other interrupts during the lock acquisition.

Usage scenario: This macro can be used when shared resources need to be protected and interruptions need to be avoided.

3. `raw_spin_lock_bh(lock)`

This macro is used to acquire a spinlock and disable softirqs. This operation ensures that it will not be interrupted by other soft interrupts during the lock acquisition.

Usage scenario: This macro can be used when shared resources need to be protected and interference from soft interrupts needs to be avoided.

4. `raw_spin_unlock(lock)`

This macro is used to release a spin lock.

Usage scenario: Locks need to be released when access to shared resources is no longer required.

5. `raw_spin_unlock_irq(lock)`

This macro is used to release a spinlock and restore previously disabled interrupts.

Usage scenario: This macro can be used when the interrupt is disabled during the lock acquisition and the lock needs to be released and the interrupt resumed.

6. `raw_spin_lock(lock)`

This macro is used to acquire a spin lock.

Usage scenario: This macro can be used when shared resources need to be protected.

Kernel code

void raw_spin_rq_lock_nested(struct rq *rq, int subclass)
{<!-- -->
raw_spinlock_t *lock;

/* Matches synchronize_rcu() in __sched_core_enable() */
preempt_disable();
if (sched_core_disabled()) {<!-- -->
raw_spin_lock_nested( &rq->__lock, subclass);
/* preempt_count *MUST* be > 1 */
preempt_enable_no_resched();
return;
}

for (;;) {<!-- -->
lock = __rq_lockp(rq);
raw_spin_lock_nested(lock, subclass);
if (likely(lock == __rq_lockp(rq))) {<!-- -->
/* preempt_count *MUST* be > 1 */
preempt_enable_no_resched();
return;
}
raw_spin_unlock(lock);
}
}

explain:

  1. raw_spinlock_t *lock;: Define a pointer variable lock pointing to the spin lock.

  2. preempt_disable();: Disable preemption to prevent being interrupted by other threads during the spinlock acquisition.

  3. if (sched_core_disabled()) {: Determine whether the scheduler has been disabled (that is, the kernel is performing RCU synchronization).

  4. raw_spin_lock_nested( & amp;rq->__lock, subclass);: If the scheduler has been disabled, directly acquire the spin lock of the given run queue and allow the use of nested locks.

  5. preempt_enable_no_resched();: Enables preemption, but does not trigger scheduling immediately, but waits for the current execution to complete before scheduling.

  6. return;: return function.

  7. lock = __rq_lockp(rq);: Acquire the spin lock of the current running queue.

  8. raw_spin_lock_nested(lock, subclass);: try to acquire a spin lock.

  9. if (likely(lock == __rq_lockp(rq))) {: Check whether the currently acquired spinlock is the same as the previously acquired lock.

  10. preempt_enable_no_resched();: If the two locks are the same, enable preemption and return the function.

  11. return;: return function.

  12. raw_spin_unlock(lock);: If the two locks are different, release the previously acquired lock and try to acquire the spin lock again.

The purpose of this function is to acquire a spin lock for a given run queue, allowing nested locks to be used. It uses mechanisms such as spin locks and loops to ensure thread safety, and disables preemption to avoid race conditions. When the scheduler is disabled, the function directly acquires the spinlock and returns; otherwise, it loops to acquire the spinlock until it succeeds.

Tips

There is a comment in the above code
/* Matches synchronize_rcu() in __sched_core_enable() */
This comment means that the code logic in this function matches the synchronize_rcu() call in the __sched_core_enable() function.

In the Linux kernel, RCU (Read-Copy-Update) is a non-blocking synchronization mechanism that avoids the use of locks by delaying update operations. When a shared resource needs to be modified, RCU will first create a new copy and apply the modification operation to the copy. Then, at the appropriate point in time, the RCU will merge the modified copy with the original data, thus completing the update operation. During this process, read operations can still continue to access the original data, because they only access immutable copies of the data.

In the scheduler, when the scheduler needs to be disabled, it is necessary to ensure that tasks on all current CPUs have entered the idle state. To achieve this, the kernel uses RCU to synchronize tasks on all CPUs. Specifically, when the scheduler is disabled, the kernel will call the __sched_core_disable() function, which will call the synchronize_rcu() function to wait for the RCU callback function on all CPUs to execute complete. When all RCU callback functions are executed, the kernel will continue to execute the operation of disabling the scheduler. Therefore, during the execution of the synchronize_rcu() function, all CPU tasks are blocked.

In this question, the code logic in this function matches the call of the synchronize_rcu() function, because it judges whether the scheduler has been disabled before disabling preemption. If the scheduler has been disabled, directly acquire the spin lock and enable preemption; otherwise, loop to try to acquire the spin lock until it succeeds. This can ensure that all current tasks on the CPU have entered the idle state, and avoid being interrupted by other threads during the acquisition of the spin lock.

#define preempt_disable() uatomic_inc( & preempt_count)
#define preempt_enable() uatomic_dec( & preempt_count)
These two macro definitions are used to disable and enable preemption in the Linux kernel.

1. `preempt_disable()`: This macro will use the atomic operation (uatomic_inc) to increase the preemption counter (preempt_count) on the current CPU by 1,
2. Preemption is thus disabled. When preemption is disabled, the current process will keep running until it voluntarily relinquishes the CPU or until preemption is re-enabled.

3. `preempt_enable()`: This macro will use the atomic operation (uatomic_dec) to decrement the preemption counter (preempt_count) on the current CPU by 1,
4. Preemption is thus enabled. When preemption is enabled, the current process may be interrupted by other high-priority processes to yield CPU resources.

These two macros are usually used to protect the critical section to ensure that it will not be interrupted by other processes or interrupts during access to shared resources.
For example, `preempt_disable()` can be used to disable preemption when making modifications to shared data structures in the kernel,
And use mechanisms such as corresponding spin locks or semaphores to protect shared data structures. Then, use `preempt_enable()` to enable preemption,
to allow other processes or interrupts to interrupt the current process.

Kernel code

static inline void __raw_read_lock(rwlock_t *lock)
{<!-- -->
preempt_disable();
rwlock_acquire_read( & amp; lock->dep_map, 0, 0, _RET_IP_);
LOCK_CONTENDED(lock, do_raw_read_trylock, do_raw_read_lock);
}

This is an inline function that implements the acquisition of a read lock. The function accepts a pointer to a lock variable of type rwlock_t as a parameter.

The function first calls the preempt_disable() function to disable preemption to ensure that no task switching occurs while the lock is held. Then call the rwlock_acquire_read() function to acquire the read lock, and pass the relevant parameters. This function is a general function used in the kernel to acquire read-write locks, and will perform corresponding processing according to the current state of the lock and whether there is competition.

If the read lock is currently occupied, or other processes are competing for the read lock, the LOCK_CONTENDED macro will be called to perform the wait (spin) operation. Among them, do_raw_read_trylock() and do_raw_read_lock() are functions that try to acquire a read lock and actually acquire a read lock, respectively, and which function will be called depends on the current lock status.

This function realizes the acquisition operation of the read lock by disabling preemption, calling the general read lock acquisition function and waiting mechanism.

Semaphore