About multi-process (thread) synchronization and mutual exclusion in linux

One problem that must be solved in Linux device drivers is the concurrent access of multiple processes to shared resources. Concurrent access will lead to race conditions. Linux provides a variety of ways to solve the race condition problem, and these ways are suitable for different application scenarios.

The Linux kernel is a multi-process, multi-threaded operating system, which provides a fairly complete kernel synchronization method. The list of kernel synchronization methods is as follows:
interrupt masking
atomic operation
spin lock
read-write spin lock
sequence lock
semaphore
read-write semaphore
BKL (big kernel lock)
Seq lock


1. Concurrency and competition:

Definition:
Concurrency refers to multiple execution units being executed simultaneously and in parallel, while concurrent execution units' access to shared resources (hardware resources and software global variables, static variables, etc.) can easily lead to race conditions ( race conditions) .
In linux, the main race conditions occur in the following situations:
1. Symmetrical multiprocessor (SMP)
The characteristic of multiple CPUs is that multiple CPUs use a common system bus, so they can access common peripherals and memory.
2. A process within a single CPU and the process that preempts it
3.
As long as multiple concurrent execution units have access to shared resources between interrupts (hard interrupts, soft interrupts, tasklets, bottom half) and processes, race conditions are possible occur.
Race conditions can also occur if an interrupt handler accesses a resource that the process is accessing.
Multiple interrupts may themselves cause concurrency and cause race conditions (interrupts being interrupted by higher priority interrupts).

The way to solve the race problem is to ensure mutually exclusive access to shared resources. The so-called mutually exclusive access means that when an execution unit accesses a shared resource, other execution units are prohibited from accessing.

The code area that accesses shared resources is called a critical area. The critical area needs to be protected by some mutual exclusion mechanism. Interrupt masking, atomic operations, spin locks, and semaphores are all mutually exclusive approaches that can be used in linux device drivers.

Critical regions and race conditions:
The so-called critical regions are code segments that access and manipulate shared data. In order to avoid concurrent access in critical regions, programmers must ensure that these codes are executed atomically—that is, the code is executing It cannot be interrupted until the end, just as the entire critical section is an indivisible instruction. If two threads of execution may be in the same critical section, then the program contains a bug. If this happens, we will These are called race conditions , and avoiding concurrency and preventing race conditions is called synchronization.

Deadlock:
Deadlock requires certain conditions: there must be one or more execution threads and one or more resources, each thread is waiting for one of the resources, but all resources have been occupied, all threads are They are waiting for each other, but they will never release the resources they have already occupied, so no thread can continue, which means a deadlock occurs.

2. Interrupt shielding

An easy way to avoid race conditions within a single CPU is to mask the system's interrupts before entering the critical section.
Since the process scheduling and other operations of the linux kernel rely on interrupts, the concurrency between the kernel preempting processes can be avoided.

How to use interrupt mask:
local_irq_disable()//shield interrupt
//critical area
local_irq_enable()//enable interrupt

Features:
Since many important operations such as asynchronous IO and process scheduling of the linux system depend on interrupts, all interrupts cannot be processed during the interrupt shielding period, so long-term shielding is very dangerous, which may cause data loss or even system crashes , which requires that after masking the interrupt, the current kernel execution path should execute the code in the critical section as soon as possible.
Interrupt masking can only prohibit interrupts within the CPU, so it cannot solve the race condition caused by multiple CPUs, so using interrupt masking alone is not a recommended method to avoid race conditions. It is generally used in conjunction with spin locks.

3. Atomic operations

