[Java concurrent programming] lock mechanism (1): the realization of the lock mechanism in Linux (understand)

1 critical section

1.1 Introduction

In early computer systems, there was only one task process being executed, and there was no resource sharing and competition. With the rapid development of technology and requirements, a single CPU processes multiple task processes at the same time in a period of time through time slicing. When multiple processes access shared resources concurrently, it causes race conditions between processes. This includes the well-known SMP (Symmetric Multi-Processor Structure) system, competition among multiple cores, single CPU interruption and mutual preemption among processes and many other issues.

More specifically, for a certain piece of code, it may be executed multiple times in the program (multithreading is a typical scenario). The process of each execution is called the execution path of the code. When two or more When the code path competes for common resources, the code segment is the critical section as shown in the figure

img

In order to protect shared resources from being accessed at the same time, a variety of synchronization lock mechanisms are provided in the Linux kernel, including: atomic operations, spin locks, semaphores, mutexes, etc., except for atomic operations, regardless of the lock mechanism It is not a free lunch. The lock operation is accompanied by high-consumption processes such as switching from user mode to kernel mode and process context switching.

1.2 Switch between user mode and kernel mode

In order to centralize management and reduce the access and use conflicts of limited resources, the CPU has set multiple privilege levels. For the Intel x86 architecture CPU, there are a total of 0~3 four privilege levels, with level 0 being the highest and level 3 being the lowest. When executing each instruction, the privilege level of the instruction will be checked accordingly. The related concepts are CPL, DPL and RPL, so I won't elaborate on it here. For security considerations, the Linux system is divided into kernel mode and user mode, which run in kernel space and user space respectively, and correspondingly use level 0 privilege level and level 3 privilege level.

Kernel mode programs can execute privileged instructions, and the operating system itself runs in it.

User mode does not allow direct access to the core data of the operating system, equipment and other key resources. You must enter the kernel mode through a system call or interrupt before you can access it. When the system call or interrupt returns, it returns to user space.

The steps to switch from user mode to kernel mode mainly include:

  1. Extract the ss0 and esp0 information of the kernel stack from the descriptor of the current process.
  2. Use the kernel stack pointed to by ss0 and esp0 to save the cs, eip, eflags, ss, and esp information of the current process. This process also completes the switching process from the user stack to the kernel stack, and at the same time saves the execution of the suspended program. One instruction.
  3. Load the cs and eip information of the interrupt handler previously retrieved by the interrupt vector into the corresponding register and start the execution of the interrupt handler. At this time, the program execution in the kernel mode is transferred.

Simply put, switching between user mode and kernel mode generally requires saving the context of the user program. When entering the kernel, it is necessary to save the registers of the user mode. When the kernel mode returns to the user mode, the contents of these registers will be restored. In other words, this is a big overhead.

1.3 Process context switching

The definition of context switch, http://www.linfo.org/context_switch.html has been explained in detail in this article, only the following key points are refined:

  • Process context switching can be described as the kernel performing the following operations
    1. Suspend a process and store the status of the process's registers and program counter at the time
    2. Resume the next process to be executed from the memory, restore the original state of the process to the register, return to its last suspended execution code and continue execution
  • Context switching can only occur in the kernel mode, so it will trigger the user mode and the kernel mode switch

2. Linux lock mechanism

2.1 Spin lock

The implementation of spin lock is to protect a short critical section operation code, to ensure that the operation of this critical section is atomic, thereby avoiding concurrent competition. In the Linux kernel, spinlocks are usually used for operations that include kernel data structures. You can see that spinlocks are embedded in many kernel data structures. Most of these are used to ensure that it is operated atomically. Such structures all go through this process: lock-operate-unlock. If the kernel control path finds that the spinlock is "open" (can be acquired), it acquires the lock and continues its execution.

