linux 锁机制 - spinlock

  • 了解linux spinlock

1.spinlock

  在linux kernel的实现中,经常会遇到这样的场景:共享数据被中断上下文和进程上下文访问,该如何保护呢?如果只有进程上下文的访问,那么可以考虑使用semaphore或者mutex的锁机制,但是现在中断上下文也参和进来,那些可以导致睡眠的lock就不能使用了,这时候,可以考虑使用spin lock。

  spinlock又称自旋锁,是实现保护共享资源而提出一种锁机制。自旋锁与互斥锁比较类似,都是为了解决对某项资源的互斥使用。

  无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

2.spinlock原理

  跟互斥锁一样,一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁。如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。自旋锁是一种比较低级的保护数据结构或代码片段的原始方式,这种锁可能存在两个问题:

  • 死锁
  • 过多占用cpu资源

spin lock特点如下:

  • spin lock是一种死等的锁机制。当发生访问资源冲突的时候,可以有两个选择:一个是死等,一个是挂起当前进程,调度其他进程执行。spin lock是一种死等的机制,当前的执行thread会不断的重新尝试直到获取锁进入临界区。

  • 只允许一个thread进入。semaphore可以允许多个thread进入,spin lock不行,一次只能有一个thread获取锁并进入临界区,其他的thread都是在门口不断的尝试。

  • 执行时间短。由于spin lock死等这种特性,因此它使用在那些代码不是非常复杂的临界区(当然也不能太简单,否则使用原子操作或者其他适用简单场景的同步机制就OK了),如果临界区执行时间太长,那么不断在临界区门口“死等”的那些thread是多么的浪费CPU啊(当然,现代CPU的设计都会考虑同步原语的实现,例如ARM提供了WFE和SEV这样的类似指令,避免CPU进入busy loop的悲惨境地)

  • 可以在中断上下文执行。由于不睡眠,因此spin lock可以在中断上下文中适用。

3.spinlock适用情况

  自旋锁比较适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。

  信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共享资源的访问时间非常短,自旋锁也可以。但是如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP(多处理器)的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。另外格外注意一点:自旋锁不能递归使用。

4.场景分析

进程上下文

  考虑下面的场景:

  • 进程A在某个系统调用过程中访问了共享资源R;

  • 进程B在某个系统调用过程中也访问了共享资源R;

  假设在A访问共享资源R的过程中发生了中断,中断唤醒了沉睡中的,优先级更高的B,在中断返回现场的时候,发生进程切换,B启动执行,并通过系统调用访问了R,如果没有锁保护,则会出现两个thread进入临界区,导致程序执行不正确。

  加上spin lock:A在进入临界区之前获取了spin lock,同样的,在A访问共享资源R的过程中发生了中断,中断唤醒了沉睡中的,优先级更高的B,B在访问临界区之前仍然会试图获取spin lock,这时候由于A进程持有spin lock而导致B进程进入了永久的spin……怎么解决? 在A进程获取spin lock的时候,禁止本CPU上的抢占。

Note:
  上面的永久spin的场合仅仅在本CPU的进程抢占本CPU的当前进程这样的场景中发生。如果A和B运行在不同的CPU上,那么会简单一些:A进程虽然持有spin lock,而导致B进程进入spin状态,不过由于运行在不同的CPU上,A进程会持续执行并会很快释放spin lock,解除B进程的spin状态。

中断上下文

  • 运行在CPU0上的进程A在某个系统调用过程中访问了共享资源R

  • 运行在CPU1上的进程B在某个系统调用过程中也访问了共享资源R

  • 外设P的中断handler中也会访问共享资源R

  在这样的场景下,使用spin lock可以保护访问共享资源R的临界区吗?

  假设CPU0上的进程A持有spin lock进入临界区,这时候,外设P发生了中断事件,并且调度到了CPU1上执行,看起来没有什么问题,执行在CPU1上的handler会稍微等待一会CPU0上的进程A,等它离开临界区就会释放spin lock的;但是,如果外设P的中断事件被调度到了CPU0上执行会怎么样?CPU0上的进程A在持有spin lock的状态下被中断上下文抢占,而抢占它的CPU0上的handler在进入临界区之前仍然会试图获取spin lock,悲剧发生了,CPU0上的P外设的中断handler永远的进入spin状态,这时候,CPU1上的进程B也不可避免在试图持有spin lock的时候失败而导致进入spin状态。

  为了解决这样的问题,linux kernel采用:如果涉及到中断上下文的访问,spin lock需要和禁止本CPU上的中断联合使用

  kernel中提供bottom half机制,虽然同属中断上下文,不过还是稍有不同。可以把上面的场景简单修改一下:外设P不是中断handler中访问共享资源R,而是在的bottom half中访问。使用spin lock+禁止本地中断当然是可以达到保护共享资源的效果,但是只需要disable bottom half就可以了。

  最后,讨论中断上下文之间的竞争。同一种中断handler之间在uni core和multi core上都不会并行执行,这是linux kernel的特性。如果不同中断handler需要使用spin lock保护共享资源,对于新的内核(不区分fast handler和slow handler),所有handler都是关闭中断的,因此使用spin lock不需要关闭中断的配合。bottom half又分成softirq和tasklet,同一种softirq会在不同的CPU上并发执行,因此如果某个驱动中的sofirq的handler中会访问某个全局变量,对该全局变量是需要使用spin lock保护的,不用配合disable CPU中断或者bottom half。tasklet更简单,因为同一种tasklet不会多个CPU上并发,。