Definition: An atomic operation is an operation whose execution is not interrupted by other code paths.
(Atoms originally refer to indivisible particles, so atomic operations are instructions that cannot be divided)
(It ensures that instructions are executed in an "atomic" manner and cannot be interrupted)
Atomic operations are indivisible, and after execution Not interrupted by any other tasks or events. In a uniprocessor system (UniProcessor), operations that can be completed in a single instruction can be considered "atomic operations" because interrupts can only occur between instructions. This is also the reason why instructions such as test_and_set and test_and_clear are introduced in some CPU instruction systems for mutual exclusion of critical resources. However, it is different in the Symmetric Multi-Processor (Symmetric Multi-Processor) structure. Since there are multiple processors running independently in the system, even operations that can be completed in a single instruction may be interfered. Let's take decl (decrement instruction) as an example, which is a typical "read-modify-write" process involving two memory accesses.

Popular understanding:
Atomic operations, as the name suggests, mean that they cannot be subdivided like atoms. An operation is an atomic operation, which means that the operation is executed in an atomic manner. It must be executed in one go. The execution process cannot be interrupted by other behaviors of the OS. It is an overall process. During its execution, the OS Other behaviors cannot be inserted .

Classification: The linux kernel provides a series of functions to implement atomic operations in the kernel, which are divided into integer atomic operations and bit atomic operations. The common point is that the operations are atomic in any case, and the kernel code can safely call them without not be interrupted.

Atomic integer operations:
atomic operations on integers can only be processed on atomic_t type data. The reason why a special data type is introduced here instead of directly using the int type of C language is mainly due to two reasons
: First, let the atomic function only accept the atomic_t type of operand, which can ensure that the atomic operation can only be used with this special type of data, and at the same time, it also ensures that the data of this type will not be passed to any other non-atomic functions;
second , Use the atomic_t type to ensure that the compiler does not optimize the access to the corresponding value-this makes the atomic operation finally receive the correct memory address, not an alias, and finally when implementing atomic operations on different architectures, using atomic_t can mask the differences.
The most common use of atomic integer operations is to implement counters.
Another point needs to be explained that the atomic operation can only guarantee that the operation is atomic, either completed or not completed, there is no possibility of half of the operation, but the atomic operation does not guarantee the order of the operation, that is, it cannot guarantee that the two operations are performed according to a certain completed in sequence. If you want to guarantee the sequentiality of atomic operations, use memory barrier instructions.

atomic_t和ATOMIC_INIT(i)定义
typedef struct { volatile int counter; } atomic_t;
#define ATOMIC_INIT(i) { (i) }

When you write code, try not to use complex locking mechanisms when you can use atomic operations. For most architectures, atomic operations bring less overhead to the system than more complex synchronization methods. The impact on cache lines is also small, but for code with high performance requirements, it is wise to test and compare various synchronization methods.

Atomic bit operations:
Functions that operate on bit-level data operate on ordinary memory addresses. Its parameters are a pointer and a bit number.

For convenience, the kernel also provides a set of non-atomic bit functions corresponding to the above operations. The operation of the non-atomic bit functions is exactly the same as that of the atomic bit functions. However, the former does not guarantee atomicity, and its name prefix has two more underscores. For example, the non-atomic counterpart to test_bit() is _test_bit(). If you don't need atomic operations (for example, if you have protected your data with locks), then these non-atomic bit functions are more efficient than atomic Bit functions may perform faster.

4. Spin lock

The introduction of spin locks:
It would be nice if each critical section could be as simple as adding variables. Unfortunately, this is not the case, but the critical section can span multiple functions, for example: first move data from a data result, right It performs format conversion and parsing, and finally adds it to another data structure. The entire execution process must be atomic. Before the data is updated, no other code can read the data. Obviously, simple atomic operations are Powerless (in a uniprocessor system (UniProcessor), operations that can be completed in a single instruction can be considered "atomic operations" because interrupts can only occur between instructions), which requires the use of more complex synchronization method - lock to provide protection.

Introduction to spin locks:

The most common lock in the Linux kernel is a spin lock (spin lock). A spin lock can only be held by one executable thread at most. If an execution thread tries to acquire a contended (already held) spin lock , then the thread will keep busy looping-spinning-waiting for the lock to become available again. If the lock is not contended, the execution thread requesting the lock can get it immediately and continue execution. At any time, the spin lock can prevent multiple Since a single thread of execution enters the comprehension zone at the same time, note that the same lock can be used in multiple places—for example, all access to a given piece of data can be protected and synchronized .

A contended spinlock causes the thread that requested it to spin while waiting for the lock to become available (a waste of processor time in particular), so spinlocks should not be held for long The original intention of the spinlock is to perform lightweight locking in a short period of time, and another method can be used to deal with the contention for the lock: let the request thread sleep and wake it up until the lock is available again, so that the processor does not have to cycle Waiting, you can execute other codes, which will also bring a certain amount of overhead - there are two obvious context switches here, and the blocked thread needs to be swapped out and swapped in. Therefore, the time spent holding the spin lock is preferably less than the time spent on completing two context switches. Of course, most of us will not be bored to measure the time spent on context switching, so we let the time spent holding the spin lock be as long as possible. As short as possible, semaphores can provide the second mechanism above, which allows waiting threads to go to sleep instead of spinning when contention occurs.

Spin locks can be used in interrupt handlers (semaphores cannot be used here because they will cause sleep). When using spin locks in interrupt handlers, be sure to disable local interrupts before acquiring the lock (in the current Interrupt request on the processor), otherwise, the interrupt handler will interrupt the kernel code that is holding the lock, and may try to contend for the already held spin lock, so that the interrupt handler will automatically Spin, waiting for the lock to be available again, but the lock holder cannot run until the interrupt handler finishes executing. This is exactly the double request deadlock we mentioned in the previous chapter. Note that what needs to be closed is only An interrupt on the current processor, if the interrupt occurs on a different processor, will not prevent the lock holder (on a different processor) from eventually releasing the lock even if the interrupt handler spins on the same lock.

Simple understanding of spinlock:
The easiest way to understand spinlock is to treat it as a variable that marks a critical section either as "I am currently running, please wait a moment" or as "I am currently running Not running, can be used". If the execution unit A enters the routine first, it will hold the spin lock. When the execution unit B tries to enter the same routine, it will know that the spin lock has been held, and it cannot enter until the execution unit A is released.

API function of spin lock:

In fact, the underlying source codes of several semaphores and mutual exclusion mechanisms introduced use spin locks, which can be understood as repackaging of spin locks. So from here we can understand why spin locks can usually provide higher performance than semaphores.
A spin lock is a mutex device that can only have two values: "locked" and "unlocked". It is usually implemented as a single bit in an integer.
The "test and set" operation must be done atomically.
Whenever the kernel code owns a spinlock, preemption on the associated CPU is prohibited.
The core rules applicable to spin locks:
(1) Any code that owns spin locks must be atomic, except for service interrupts (in some cases, the CPU cannot be given up, such as interrupt services must also obtain spin locks. In order To avoid this kind of lock trap, you need to disable interrupts when you have a spin lock), and you cannot give up the CPU (such as sleep, which can happen in many unexpected places). Otherwise, the CPU will probably spin forever (crash).
(2) The shorter the time of owning the spin lock, the better .

It should be emphasized that the spin lock is not designed for the synchronization mechanism of multiprocessors. For a single processor (for a single processor and non-preemptive kernel, the spin lock does nothing), the kernel will not The spinlock mechanism is introduced. For preemptible kernels, it is only used to set whether the kernel's preemption mechanism is enabled. That is to say, locking and unlocking actually become the prohibition or enablement of the kernel preemption function. If the kernel doesn't support preemption, then spinlocks aren't compiled into the kernel at all.
The spinlock_t type is used in the kernel to represent the spin lock, which is defined in

<linux/spinlock_types.h>:
typedef struct {
raw_spinlock_t raw_lock;
#if defined(CONFIG_PREEMPT) && defined(CONFIG_SMP)
unsigned int break_lock;
#endif
} spinlock_t;