On the contrary, if the kernel control path finds that the lock is "locked" by the kernel control path running on another CPU, it "spins" in place and repeatedly executes a tight loop detection instruction until the lock is released. Spinlock is a loop detection "busy waiting", that is, the kernel has nothing to do (except wasting time) while waiting, and the process keeps running on the CPU, so the critical area it protects must be small and the operation process must be short. However, spin locks are usually very convenient, because many kernel resources only lock a very short period of time, so waiting for the release of the spin lock will not consume too much CPU time.

2.1.1 What the spin lock needs to do

Considering the purpose of ensuring the atomicity of access to critical regions, spin locks should prevent any concurrent interference that occurs during code execution. These "interferences" include:

  1. Interrupts, including hardware interrupts and software interrupts (only needed when the interrupt code may access the critical section) This interference exists in any system. The arrival of an interrupt causes the execution of the interrupt routine. If the critical area is accessed in the interrupt routine Zone, the atomicity is broken. Therefore, if there is a code that accesses a critical section in a certain interrupt routine, it must be protected with spinlock. For different types of interrupts (hardware interrupts and software interrupts) corresponding to different versions of the spin lock implementation, which contains the interrupt disable and enable code. But if you guarantee that no interrupt code will access the critical section, then use the spinlock API without interrupt disable.
  2. Kernel preemption (only exists in preemptible kernels) In kernels after 2.6, kernel preemption is supported and is configurable. This makes the UP system similar to SMP, and there will be concurrency in the kernel mode. In this case, entering the critical section needs to avoid concurrency caused by preemption, so the solution is to disable preemption when locking (preempt_disable();), and enable preemption when unlocking (preempt_enable(); note that it will be executed once at this time Preemptive scheduling)
  3. Access to the same critical section by other processors (SMP system only) In an SMP system, multiple physical processors work at the same time, resulting in possible physical concurrency of multiple processes. In this way, a flag needs to be added to the memory, and every code that needs to enter the critical section must check this flag to see if any process is already in the critical section. In this case, the code for checking the flag must also be atomic and fast, which requires careful implementation. Under normal circumstances, each framework has its own assembly implementation plan to ensure the atomicity of the check.

According to the above introduction, we can easily know that the operation of the spin lock includes:

  • Interrupt control (only needed when the interrupt code may access the critical section)
  • Preemption control (only needed in preemptible kernels)
  • Spin lock flag control (only required for SMP system)

Interrupt control is based on different code access critical sections and different variants are selected during programming. Some APIs have them, some do not. The preemption control and spin lock flag control are determined at compile time based on the kernel configuration (whether kernel preemption is supported) and the hardware platform (whether it is SMP). If not needed, the corresponding control code is compiled into an empty function. For non-preemptive kernels, each critical section protected by a spinlock has an API that prohibits kernel preemption, but it is a no-op. Since there is no physical parallelism in the UP system, the spin part can be castrated, leaving the preemption and interrupt operation part.

Some people think that the spin detection of the spin lock can be realized with for, this kind of idea is "Too young, too simple, sometimes naive"! You can use C to explain in theory, but if you use for, at least there will be two problems:

  1. How do you ensure that other processors will not access the same logo at the same time under SMP? (That is, exclusive access to the logo)
  2. It must be ensured that each processor will not read the cache but the real mark in the memory (it can be realized, volitale can be used in programming). To solve this problem fundamentally, it is necessary to realize exclusive access to the physical memory address at the bottom of the chip , And use special assembly instructions to access. Please see the implementation analysis of the spin lock in the reference material. Take arm as an example. Starting from the ARM architecture instruction set (V6, V7) where SMP exists, LDREX and STREX instructions are used to achieve true spin waiting.

2.1.2 Rules for the use of spin lock variants

Regardless of whether it is a preemptive UP, non-preemptive UP or SMP system, as long as a certain type of interrupt code may access a critical area, it is necessary to control the interrupt to ensure the atomicity of the operation. So this has something to do with the access of the critical section in the module code. Whether it is possible to operate the critical section in an interrupt, only the programmer knows. So there are spin lock variants for different interrupt types in the spin lock API: the critical section will not be operated in any interrupt routine