5.spinlock的定义以及相应的API

5.1.struct spinlock

include/linux/spinlock_types.h:
   61 typedef struct spinlock {
   62     union {
   63         struct raw_spinlock rlock;                                                                     
   72     };
   73 } spinlock_t;

   20 typedef struct raw_spinlock {
   21     arch_spinlock_t raw_lock;                                                                          
   29 } raw_spinlock_t;

arch/arm/include/asm/spinlock_types.h:
   11 typedef struct {
   12     union {
   13         u32 slock;
   24 } arch_spinlock_t;  //spin lock是和architecture相关的,arch_spinlock是architecture相关的实现

  首先定义一个spinlock_t的数据类型,其本质上是一个整数值(对该数值的操作需要保证原子性),该数值表示spin lock是否可用。初始化的时候被设定为1。当thread想要持有锁的时候调用spin_lock函数,该函数将spin lock那个整数值减去1,然后进行判断,如果等于0,表示可以获取spin lock,如果是负数,则说明其他thread的持有该锁,本thread需要spin。

5.2.API
在这里插入图片描述
5.3.自旋锁定义

动态的:
spinlock_t lock;
spin_lock_init (&lock);

静态的:
DEFINE_SPINLOCK(lock);

5.4.spin_lock

该函数实现:

  • 1.只禁止内核抢占,不会关闭本地中断;

  • 2.为何需要关闭内核抢占:假如进程A获得spin_lock->进程B抢占进程A->进程B尝试获取spin_lock->由于进程B优先级比进程A高,先于A运行,而进程B又需要A unlock才得以运行,这样死锁。所以这里需要关闭抢占。 这个原理RTOS的

    • a. 因为ThreadX的semaphore,假如进程B获取sema失败,会一直等待,直到A进程释放,不会死锁。
    • b. Mutex: mutex获取一旦失败,进程会进入sleep,直到其他进程释放;而spin_lock则不同,会一直轮训访问,且直到时间片耗完。

函数调用流程:

spin_lock()
	\->raw_spin_lock()
		\->__raw_spin_lock
			{
				preempt_disable(); //关闭内核抢占
				spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); //获取锁
				LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);//上锁
			}
			-----------------------------  arch
			-> arch_spin_lock()
			-> arch_spin_trylock()
  • 调用preempt_disable()来关闭调度。也就是说,运行在一个CPU上的代码使用spin_lock()试图加锁之后,基于该CPU的线程调度和抢占就被禁止了,这也体现了spinlock作为"busy loop"形式的锁的语义。
  • do_raw_spin_lock():和架构相关的arch_spin_lock()。
static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock) 
{ 
    __acquire(lock); 
    arch_spin_lock(&lock->raw_lock); 
}

static inline void arch_spin_lock(arch_spinlock_t *lock) 
{ 
    unsigned long tmp; 
    u32 newval; 
    arch_spinlock_t lockval;

    prefetchw(&lock->slock);------------------------(1) 
    __asm__ __volatile__( 
"1:    ldrex    %0, [%3]\n"-------------------------(2) 
"    add    %1, %0, %4\n" 
"    strex    %2, %1, [%3]\n"------------------------(3) 
"    teq    %2, #0\n"----------------------------(4) 
"    bne    1b" 
    : "=&r" (lockval), "=&r" (newval), "=&r" (tmp) 
    : "r" (&lock->slock), "I" (1 << TICKET_SHIFT) 
    : "cc");

    while (lockval.tickets.next != lockval.tickets.owner) {------------(5) 
        wfe();-------------------------------(6) 
        lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);------(7) 
    }

    smp_mb();------------------------------(8) 
}
  • (1)和preloading cache相关的操作,主要是为了性能考虑

  • (2)将slock的值保存在lockval这个临时变量中

  • (3)将spin lock中的next加一

  • (4)判断是否有其他的thread插入。

  • (5)判断当前spin lock的状态,如果是unlocked,那么直接获取到该锁

  • (6)如果当前spin lock的状态是locked,那么调用wfe进入等待状态。

  • (7)其他的CPU唤醒了本cpu的执行,说明owner发生了变化,该新的own赋给lockval,然后继续判断spin lock的状态,也就是回到step 5。

  • (8)memory barrier的操作。

  假设一个CPU上的线程T持有了一个spinlock,发生中断后,该CPU转而执行对应的hardirq。如果该hardirq也试图去持有这个spinlock,那么将无法获取成功,导致hardirq无法退出。在hardirq主动退出之前,线程T是无法继续执行以释放spinlock的,最终将导致该CPU上的代码不能继续向前运行,形成死锁(dead lock)。