For kernels that do not support SMP, struct raw_spinlock_t has nothing and is an empty structure. For kernels that support multiprocessors, struct raw_spinlock_t is defined as

typedef struct {
unsigned int slock;
} raw_spinlock_t;

slock indicates the status of the spin lock, "1" indicates that the spin lock is in the unlocked state (UNLOCK), and "0" indicates that the spin lock is in the locked state (LOCKED).
break_lock indicates whether the process is currently waiting for the spin lock. Obviously, it only works on the SMP kernel that supports preemption.

The implementation of spin lock is a complicated process. It is not complicated because of how much code or logic is needed to implement it. In fact, its implementation code is very small. The implementation of the spin lock is closely related to the architecture. The core code is basically written in assembly language. The core code related to the structure of the association is placed in the relevant directory, such as <asm/spinlock.h>. For us driver developers, we don't need to understand the internal details of such a spinlock. If you are interested in it, please refer to the Linux kernel source code. For the spinlock interface we drive, we only need to include the <linux/spinlock.h> header file. Before we introduce the API of spinlock in detail, let's take a look at a basic usage format of spinlock:
#include <linux/spinlock.h>
spinlock_t lock = SPIN_LOCK_UNLOCKED;

spin_lock(&lock);

spin_unlock(&lock);

In terms of use, the API of spinlock is still very simple. Generally, the APIs we will use are as follows. In fact, they are all macro interfaces defined in <linux/spinlock.h>. The real implementation is in

<asm/spinlock.h>中
#include <linux/spinlock.h>
SPIN_LOCK_UNLOCKED
DEFINE_SPINLOCK
spin_lock_init( spinlock_t *)
spin_lock(spinlock_t *)
spin_unlock(spinlock_t *)
spin_lock_irq(spinlock_t *)
spin_unlock_irq(spinlock_t *)
spin_lock_irqsace(spinlock_t *,unsigned long flags)
spin_unlock_irqsace(spinlock_t *, unsigned long flags)
spin_trylock(spinlock_t *)
spin_is_locked(spinlock_t *)


There are two initialization forms for initializing spinlock, one is static initialization and the other is dynamic initialization. For static spinlock objects, we initialize them with SPIN_LOCK_UNLOCKED, which is a macro. Of course, we can also declare the spinlock and initialize it together. This is the job of the DEFINE_SPINLOCK macro. Therefore, the following two lines of code are equivalent.
DEFINE_SPINLOCK (lock);
spinlock_t lock = SPIN_LOCK_UNLOCKED;

The spin_lock_init function is generally used to initialize a dynamically created spinlock_t object, and its parameter is a pointer to a spinlock_t object. Of course, it can also initialize a static uninitialized spinlock_t object.
spinlock_t *lock
...
spin_lock_init(lock);

• Acquiring a lock
The kernel provides three functions for acquiring a spin lock.
spin_lock: Acquire the specified spin lock.
spin_lock_irq: Disable local interrupts and acquire spin locks.
spin_lock_irqsace: Save the local interrupt status, disable the local interrupt and acquire the spin lock, and return the local interrupt status.

Spin locks can be used in interrupt handlers. In this case, you need to use a function that has the function of turning off local interrupts. We recommend using spin_lock_irqsave because it will save the interrupt flag before locking, so that the interrupt when unlocking will be restored correctly. sign. If the spin_lock_irq interrupt is turned off when locking, then the interrupt will be turned on by mistake when unlocking.

The other two functions related to spin lock acquisition are:
spin_trylock(): try to acquire a spin lock, and return a non-zero value immediately if the acquisition fails, otherwise return 0.
spin_is_locked(): Determine whether the specified spin lock has been acquired. Returns non-zero if yes, otherwise, returns 0.
• Release the lock
Corresponding to the acquisition of the lock, the kernel provides three relative functions to release the spin lock.
spin_unlock: Release the specified spin lock.
spin_unlock_irq: Release the spin lock and activate the local interrupt.
spin_unlock_irqsave: Release the spin lock and restore the saved local interrupt state.