static inline void spin_lock(spinlock_t *lock)

static inline void spin_unlock(spinlock_t *lock)

If the critical section is operated in a software interrupt:

static inline void spin_lock_bh(spinlock_t *lock)
static inline void spin_unlock_bh(spinlock_t *lock)

bh stands for bottom half, that is, the bottom half of the interrupt. The bottom half of the kernel interrupt is generally handled by software interrupts (tasklet, etc.), so it gets its name.
If the critical section is operated in a hardware interrupt:

static inline void spin_lock_irq(spinlock_t *lock)
static inline void spin_unlock_irq(spinlock_t *lock)

If you need to save the interrupt status while controlling the hardware interrupt:

spin_lock_irqsave(lock, flags)
static inline void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)

The description of these situations seems a bit simple. I found an article on the Internet ((turn) spinlock explained classic and thorough), which is very detailed. I made a slight modification and reproduced as follows:

There are several versions of acquiring and releasing a spin lock, so it is very necessary to let the reader know what version of the macro to acquire and release the lock under what circumstances.

If the protected shared resource is only accessed in the process context and soft interrupt (including tasklet, timer) context, then when the shared resource is accessed in the process context, it may be interrupted by the soft interrupt, which may enter the soft interrupt context to protect the Access to shared resources, so in this case, access to shared resources must be protected with spin_lock_bh and spin_unlock_bh. Of course, spin_lock_irq and spin_unlock_irq and spin_lock_irqsave and spin_unlock_irqrestore can also be used. They invalidate the local hard interrupt, and the invalid hard interrupt implicitly invalidates the soft interrupt. But using spin_lock_bh and spin_unlock_bh is the most appropriate, it is faster than the other two.

If the protected shared resource is only accessed in two or more tasklet or timer contexts, then access to the shared resource only needs to be protected with spin_lock and spin_unlock, and the _bh version is not necessary, because it is impossible when tasklet or timer is running There are other tasklets or timers running on the current CPU.

If the protected shared resource is only accessed in a tasklet or timer context, then no spin lock protection is needed, because the same tasklet or timer can only run on one CPU, even in the SMP environment. In fact, the tasklet has been bound to the current CPU when it calls tasklet_schedule to mark that it needs to be scheduled, so the same tasklet can never run on other CPUs at the same time. The timer is also assigned to the current CPU when it is added to the timer queue using add_timer, so the same timer can never run on other CPUs. Of course, it is even more impossible for two instances of the same tasklet to run on the same CPU at the same time.

If the protected shared resource is only accessed in the context of a soft interrupt (except tasklet and timer), then this shared resource needs to be protected with spin_lock and spin_unlock, because the same soft interrupt can run on different CPUs at the same time.

If the protected shared resource is accessed in two or more soft interrupt contexts, of course, this shared resource needs to be protected with spin_lock and spin_unlock. Different soft interrupts can run on different CPUs at the same time.

If the protected shared resource is accessed in soft interrupt (including tasklet and timer) or process context and hard interrupt context, then during soft interrupt or process context access, it may be interrupted by hard interrupt, thus entering the hard interrupt context to perform shared resources Access, therefore, spin_lock_irq and spin_unlock_irq need to be used in the context of the process or soft interrupt to protect access to shared resources.

The version used in the interrupt handler depends on the situation. If only one interrupt handler accesses the shared resource, then only spin_lock and spin_unlock are required in the interrupt handler to protect access to the shared resource. Because during the execution of the interrupt handler, it is impossible to be interrupted by a soft interrupt or process on the same CPU.

But if there are different interrupt handling handles accessing the shared resource, then spin_lock_irq and spin_unlock_irq need to be used in the interrupt handling handle to protect access to the shared resource.

