自旋锁
上回,我们说到为了避免并发,防止竞争,内核提供了一些方法来实现对内核共享数据的保护。如果临界区只是一个变量,那么使用原子操作即可,但实际上临界区大多是一些数据操作的集合,这时候使用原子操作不太合理,我们就要用到锁机制。Linux内核中常见的锁机制有忙等待和睡眠锁两种,锁代表分别为自旋锁和信号量,本文主要介绍自旋锁。
自旋锁同一时刻只能被一个内核任务持有,一个内核任务试图获得一个自旋锁,如果该锁没有被其它内核任务争用,那么它就可以马上获得这个自旋锁,如果该锁被其他内核任务争用,那么它就一直在忙等待(自旋等待,占用CPU时间),直到另一个内核任务释放该自旋锁。

自旋锁常用于多处理器保护临界区资源,在内核中主要用于中断处理,它适用于短期轻量级锁定,若是长时间锁定最好用信号量。
自旋锁的特性:
- 忙等待
- 同一时刻一个任务一把锁
- 尽快完成临界区任务
- 自旋锁可在中断上下文中使用
自旋锁的定义:
自旋锁定义的数据结构考虑了不同处理器体系结构的支持和实时性内核的要求,定义了 arch_spinlock_t 数据结构和 raw_spinlock 数据结构。在4.19内核中,自旋锁 spinlock 数据结构定义在
include\linux\spinlock_types.h
中,spinlock 结构定义如下:
typedef struct spinlock {
union {
struct raw_spinlock rlock;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
struct {
u8 __padding[LOCK_PADSIZE];
struct lockdep_map dep_map;
};
#endif
};
} spinlock_t;
可以看到 spinlock 对 raw_spinlock 做了封装,再看raw_spinlock 结构:
typedef struct raw_spinlock {
arch_spinlock_t raw_lock;
#ifdef CONFIG_DEBUG_SPINLOCK
unsigned int magic, owner_cpu;
void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
} raw_spinlock_t;
可以看到 raw_spinlock 对 arch_spinlock_t 做了封装,再看 arch_spinlock_t 结构,在4.19内核中,arch_spinlock_t 结构定义在
include\linux\spinlock_types_up.h
中,arch_spinlock_t 结构定义如下:
#ifdef CONFIG_DEBUG_SPINLOCK
typedef struct {
volatile unsigned int slock;
} arch_spinlock_t;
#define __ARCH_SPIN_LOCK_UNLOCKED { 1 }
#else
typedef struct { } arch_spinlock_t;
#define __ARCH_SPIN_LOCK_UNLOCKED { }
#endif
typedef struct {
/* no debug version on UP */
} arch_rwlock_t;
对于以上结构:
- 不同的处理器架构需要自定义数据类型 arch_spinlock_t ;
- 尽量使用 spinlock ;
- 绝对不允许被抢占和睡眠的地方,使用raw_spinlock,否则使用spinlock ;
- 如果临界区足够小,使用raw_spinlock。
自旋锁的使用:
简单的使用举例
DEFINE_SPINLOCK(my_lock); //定义一个自旋锁
spin_lock(&my_lock); //加锁
/*
/*临界区*/
*/
spin_unlock(&my_locl); //解锁
自旋锁变种
请大家先设想这样一种情况,我们在内核中操作一个链表,此时操作链表的地方就是一个临界区,为了防止多处理器竞争,我们使用自旋锁来进行保护。当处于临界区时,突然发生了外部硬件中断,此时系统暂停当前进程的执行,转去处理该中断。没想到的是,这个外部中断也要操作这个链表,它首先要保护这个临界区,就去申请自旋锁了。可是,这个自旋锁已经被别人持有了,中断处理函数只能在那里忙等待,而自旋锁又被中断打断而不能尽快释放锁,这时候就发生了死锁。
这时候就出现了Linux内核自旋锁的变种spin_lock_irq() ,该函数在获取自旋锁时关闭本地CPU中断,防止本地中断处理程序和自旋锁持有者之间争用锁,其定义在4.19版内核
include\linux\spinlock.h
中,具体定义如下:
static __always_inline void spin_lock_irq(spinlock_t *lock)
{
raw_spin_lock_irq(&lock->rlock);
}
raw_spin_lock_irq() 函数定义如下:
_raw_spin_lock_irq() 函数定义在4.19内核
include\linux\spinlock_api_smp.h
中:
#ifdef CONFIG_INLINE_SPIN_LOCK_IRQ
#define _raw_spin_lock_irq(lock) __raw_spin_lock_irq(lock)
#endif
__raw_spin_lock_irq() 函数定义如下:
static inline void __raw_spin_lock_irq(raw_spinlock_t *lock)
{
local_irq_disable();
preempt_disable();
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}
spin_lock_irq() 函数比 spin_lock() 函数,多了一个 local_irq_disable() 函数,作用就是关闭本地处理器中断,这样就可以保证在获取到自旋锁时不会发生中断,从而避免发生死锁。
使用自旋锁的重要原则是:拥有自旋锁的临界区代码必须是原子执行,不能休眠或主动调度。
常见自旋锁操作函数如下:
函数 | 作用 |
---|---|
DEFINE_SPINLOCK(lock) | 定义并且初始化静态自旋锁 |
spin_lock_init(spinlock_t *lock) | 在运行时动态初始化自旋锁 |
spin_lock(spinlock_t *lock) | 申请自旋锁,如果锁被其他处理器占有,当前处理器自旋等待 |
spin_unlock(spinlock_t *lock) | 释放自旋锁 |
spin_lock_bh(spinlock_t *lock) | 申请自旋锁,并且禁止当前处理器的软中断 |
spin_unlock_bh(spinlock_t *lock) | 释放自旋锁,并且开启当前处理器的软中断 |
spin_lock_irq(spinlock_t *lock) | 申请自旋锁,并且禁止当前处理器的硬中断 |
spin_unlock_irq(spinlock_t *lock) | 释放自旋锁,并且开启当前处理器的硬中断 |
spin_lock_irqsave(spinlock_t *lock,unsigned long flags) | 申请自旋锁,保存当前处理器的硬中断状态,并且禁止当前处理器的硬中断 |
spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) | 释放自旋锁,并且恢复当前处理器的硬中断状态 |
spin_trylock(spinlock_t *lock) | 申请自旋锁,如果申请成功,返回1;如果锁被其他处理器占有,当前处理器不等待,立即返回0 |