Five, read and write spin lock

If the data protected by the critical section is readable and writable, as long as there is no write operation, concurrent operations can be supported for reading. For this requirement that only write operations are mutually exclusive, it is obviously impossible to meet this requirement if you still use spin locks (it is too wasteful for read operations). For this reason, the kernel provides another kind of lock - read-write spin lock, read spin lock is also called shared spin lock, and write spin lock is also called exclusive spin lock.
The read-write spin lock is a lock mechanism with a smaller granularity than the spin lock. It retains the concept of "spin", but in terms of write operations, there can only be at most one writing process. In terms of read operations, there can be Multiple read execution units, of course, read and write cannot be performed at the same time.
The use of read-write spin locks is similar to that of ordinary spin locks. First, the read-write spin lock object must be initialized:
// Static initialization
rwlock_t rwlock = RW_LOCK_UNLOCKED;
// Dynamic initialization
rwlock_t *rwlock;

rw_lock_init(rwlock);

Acquire a read spin lock on shared data in the read operation code:
read_lock(&rwlock);

read_unlock(&rwlock);

Acquire a write spinlock for shared data in the write code:
write_lock(&rwlock);

write_unlock(&rwlock);

It should be noted that if there are a large number of write operations, the write operation will spin on the write spin lock and be in a write starvation state (waiting for the read spin lock to be fully released), because the read spin lock will freely acquire the read spin lock.

The functions of reading and writing spin locks are similar to ordinary spin locks, so we will not introduce them one by one here, but we list them in the table below.

RW_LOCK_UNLOCKED
rw_lock_init(rwlock_t *)
read_lock(rwlock_t *)
read_unlock(rwlock_t *)
read_lock_irq(rwlock_t *)
read_unlock_irq(rwlock_t *)
read_lock_irqsave(rwlock_t *, unsigned long)
read_unlock_irqsave(rwlock_t *, unsigned long)
write_lock(rwlock_t *)
write_unlock(rwlock_t *)
write_lock_irq(rwlock_t *)
write_unlock_irq(rwlock_t *)
write_lock_irqsave(rwlock_t *, unsigned long)
write_unlock_irqsave(rwlock_t *, unsigned long)
rw_is_locked(rwlock_t *)

Six, sequence

Sequence lock (seqlock) is an optimization of read-write lock. If you use sequence lock, the read execution unit will never be blocked by the write execution unit. When the shared resource is performing a write operation, it can still continue to read without waiting for the write execution unit to complete the write operation, and the write execution unit does not need to wait for all the read execution units to complete the read operation before performing the write operation.
However, the write execution unit and the write execution unit are still mutually exclusive, that is, if a write execution unit is performing a write operation, other write execution units must spin until the write execution unit releases the sequence lock.
If the read execution unit has already performed a write operation during the read operation, then the read execution unit must re-read the data to ensure that the obtained data is complete. The probability of such locks being read and written at the same time is relatively small. The performance is very good, and it allows reading and writing at the same time, thus greatly improving the concurrency.
Note that the sequence has a limitation, that is, the shared resource it must be protected does not contain pointers, because the write execution unit may make the pointer Invalid, but the read execution unit will cause Oops if it is trying to access the pointer.

Seven, semaphore

The semaphore in Linux is a sleep lock. If a task tries to obtain an already occupied semaphore, the semaphore will push it into a waiting queue and then let it sleep. At this time, the processor can regain its freedom. In order to execute other codes, when the process holding the semaphore releases the semaphore, which task in the waiting queue is woken up and obtains the semaphore.
Semaphore, or flag, is the classic P/V primitive operation we learn in the operating system.
P: If the semaphore value is greater than 0, decrement the value of the semaphore, and the program continues to execute, otherwise, sleep and wait for the semaphore to be greater than 0.
V: Increment the value of the semaphore, if the value of the incremented semaphore is greater than 0, wake up the waiting process.