In the case of using spin_lock_irq and spin_unlock_irq, you can use spin_lock_irqsave and spin_unlock_irqrestore instead. Which one should be used depends on the situation. If you can be sure that interrupts are enabled before accessing shared resources, then spin_lock_irq is better some. Because it is faster than spin_lock_irqsave, but if you are not sure whether the interrupt is enabled, it is better to use spin_lock_irqsave and spin_unlock_irqrestore, because it will restore the interrupt flag before accessing the shared resource instead of directly enabling the interrupt.

Of course, in some cases, the interrupt must be invalid when accessing shared resources, and the interrupt must be enabled after the access is completed. In this case, it is best to use spin_lock_irq and spin_unlock_irq.

spin_lock is used to prevent simultaneous access to shared resources by execution units on different CPUs and asynchronous access to shared resources caused by mutual preemption of different process contexts, while interrupt invalidation and soft interrupt invalidation are to prevent soft interrupts on the same CPU Or interrupt asynchronous access to shared resources.

2.1.3 Spin lock use and precautions

The spin lock is used as follows;

//1.分配自旋锁
spinlock_t lock;
//2.初始化自旋锁
spin_lock_init(&lock);
//3.访问临界区之前获取锁:
spin_lock(&lock);  //获取自旋锁,立即返回,如果没有获取锁,将进行忙等待
 或者
spin_trylock(&lock); //获取锁,返回true,否则返回false,所以这个函数一定要对返回值进行判断!
//4 .访问临界区
//5.释放自旋锁
spin_unlock(&lock);

Notes for spin locks:

  1. The spin lock keeps the CPU in a busy state, so the execution time of the critical section should be as short as possible;

  2. Spin locks are not reentrant;

  3. The critical section protected by the spin lock should not have a sleep operation:

    1) For an interrupted spin lock, the following two situations may occur in the sleep operation:
    a. Deadlock: Task A sleeps after obtaining the spin lock, and then an interrupt occurs, and the interrupt handler intends to acquire the same A spin lock, self-deadlock will occur at this time-the spin lock is not reentrant.
    b. CPU waste: If there is no operation to acquire the same spin lock inside the interrupt handler, theoretically scheduling can occur. Suppose that process B intends to gain control of the CPU, but because process A has not unlocked the spin lock yet, it is still in the critical region of the spin lock, causing process B to fail to run. That is to say, the CPU will not be able to run any programs, and will always be in a state of nothing to do, causing waste of the CPU.

    2) For spinlocks that turn off interrupts by the way, it is obvious that it is impossible to sleep in the critical region, because waking up a sleeping process depends on the scheduler, and the scheduler uses the clock interrupt to determine the appropriate wake-up process. When the process sleeps when the interrupt is turned off, the scheduler will no longer receive the clock interrupt (because the operation of turning on the interrupt is also controlled by the process), so it will never be able to wake up the sleeping process. That is to say, the process will be asleep.

To put it simply, the original intention of the spin lock is to perform lightweight locking in a short period of time. A contended spin lock causes the thread requesting it to spin while waiting for the lock to be available again (especially a waste of processor time), so the spin lock should not be held for too long. If you need to lock for a long time, it is best to use semaphores.

2.2 Semaphore

Semaphores are used to coordinate data objects between different processes, and the most important application is inter-process communication in shared memory. Essentially, a semaphore is a counter, which is used to record access to a resource (such as shared memory). Generally speaking, in order to obtain shared resources, the process needs to perform the following operations:

  1. Test the semaphore that controls the resource.
  2. If the value of this semaphore is positive, the resource is allowed to be used. The process decrements the semaphore by 1.
  3. If the semaphore is 0, the resource is currently unavailable, and the process enters the sleep state, until the semaphore value is greater than 0, the process is awakened, go to step 1
  4. When the process no longer uses a resource controlled by a semaphore, the semaphore value is increased by 1. If a process is sleeping and waiting for this semaphore at this time, the process is awakened.

