自旋锁最多只能由一个可执行线程执有,因此其可以防止多于一个的执行线程同时进入临界区。自旋锁因为占用处理器资源,所以不应该被长时间执有。与自旋锁相比,信号量会有两次明显的上下文切换,阻塞的线程要换出与换入,因此执有自旋锁的时间最好要小于两次上下文切换耗时。
自旋锁可以用在中断处理程序中,但是信号量不可以,因为会信号量会导致中断睡眠。在中断中使用spin_lock时,一定要在获取锁之前禁止本地中断,否则中断有可能打断正持有锁的内核代码,然后去竞争这个锁而引起双重请求死锁。但是我们只需关闭当前处理器上的中断,若中断发生在不同的处理器上,即使中断在同一锁上自旋,也不会妨碍在不同处理器上的锁的持有者最终释放锁。
一个在内核态执行的路径有可能被切换出处理器,比如当前进程正在内核态执行某一系统调用时,发生了一个外部中断,当中断处理函数返回时,因为内核的可抢占性,此时将会出现一个调度点。如果处理器的运行队列中出现了一个比当前被中断进程优先级更高的进程,那么被中断的进程即使此时它正运行在内核态,将会被换出处理器。在单处理器上的这种因为内核的可抢占性所导致的两个不同进程并发执行的情形,非常类似于SMP系统上运行在不同处理器上的进程之间的并发。为了保护共享资源不会受到破坏,必须在进入临界区前,内核的可抢占性也要关闭掉。
内核提供的禁止中断,同时请求锁的接口:spin_lock_irqsave
保存当前中断状态,并禁止本地中断,然后再去获取指定的锁。
自旋锁与下半部:
Spin_lock_bh用于获取指定锁,并禁止所有下半部的执行,
若下半部与进程上下文共享数据,必须对进程上下文的共享数据进行保护。在加锁的同时还要禁止下半部的执行,因为下半部会抢占进行上下文的代码。
若中断与下半部共享数据,必须加锁的同时也要禁止中断。中断会抢占下半部。
但是下半部的tasklet中:同类tasklet的共享数据不需要保护,因为同类tasklet不可能同时运行。不用种类tasklet共享数据时,只需要加锁,不需要禁止下半部。因为同一个处理器上不会有tasklet相互抢占的现象。
软中断,无论哪种类型都必须加锁,但是不用禁止下半部。同种类型的软中断可以同时运行在一个系统的多个处理器上。统一处理器上的软中断绝不会抢占另一个软中断。
自旋锁的使用:
- spinlock_t spinlock;
- spin_lock_init(&spinlock);
- spin_lock_irqsave(&spinlock);
- 临界区
- spin_unlock_irqsave(&spinlock);
自旋锁的接口及实现:
接口在linux/spinlock.h文件中,实现在asm/spinlock文件中
Spin_lock_init:初始化一个自旋锁
- #define spin_lock_init(_lock)
- do {
- spinlock_check(_lock);
- raw_spin_lock_init(&(_lock)->rlock);
- } while (0)
在该函数中首先调用内联函数spinlock_check,当PREEMPT_RT=n时,映射spin_lock到其变体raw_spinlock。然后调用raw_spin_lock_init进行真正初始化。
spin_lock:获取锁
- static inline void spin_lock(spinlock_t *lock)
- {
- raw_spin_lock(&lock->rlock);
- }
- #define raw_spin_lock(lock) _raw_spin_lock(lock)
- void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
- {
- __raw_spin_lock(lock);
- }
__raw_spin_lock的实现是跟处理器相关的,对于ARM,其实现如下:
- static inline void __raw_spin_lock(raw_spinlock_t *lock)
- {
- preempt_disable();
- spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
- LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
- }
上面已经分析过,spin_lock不仅需要禁止本地中断,还需要禁止本地可抢占。do_raw_spin_trylock函数会调用arch_spin_trylock:
- static inline int arch_spin_trylock(arch_spinlock_t *lock)
- {
- unsigned long contended, res;
- u32 slock;
- do {
- __asm__ __volatile__(
- " ldrex %0, [%3]\n"
- " mov %2, #0\n"
- " subs %1, %0, %0, ror #16\n"
- " addeq %0, %0, %4\n"
- " strexeq %2, %0, [%3]"
- : "=&r" (slock), "=&r" (contended), "=r" (res)
- : "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
- : "cc");
- } while (res);
- if (!contended) {
- smp_mb();
- return 1;
- } else {
- return 0;
- }
- }
真正实现自旋锁的核心代码是do_raw_spin_lock函数:
- void do_raw_spin_lock(raw_spinlock_t *lock)
- {
- debug_spin_lock_before(lock);
- if (unlikely(!arch_spin_trylock(&lock->raw_lock)))
- __spin_lock_debug(lock);
- debug_spin_lock_after(lock);
- }
在__spin_lock_debug(lock)函数中,会调用真正的arch_spin_lock函数:
- static inline void arch_spin_lock(arch_spinlock_t *lock)
- {
- unsigned long tmp;
- u32 newval;
- arch_spinlock_t lockval;
- __asm__ __volatile__(
- "1: ldrex %0, [%3]\n"
- " add %1, %0, %4\n"
- " strex %2, %1, [%3]\n"
- " teq %2, #0\n"
- " bne 1b"
- : "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
- : "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
- : "cc");
- while (lockval.tickets.next != lockval.tickets.owner) {
- wfe();
- lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);
- }
- smp_mb();
- }
ldrex %0, [%3]\n相当于tmp=tmp->ran_lock,读取自旋锁V的初始状态,放在临时变量tmp中。strex%2, %1, [%3]\n表明若自旋锁处于解锁状态,说明可以进入临界区,并更新锁状态,并把更新操作执行的结果放到变量tmp中去。teq %2, #0\n用来判断操作结果,如果操作成功则代码可以进入临界区,如果不成功,代码执行bne1b指令进入忙等待。
解锁操作:
只需要更新变量,然后打开内核可抢占性。
- #define spin_lock_irqsave(lock, flags) \
- do { \
- raw_spin_lock_irqsave(spinlock_check(lock), flags); \
- } while (0)
根据是单核还是多核处理器系统中,给予不同的定义:
- #if defined(CONFIG_SMP) || defined(CONFIG_DEBUG_SPINLOCK)
- #define raw_spin_lock_irqsave(lock, flags) \
- do { \
- typecheck(unsigned long, flags); \
- flags = _raw_spin_lock_irqsave(lock); \
- } while (0)
- #else
- #define raw_spin_lock_irqsave(lock, flags) \
- do { \
- typecheck(unsigned long, flags); \
- _raw_spin_lock_irqsave(lock, flags); \
- } while (0)
- #endif
单核处理器系统可以分为内核可抢占和不可抢占两种,对于不可抢占系统并发来源主要是外部中断等异步事件。所以在这种系统中,在进入临界区只需要关闭处理器的中断即可,在离开临界区时恢复处理器中断。对于可抢占系统,并发来源除了中断与异常等异步事件外,还包括因为可抢占性导致的进程间的并发,所以在这种系统中,在进入临界区时除了要关闭处理器的中断外还需要关闭内核调度器的可抢占性。
从__raw_spin_lock_irqsave函数中可以看到,当时SMP系统时,除了要关闭本地处理器中断外,还需要关闭内核调度器的可抢占性,然后再去进行获取锁操作。
- static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock)
- {
- unsigned long flags;
- local_irq_save(flags);
- preempt_disable();
- spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
- #ifdef CONFIG_LOCKDEP
- LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
- #else
- do_raw_spin_lock_flags(lock, &flags);
- #endif
- return flags;
- }
spin_unlock_bh函数用来解决进城与中断下半部处理导致的并发中的互斥问题,该函数具有关闭软中断的能力。
- static inline void spin_unlock_bh(spinlock_t *lock)
- {
- raw_spin_unlock_bh(&lock->rlock);
- }
- static inline void __raw_spin_lock_bh(raw_spinlock_t *lock)
- {
- local_bh_disable();
- preempt_disable();
- spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
- LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
- }
本文深入解析了自旋锁的工作原理及其在操作系统内核中的应用。包括自旋锁的初始化、获取与释放过程,以及如何在不同场景下正确使用自旋锁来保护临界区资源。

862

被折叠的 条评论
为什么被折叠?