The value of the semaphore determines how many processes can enter the critical section at the same time. If the initial value of the semaphore is 1, the semaphore is a mutual exclusion semaphore (MUTEX). A semaphore with a non-zero value greater than 1 can also be called a counting semaphore. The semaphores used by general drivers are mutex semaphores.
Similar to spinlock, the implementation of semaphore is also closely related to the architecture. The specific implementation is defined in the <asm/semaphore.h> header file. For the x86_32 system, its definition is as follows: struct semaphore { atomic_t
count ; int sleepers; wait_queue_head_t wait; };



The initial value count of the semaphore is of atomic_t type, which is an atomic operation type, and it is also a kernel synchronization technology. It can be seen that the semaphore is based on atomic operations. We will introduce atomic operations in detail later in the atomic operations section.

The use of semaphores is similar to spinlocks, including creation, acquisition and release. Let's show the basic usage of semaphore first:
static DECLARE_MUTEX(my_sem);

if (down_interruptible(&my_sem))

{
return -ERESTARTSYS;
}

up(&my_sem)

The semaphore function interface in the Linux kernel is as follows:
static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);
seam_init(struct semaphore *, int);
init_MUTEX(struct semaphore *);
init_MUTEX_LOCKED(struct semaphore *)
down_interruptible(struct semaphore *);
down(struct semaphore *)
down_trylock(struct semaphore *)
up(struct semaphore *)
• Initialize semaphore
The initialization of semaphore includes static initialization and dynamic initialization. Static initialization is used to statically declare and initialize semaphores.
static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);

For dynamically declared or created semaphores, the following functions can be used for initialization:
seam_init(sem, count);
init_MUTEX(sem);
init_MUTEX_LOCKED(struct semaphore *)

Obviously, the function with MUTEX initializes the mutex semaphore. LOCKED initializes the semaphore to a locked state.
• Use semaphore
After the semaphore is initialized, we can use it
down_interruptible(struct semaphore *);
down(struct semaphore *)
down_trylock(struct semaphore *)
up(struct semaphore *)

The down function will try to acquire the specified semaphore. If the semaphore is already used, the process will enter an uninterruptible sleep state. down_interruptible puts the process into an interruptible sleep state. Regarding the details of the process state, we are introducing it in detail in the process management of the kernel.

down_trylock tries to acquire the semaphore, returns 0 if the acquisition is successful, and returns non-zero immediately if it fails.

Use the up function to release the semaphore when exiting the critical section, and wake up one of the waiting processes if the sleep queue on the semaphore is not empty.

Eight, read and write semaphores

Similar to spin locks, semaphores also have read and write semaphores. The read and write semaphore API is defined in the <linux/rwsem.h> header file, and its definition is actually related to the architecture, so the specific implementation is defined in the <asm/rwsem.h> header file. The following is an example for x86:
struct rw_semaphore { signed long count; spinlock_t wait_lock; struct list_head wait_list; };



The first thing to explain is that all read and write semaphores are mutually exclusive semaphores. The read lock is a shared lock, which means that multiple read processes are allowed to hold the semaphore at the same time, but the write lock is an exclusive lock, and only one write lock can hold the mutually exclusive semaphore at the same time. Obviously, write locks are exclusive, including exclusive read locks. Since the write lock is a shared lock, it allows multiple reader processes to hold the lock, as long as no process holds the write lock, it will always successfully hold the lock, so this will cause the writer process to write starvation.

Before using the read-write semaphore, it must be initialized. As you can imagine, it is almost the same as the read-write spin lock in use. Let's first look at the creation and initialization of the read-write semaphore:
// Static initialization
static DECLARE_RWSEM(rwsem_name);

// Dynamically initialize
static struct rw_semaphore rw_sem;
init_rwsem(&rw_sem);