It is the Linux kernel operating system, not the user process, that maintains the semaphore state. We can see from the header file /usr/src/linux/include/linux/sem.h the definition of each structure used by the kernel to maintain the semaphore state. A semaphore is a collection of data, and users can use each element of this collection individually. The first function to be called is semget to obtain a semaphore ID. Semaphore structure defined under Linux2.6.26:

struct semaphore {
    
    
    spinlock_t lock;
    unsigned int count;
    struct list_head wait_list;
};

From the definition of the semaphore above, it can be seen that the underlying semaphore uses the locking mechanism of spin lock. This spinlock is mainly used to ensure the atomic operation (count–) and test (count> 0) of the count member.

2.2.1 P operation of semaphore

  1. void down(struct semaphore *sem);
  2. int down_interruptible(struct semaphore *sem);
  3. int down_trylock(struct semaphore *sem);

Function 1 means that the process will sleep when the signal cannot be applied for; for function (2), it means that if the process goes to sleep because it cannot apply for the semaphore, it can be interrupted by the signal. The signal mentioned here is Refers to the signal of inter-process communication, such as our Ctrl+C, but the return value of this function is not 0 at this time;

int down_interruptible(struct semaphore *sem)
{
    
    
        unsigned long flags;
        int result = 0;
  
        spin_lock_irqsave(&sem->lock, flags);
        if (likely(sem->count > 0))
                sem->count--;
        else
                result = __down_interruptible(sem);
        spin_unlock_irqrestore(&sem->lock, flags);
  
        return result;
}

Understanding of this function: Under the premise of ensuring atomic operations, first test whether count is greater than 0, if it means that the semaphore can be obtained, in this case, you need to first count-to ensure that other processes can obtain the semaphore , And then the function returns, and its caller begins to enter the critical section. If the semaphore is not obtained, the current process uses the wait_list in the struct semaphore to join the waiting queue and start sleeping.

For situations that require sleep, in the __down_interruptible() function, a variable of type struct semaphore_waiter will be constructed (struct semaphore_waiter is defined as follows:

struct semaphore_waiter
{
    
    
    struct list_head list;
    struct task_struct *task;
    int up;
};

Assign the current process to the task, and use its list member to add the variable node to a list headed by wait_list in the sem. Assuming that multiple processes call down_interruptible on the sem, the sem's wait_list is formed The queue is as follows:

img

Schematic diagram of waiting queue

(Note: To block a process, the general process is to first put the process in the waiting queue, and then change the state of the process, such as TASK_INTERRUPTIBLE, and then call the scheduling function schedule(), which will remove the current process from the cpu Taken from the run queue)
function (3) tries to obtain a semaphore. If it is not obtained, the function immediately returns 1 without letting the current process go to sleep.

2.2.2 V operation of semaphore

void up(struct semaphore *sem); the
prototype is as follows:

void up(struct semaphore *sem)
{
    unsigned long flags;
    spin_lock_irqsave(&sem->lock, flags);
    if (likely(list_empty(&sem->wait_list)))
            sem->count++;
    else
            __up(sem);
    spin_unlock_irqrestore(&sem->lock, flags);
}

If there are no other threads waiting on the semaphore that is currently about to be released, then just count + +. If other threads are sleeping because they are waiting for the semaphore, call
the definition of __up. __up:

static noinline void __sched __up(struct semaphore *sem)
{
    struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list, struct semaphore_waiter, list);
    list_del(&waiter->list);
    waiter->up = 1;
    wake_up_process(waiter->task);
}

This function first obtains the first valid node of the linked list with the wait_list at the head of the sem, then deletes it from the linked list, and then wakes up the sleeping process on the node. It can be seen that for every down_interruptible call on the sem, a new node is added to the end of the wait_list list of the sem. For each up call on the sem, the first valid node in the wait_list list is deleted, and the process sleeping on that node is awakened.