在这里插入图片描述
  为了防止这种情况的发生,我们需要使用spin_lock_irq()函数,一个spin_lock()和local_irq_disable()的结合体,它可以在spinlock加锁的同时关闭中断。

4.5.spin_lock_irq

1 static inline void __raw_spin_lock_irq(raw_spinlock_t *lock)  
2 {  
3         local_irq_disable();  
4         preempt_disable();  
5         spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);  
6         LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);  
7 }  
  • 1.禁止内核抢占,且关闭本地中断
  • 2.那么在spin_lock中关闭了内核抢占,不关闭中断会出现什么情况呢?假如中断中也想获得这个锁,会出现和spin_lock中举得例子相同。所以这个时候,在进程A获取lock之后,使用spin_lock_irq将中断禁止,就不会出现死锁的情况。
  • 3.在任何情况下使用spin_lock_irq都是安全的。因为它既禁止本地中断,又禁止内核抢占。
  • 4.spin_lock比spin_lock_irq速度快,但是它并不是任何情况下都是安全的。

4.6.spin_lock_irqsave

 1 static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock)
 2 {
 3     unsigned long flags;
 4 
 5     local_irq_save(flags);
 6     preempt_disable();
 7     spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
 8     /*
 9      * On lockdep we dont want the hand-coded irq-enable of
10      * do_raw_spin_lock_flags() code, because lockdep assumes
11      * that interrupts are not re-enabled during lock-acquire:
12      */
13 #ifdef CONFIG_LOCKDEP
14     LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
15 #else
16     do_raw_spin_lock_flags(lock, &flags);
17 #endif
18     return flags;
19 }
  • 1.禁止内核抢占,关闭中断,保存中断状态寄存器的标志位;

  • 2.spin_lock_irqsave在锁返回时,之前开的中断,之后也是开的;之前关,之后也是关。但是spin_lock_irq则不管之前的开还是关,返回时都是开的。

  • 3.spin_lock_irq在自旋的时候,不会保存当前的中断标志寄存器,只会在自旋结束后,将之前的中断打开。

  然而local_irq_save()只能对本地CPU执行关中断操作,所以即便使用了spin_lock_irqsave(),如果其他CPU上发生了中断,那么这些CPU上hardirq,也有可能试图去获取一个被本地CPU上运行的线程T占有的spinlock。不过没有关系,因为此时hardirq和线程T运行在不同的CPU上,等到线程T继续运行释放了这个spinlock,hardirq就有机会获取到,不至于造成死锁。

6.Wait for Event mechanism

  A PE can use the Wait for Event (WFE) mechanism to enter a low-power state, depending on the value of an Event Register for that PE. To enter the low-power state, the PE executes a Wait For Event instruction, WFE, and if the Event Register is clear, the PE can enter the low-power state.

  If the PE does enter the low-power state, it remains in that low-power state until it receives a WFE wake-up event.

  The architecture does not define the exact nature of the low-power state, except that the execution of a WFE instruction must not cause a loss of memory coherency.

Spinlock as an example of using Wait For Event and Send Event

  A multiprocessor operating system requires locking mechanisms to protect data structures from being accessed simultaneously by multiple PEs. These mechanisms prevent the data structures becoming inconsistent or corrupted if different PEs try to make conflicting changes. If a lock is busy, because a data structure is being used by one PE, it might not be practical for another PE to do anything except wait for the lock to be released. For example, if a PE is handling an interrupt from a device, it might need to add data received from the device to a queue. If another PE is removing data from the same queue, it will have locked the memory area that holds the queue. The first PE cannot add the new data until the queue is in a consistent state and the second PE has released the lock. The first PE cannot return from the interrupt handler until the data has been added to the queue, so it must wait.

  Typically, a spin-lock mechanism is used in these circumstances:

  • A PE requiring access to the protected data attempts to obtain the lock using single-copy atomic synchronization primitives such as the Load-Exclusive and Store-Exclusive operations.
  • If the PE obtains the lock it performs its memory operation and then releases the lock.
  • If the PE cannot obtain the lock, it reads the lock value repeatedly in a tight loop until the lock becomes available. When the lock becomes available, the PE again attempts to obtain it.

  A spin-lock mechanism is not ideal for all situations:

  • In a low-power system the tight read loop is undesirable because it uses energy to no effect.
  • In a multi-PE implementation the execution of spin-locks by multiple waiting PEs can degrade overall performance.

Using the Wait For Event and Send Event mechanism can improve the energy efficiency of a spinlock:

  • A PE that fails to obtain a lock executes a WFE instruction to request entry to a low-power state, at the time when the exclusive monitor is set holding the address of the location holding the lock.
  • When a PE releases a lock, the write to the lock location causes the exclusive monitor of any PE monitoring the lock location to be cleared. This clearing of the exclusive monitors generates a WFE wake-up event for each of those PEs. Then, these PEs can attempt to obtain the lock again.

refer to

  • https://www.cnblogs.com/aaronLinux/p/5890924.html
  • https://zhuanlan.zhihu.com/p/90634198
  • https://blog.csdn.net/changexhao/article/details/80666823
发布了161 篇原创文章 · 获赞 15 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_41028621/article/details/105023943