Read process obtains semaphore protection critical section data:
down_read(&rw_sem);

up_read(&rw_sem);

Write process obtains semaphore protection critical section data:
down_write(&rw_sem);

up_write(&rw_sem);

For more read and write semaphore APIs, please refer to the following table:

#include <linux/rwsem.h>
DECLARE_RWSET(name);
init_rwsem(struct rw_semaphore *);
void down_read(struct rw_semaphore *sem);
void down_write(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
void downgrade_write(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);

Like the spinlock, down_read_trylock and down_write_trylock will try to acquire the semaphore, and return 1 if the acquisition is successful, otherwise return 0. It's strange why the return value is opposite to the corresponding function of the semaphore, and you must be careful when using it.

Nine, the difference between spin lock and semaphore

In the driver program, when multiple threads access the same resource at the same time (the global variable in the driver program is a typical shared resource), a "race" may occur, so we must control the concurrency of the shared resource. The most commonly used methods to solve concurrency control in the Linux kernel are spin locks and semaphores (used as mutexes most of the time).

Spinlocks and semaphores are "similar but not similar", which means that they are similar in function, and "different" means that they are completely different in essence and implementation mechanism, and do not belong to the same category.

The spin lock will not cause the caller to sleep. If the spin lock has been held by other execution units, the caller will keep looping to see if the spin lock holder has released the lock. "Spin" means "spin in place" ". The semaphore causes the caller to sleep, which drags the process out of the run queue unless the lock is acquired. This is their "different".

However, whether it is a semaphore or a spin lock, there can be at most one holder at any time, that is, at most one execution unit can acquire the lock at any time. That's how "similar" they are.

In view of the above-mentioned characteristics of spin locks and semaphores, generally speaking, spin locks are suitable for very short holding times and can be used in any context; semaphores are suitable for long holding times and can only be used in processes contextual use . If the protected shared resource is only accessed in the process context, the shared resource can be protected with a semaphore. If the access time to the shared resource is very short, a spin lock is also a good choice. However, if the protected shared resource needs to be accessed in the interrupt context (including the bottom half, the interrupt handler and the top half, the soft interrupt), spin locks must be used.
The differences are summarized as follows:
1. Since the processes competing for the semaphore will sleep while waiting for the lock to become available again, the semaphore is suitable for situations where the lock will be held for a long time.
2. On the contrary, when the lock is held for a short time, it is not appropriate to use the semaphore, because the time-consuming caused by sleep may be longer than the entire time that the lock is occupied.
3. Since the execution thread will sleep when the lock is contended, the semaphore lock can only be acquired in the process context, because scheduling cannot be performed in the interrupt context (using a spin lock).
4. You can go to sleep while holding the semaphore (of course you may not need to sleep), because when other processes try to obtain the same semaphore, there will be no deadlock, (because the process is just going to sleep, And you will eventually continue execution).
5. You cannot occupy the spin lock while you occupy the semaphore, because you may sleep while waiting for the semaphore, but you are not allowed to sleep while holding the spin lock.
6. The critical section protected by the semaphore lock can contain code that may cause blocking, while the spin lock must absolutely avoid being used to protect the critical section containing such code, because blocking means switching the process, if the process is switched out Later, another process tries to acquire this spin lock, and a deadlock will occur.
7. The semaphore is different from the spin lock, it will not prohibit the kernel preemption (when the spin lock is held, the kernel cannot be preempted), so the code holding the semaphore can be preempted, which means that the semaphore will not be preempted. Scheduling wait times have a negative impact.
In addition to the synchronization mechanism methods introduced above, there are BKL (big kernel lock), Seq lock, etc.
BKL is a global spin lock, which is mainly used to facilitate the transition from Linux's original SMP to a fine-grained locking mechanism.
Seq locks are used to read and write shared data, and the realization of such locks only depends on a sequence counter.

Guess you like

Origin blog.csdn.net/qq_44333320/article/details/125986257