2.2.3 The use of semaphore

//1.分配信号量对象
  struct semaphore sema;
//2.初始化为互斥信号量
  init_MUTEX(&sema);
或者:
  DECLARE_MUTEX(sema);
//3.访问临界区之前获取信号量
  down(&sema);
  //如果获取信号量,立即返回
  //如果信号量不可用,进程将在此休眠,并且休眠的状态是 [ 不可中断的休眠状态 TASK_UNINTERRUPTIBLE] !
  或者
  down_interruptible(&sema);
  //如果信号量不可用,进程将进入 [ 可中断的休眠状态 TASK_INTERRUPTIBLE ],如果返回0表示正常获取信号,如果返回非0,表示接受到了信号
  down_trylock();
  //获取信号,如果信号量不可用,返回非0,如果信号量可用,返回0;不会引起休眠,可以在中断上下文使用。返回值也要做判断!
//4.访问临界区:临界区可以休眠
//5.释放信号量
  up(&sema);
  //不仅仅释放信号量,然后唤醒休眠的进程,让这个进程去获取信号量来访问临界区

2.3 Mutex

Mutexes implement a simple form of "mutual exclusion" synchronization (hence the name mutex). The mutex prohibits multiple threads from entering the "critical section" of the protected code at the same time. Therefore, at any time, only one thread is allowed to enter such a code protection area. Before any thread enters the critical section, it must acquire (acquire) the ownership of the mutex associated with this section. If another thread already owns the mutex of the critical section, other threads can no longer enter it. These threads must wait until the current owner thread releases the mutex.

The definition of mutex in Linux 2.6.26:

struct mutex {
    
    
        /* 1: unlocked, 0: locked, negative: locked, possible waiters */
        atomic_t                  count;
        spinlock_t                wait_lock;
        struct list_head          wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
        struct thread_info        *owner;
        const char                *name;
        void                      *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
        struct lockdep_map         dep_map;
#endif
};

Compared with the previous struct semaphore, struct mutex has almost the same appearance as semaphore except for adding several member variables for debugging purposes. But the introduction of mutex is mainly to provide a mutual exclusion mechanism to avoid multiple processes running in a critical section at the same time.

If you statically declare a semaphore variable with count=1, you can use DECLARE_MUTEX(name), DECLARE_MUTEX(name) actually defines a semaphore, so its use should correspond to the P and V functions of the semaphore.

If you want to define a static mutex variable, you should use DEFINE_MUTEX

If you want to initialize a mutex variable during the running of the program, you can use mutex_init (mutex). Mutex_init is a macro. Inside the macro definition, the __mutex_init function is called.

#define mutex_init(mutex) \
do { \
    static struct lock_class_key __key; \
    \
    __mutex_init((mutex), #mutex, &__key); \
} while (0)
  
void __mutex_init(struct mutex *lock, const char *name, struct lock_class_key *key)
{
    
    
    atomic_set(&lock->count, 1);
    spin_lock_init(&lock->wait_lock);
    INIT_LIST_HEAD(&lock->wait_list);
    debug_mutex_init(lock, name, key);
  
}

It can be seen from the definition of __mutex_init that when using the mutex_init macro to initialize a mutex variable, the pointer type of mutex should be used.
P and V operations on mutex: void mutex_lock(struct mutex *lock) and void __sched mutex_unlock(struct mutex *lock)

In principle, mutex is actually a semaphore under count=1, so its PV operation should be the same as semaphore. But in the actual Linux code, from the perspective of performance optimization, it is not just a simple reuse of down_interruptible and up code. Take the mutex_lock of the ARM platform as an example. In fact, mutex_lock is implemented in two parts: fast path and slow path, mainly based on the fact that in most cases, the code that tries to obtain the mutex can always be successfully obtained . Therefore, the Linux code uses the LDREX and STREX instructions on ARM V6 to implement fast path in order to obtain the best execution performance.
mutux underlying support:

Linux: The underlying pthread mutex is implemented using futex(2) (fast userspace mutex), so there is no need to get into a system call (switch from user mode to kernel mode) every time you lock or unlock.

futex(2): fast userspace mutex (fast userspace mutex), in the case of non-race state (or non-lock contention, which means that the application can get the lock immediately without waiting), the futex operation is completely in the user state The kernel mode is only responsible for processing the operation under the race state (or lock contention, which means that the application has been applied but other threads have obtained the lock in advance and need to wait for the lock to be released before obtaining) (call the corresponding system call to arbitrate) , Futex has two main functions:

futex_wait(addr, val)   //检查*addr == val ?,若相等则将当前线程放入等待队列addr中随眠(陷入到内核态等待唤醒),否则调用失败并返回错误(依旧处于用户态)
futex_wake(addr, val)   //唤醒val个处于等待队列addr中的线程(从内核态回到用户态

Therefore, the mutex using futex(2) does not need to switch from user mode to kernel mode every time it is unlocked, which is more efficient.
Windows: The underlying CRITICAL_SECTION is embedded with a small spin lock. If the lock cannot be obtained immediately, it will first It will spin for a short period of time. If it can't get it, the current thread will be suspended.

2.3.1 Use of Mutex

Pthread mutex interface function:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
    
int pthread_mutex_lock(pthread_mutex_t *mutex);
    
int pthread_mutex_unlock(pthread_mutex_t *mutex);
    
int pthread_mutex_destroy(pthread_mutex_t *mutex);

3. The difference between various locks

3.1 The difference between semaphore/mutex and spinlock

The semaphore/mutual exclusion allows the process to sleep belongs to the sleep lock, and the spin lock does not allow the caller to sleep, but let it wait in a loop, so the following differences apply:

  1. Semaphore and read-write semaphore are suitable for long hold time, they will cause the caller to sleep, so spin lock is suitable for very short hold time
  2. Spin locks can be used for interrupts, not for process context (which can cause deadlocks). The semaphore is not allowed to be used in interrupts, but can be used in process context
  3. The preemption is invalid during the spin lock retention period. When the spin lock is held, the kernel cannot be preempted, while the semaphore and read-write semaphore can be preempted during the retention period

Another thing to note is:

  1. The critical section protected by the semaphore lock can contain code that may cause blocking, while the spin lock must definitely be avoided to protect the critical section containing such code, because blocking means that the process must be switched. If the process is switched out, Another process attempts to acquire this spinlock, and a deadlock will occur.
  2. You cannot occupy the spin lock while you are occupying the semaphore, because you may sleep while waiting for the semaphore, but you are not allowed to sleep while holding the spin lock.

3.2 The difference between semaphore and mutex

Conceptual difference:
Semaphore: It is used for synchronization between processes (threads). When a process (thread) completes a certain action, it tells other processes (threads) through the semaphore, and other processes (threads) perform some Some actions. There are two-value and multi-value semaphores.
Mutex: It is used for mutual exclusion between threads. A thread occupies a shared resource, then other threads cannot access it. Until this thread leaves, other threads can start to use the shared resource. You can think of a mutex as a binary semaphore.

When locked:
Semaphore: As long as the value of the semaphore is greater than 0, other threads can sem_wait successfully. After success, the value of the semaphore is reduced by one. If the value value is not greater than 0, sem_wait blocks until the value value is increased by one after sem_post is released.
Mutex: As long as it is locked, no other thread can access the protected resource. If there is no lock, the resource is successfully obtained, otherwise it will block and wait for the resource to be available.
Use place: Semaphore is mainly used for inter-process communication, of course, it can also be used for inter-thread communication. The mutex can only be used for inter-thread communication.

Guess you like

Origin blog.csdn.net/weixin_43935927/article/details/108632803