spin lock
时间:2010-07-28 来源:438031693
最近在调试一个PCI设备驱动,发现在读写设备的时候,周期性的,会出现短时非常占用CPU的情况,持续6到10秒钟,周期是1到2分钟,重启以后,周期就会变化。经过排查,没有发现哪个while循环或for循环异常。但是用到了很多的spin_lock_irqsave spin_unlock_irqrestore,怀疑是自旋锁出现的忙等待,但是CPU是单核的,下面的文章中提到,只有多核才会出现冲突,这个有待考证。
—————————————————————————————————————————————————
linux内核中最常见的锁是自旋锁(spin lock)。自旋锁最多只能被一个可执行线程持有。如果一个执行线程试图获得一个被争用的自旋锁,那么该线程就会一直进行忙循环等待锁重新可用。要是锁未被争用,请求锁的执行线程便能立刻得到它,继续执行。在任意时间,自旋锁都可以防止多于一个的执行线程同时进入临界区。
一个被正用的自旋锁使得请求它的线程在等待锁重新可用时自旋(特别浪费处理器时间),这种行为是自旋锁的特点。自旋锁不应该被长时间持有。持有自旋锁的时间最好小于完成两次上下文切换的的时间。
自旋锁的实现和体系结构密切相关,代码往往使用汇编实现。
因为自旋锁在同一时刻至多被一个执行线程持有,所以一个时刻只能有一个线程位于临界区内,这就为多处理器提供了防止并发访问所需要的包含机制。在单处理器机器上,编译的时候并不会加入自旋锁。
自旋锁不能递归!
自旋锁可以使用在中断处理程序中(此处不能使用信号量,因为它会导致睡眠)。在中断处理程序中使用自旋锁时,一定要在获取锁之前,首先禁本地中断(在当前处理器上的中断请求),否则,中断处理程序就会打断正持有锁的内核代码,有可能会试图去争用这个已经被持有的自旋锁。注意,需要关闭的只是当前处理器上的中断,因为如果中断发生在不同的处理器上,即使中断处理程序在同一锁上的自旋,也不会妨碍锁的持有者最终释放锁。
下面是相关文件如下:
- /*
- * include/linux/spinlock.h - generic spinlock/rwlock declarations
- *
- * here's the role of the various spinlock/rwlock related include files:
- *
- * on SMP builds:
- *
- * asm/spinlock_types.h: contains the raw_spinlock_t/raw_rwlock_t and the
- * initializers
- *
- * linux/spinlock_types.h:
- * defines the generic type and initializers
- *
- * asm/spinlock.h: contains the __raw_spin_*()/etc. lowlevel
- * implementations, mostly inline assembly code
- *
- * (also included on UP-debug builds:)
- *
- * linux/spinlock_api_smp.h:
- * contains the prototypes for the _spin_*() APIs.
- *
- * linux/spinlock.h: builds the final spin_*() APIs.
- *
- * on UP builds:
- *
- * linux/spinlock_type_up.h:
- * contains the generic, simplified UP spinlock type.
- * (which is an empty structure on non-debug builds)
- *
- * linux/spinlock_types.h:
- * defines the generic type and initializers
- *
- * linux/spinlock_up.h:
- * contains the __raw_spin_*()/etc. version of UP
- * builds. (which are NOPs on non-debug, non-preempt
- * builds)
- *
- * (included on UP-non-debug builds:)
- *
- * linux/spinlock_api_up.h:
- * builds the _spin_*() APIs.
- *
- * linux/spinlock.h: builds the final spin_*() APIs.
- */
内核提供的禁止中断同时请求锁的接口:
- 在<Spinlock.h(include\linux)>中
- #if defined(CONFIG_SMP) || defined(CONFIG_DEBUG_SPINLOCK)
- #define spin_lock_irqsave(lock, flags) flags = _spin_lock_irqsave(lock)
- #define read_lock_irqsave(lock, flags) flags = _read_lock_irqsave(lock)
- #define write_lock_irqsave(lock, flags) flags = _write_lock_irqsave(lock)
- #ifdef CONFIG_DEBUG_LOCK_ALLOC
- #define spin_lock_irqsave_nested(lock, flags, subclass) \
- flags = _spin_lock_irqsave_nested(lock, subclass)
- #else
- #define spin_lock_irqsave_nested(lock, flags, subclass) \
- flags = _spin_lock_irqsave(lock)
- #endif
- #else
- #define spin_lock_irqsave(lock, flags) _spin_lock_irqsave(spinlock_t * lock)(lock, flags)
- #define read_lock_irqsave(lock, flags) _read_lock_irqsave(lock, flags)
- #define write_lock_irqsave(lock, flags) _write_lock_irqsave(lock, flags)
- #define spin_lock_irqsave_nested(lock, flags, subclass) \
- spin_lock_irqsave(lock, flags)
- #endif
- #define spin_unlock_irqrestore(lock, flags) \
- _spin_unlock_irqrestore(lock, flags)
函数spin_lock_irqsave()保存中断的当然状态,并禁止本地中断,然后再去获取指定的锁。spin_lock_irqsave()对指定的锁解锁,然后让中断恢复到加锁前的状态。所以即使中断最初是被禁止的,你的代码也不会错误地激活它们,相反,会继续让它们禁止。flag变量应该由数值传递,因为锁函数有些是通过宏的方式实现的。
加锁和解锁分别可以禁止和允许内核抢占。
如果能确定中断在加锁前是激活的,就不需要在解锁后恢复中断之前的状态了。这时可以使用spin_lock_irq()和spin_unlock_irq()。
- #define spin_lock_irq(lock) _spin_lock_irq(lock)
- #if defined(CONFIG_DEBUG_SPINLOCK) || defined(CONFIG_PREEMPT) || \
- !defined(CONFIG_SMP)
- # define read_unlock_irq(lock) _read_unlock_irq(lock)
- #else
- # define spin_unlock_irq(lock) \
- do { \
- __raw_spin_unlock(&(lock)->raw_lock); \
- __release(lock); \
- local_irq_enable(); \
- } while (0)
由于内核庞大而复杂,所以在内核的执行路线上,很难搞清楚中断在当前调用点上是否处于激活状态,故一般不提倡使用spin_lock_irq()方法。
针对自旋锁的操作:
spin_lock_init()用来初始化动态创建的自旋锁。
- #ifdef CONFIG_DEBUG_SPINLOCK
- extern void __spin_lock_init(spinlock_t *lock, const char *name,
- struct lock_class_key *key);
- # define spin_lock_init(lock) \
- do { \
- static struct lock_class_key __key; \
- \
- __spin_lock_init((lock), #lock, &__key); \
- } while (0)
- #else
- # define spin_lock_init(lock) \
- do { *(lock) = SPIN_LOCK_UNLOCKED; } while (0)
- #endif
- #ifdef CONFIG_DEBUG_SPINLOCK
- extern void __rwlock_init(rwlock_t *lock, const char *name,
- struct lock_class_key *key);
- # define rwlock_init(lock) \
- do { \
- static struct lock_class_key __key; \
- \
- __rwlock_init((lock), #lock, &__key); \
- } while (0)
- #else
- # define rwlock_init(lock) \
- do { *(lock) = RW_LOCK_UNLOCKED; } while (0)
- #endif
- #define spin_is_locked(lock) __raw_spin_is_locked(&(lock)->raw_lock)
- #define spin_trylock(lock) __cond_lock(lock, _spin_trylock(lock))
spin_trylock()试图获得某个特定的自旋锁,如果该锁已经被争用,那么立刻返回非0值,而不会自旋等待锁被释放;如果获得这个自旋锁,返回0。
spin_is_locked()用于检查特定的锁当前是否已被占用,如被占用返回非0,否则返回0。
- #define spin_lock(lock) _spin_lock(lock)
- /*
- * We inline the unlock functions in the nondebug case:
- */
- #if defined(CONFIG_DEBUG_SPINLOCK) || defined(CONFIG_PREEMPT) || \
- !defined(CONFIG_SMP)
- # define spin_unlock(lock) _spin_unlock(lock)
- #else
- # define spin_unlock(lock) \
- do {__raw_spin_unlock(&(lock)->raw_lock); __release(lock); } while (0)
自旋锁和下半部
函数spin_lock_bh()用于获取指定锁,同时它会禁止所有下半部的执行。spin_unlock_bh()函数执行相反的操作。
- #define spin_lock_bh(lock) _spin_lock_bh(lock)
- #define spin_unlock_bh(lock) _spin_unlock_bh(lock)
- void __lockfunc _spin_lock_bh(spinlock_t *lock)
- {
- local_bh_disable();
- preempt_disable();
- spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
- _raw_spin_lock(lock);
- }
- void __lockfunc _spin_unlock_bh(spinlock_t *lock)
- {
- spin_release(&lock->dep_map, 1, _RET_IP_);
- _raw_spin_unlock(lock);
- preempt_enable_no_resched();
- local_bh_enable_ip((unsigned long)__builtin_return_address(0));
- }
由于下半部可以抢占进程上下文中的代码,所以当下半部和进程上下文共享数据时,必须对进程上下文中的共享数据进行包含,所以需要加锁的同时还要禁止下半部执行。同样,由于中断处理程序可以抢占下半部,所以如果中断处理程序和下半部共享数据,那么就必须在获取恰当的锁的同时还有禁止中断。
————————————————————————————————————————————————
自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。
信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用(_trylock的变种能够在中断上下文使用),而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。
如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共巷资源的访问时间非常短,自旋锁也可以。但是如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。
自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。
跟互斥锁一样,一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁。如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。
无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。
自旋锁的API有:
de>spin_lock_init(x)de>
该宏用于初始化自旋锁x。自旋锁在真正使用前必须先初始化。该宏用于动态初始化。
de>DEFINE_SPINLOCK(x)de>
该宏声明一个自旋锁x并初始化它。该宏在2.6.11中第一次被定义,在先前的内核中并没有该宏。
de>SPIN_LOCK_UNLOCKEDde>
该宏用于静态初始化一个自旋锁。
de>DEFINE_SPINLOCK(x)等同于spinlock_t x = SPIN_LOCK_UNLOCKEDspin_is_locked(x)de>
该宏用于判断自旋锁x是否已经被某执行单元保持(即被锁),如果是,返回真,否则返回假。
de>spin_unlock_wait(x)de>
该宏用于等待自旋锁x变得没有被任何执行单元保持,如果没有任何执行单元保持该自旋锁,该宏立即返回,否则将循环在那里,直到该自旋锁被保持者释放。
de>spin_trylock(lock)de>
该宏尽力获得自旋锁lock,如果能立即获得锁,它获得锁并返回真,否则不能立即获得锁,立即返回假。它不会自旋等待lock被释放。
de>spin_lock(lock)de>
该宏用于获得自旋锁lock,如果能够立即获得锁,它就马上返回,否则,它将自旋在那里,直到该自旋锁的保持者释放,这时,它获得锁并返回。总之,只有它获得锁才返回。
de>spin_lock_irqsave(lock, flags)de>
该宏获得自旋锁的同时把标志寄存器的值保存到变量flags中并失效本地中断。
de>spin_lock_irq(lock)de>
该宏类似于spin_lock_irqsave,只是该宏不保存标志寄存器的值。
de>spin_lock_bh(lock)de>
该宏在得到自旋锁的同时失效本地软中断。
de>spin_unlock(lock)de>
该宏释放自旋锁lock,它与spin_trylock或spin_lock配对使用。如果spin_trylock返回假,表明没有获得自旋锁,因此不必使用spin_unlock释放。
de>spin_unlock_irqrestore(lock, flags)de>
该宏释放自旋锁lock的同时,也恢复标志寄存器的值为变量flags保存的值。它与spin_lock_irqsave配对使用。
de>spin_unlock_irq(lock)de>
该宏释放自旋锁lock的同时,也使能本地中断。它与spin_lock_irq配对应用。
de>spin_unlock_bh(lock)de>
该宏释放自旋锁lock的同时,也使能本地的软中断。它与spin_lock_bh配对使用。
de>spin_trylock_irqsave(lock, flags)de>
该宏如果获得自旋锁lock,它也将保存标志寄存器的值到变量flags中,并且失效本地中断,如果没有获得锁,它什么也不做。
因此如果能够立即获得锁,它等同于spin_lock_irqsave,如果不能获得锁,它等同于spin_trylock。如果该宏获得自旋锁lock,那需要使用spin_unlock_irqrestore来释放。
de>spin_trylock_irq(lock)de>
该宏类似于spin_trylock_irqsave,只是该宏不保存标志寄存器。如果该宏获得自旋锁lock,需要使用spin_unlock_irq来释放。
de>spin_trylock_bh(lock)de>
该宏如果获得了自旋锁,它也将失效本地软中断。如果得不到锁,它什么也不做。因此,如果得到了锁,它等同于spin_lock_bh,如果得不到锁,它等同于spin_trylock。如果该宏得到了自旋锁,需要使用spin_unlock_bh来释放。
de>spin_can_lock(lock)de>
该宏用于判断自旋锁lock是否能够被锁,它实际是spin_is_locked取反。如果lock没有被锁,它返回真,否则,返回假。该宏在2.6.11中第一次被定义,在先前的内核中并没有该宏。
获得自旋锁和释放自旋锁有好几个版本,因此让读者知道在什么样的情况下使用什么版本的获得和释放锁的宏是非常必要的。
如果被保护的共享资源只在进程上下文访问和软中断上下文访问,那么当在进程上下文访问共享资源时,可能被软中断打断,从而可能进入软中断上下文来对被保护的共享资源访问,因此对于这种情况,对共享资源的访问必须使用spin_lock_bh和spin_unlock_bh来保护。
当然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqrestore也可以,它们失效了本地硬中断,失效硬中断隐式地也失效了软中断。但是使用spin_lock_bh和spin_unlock_bh是最恰当的,它比其他两个快。
如果被保护的共享资源只在进程上下文和tasklet或timer上下文访问,那么应该使用与上面情况相同的获得和释放锁的宏,因为tasklet和timer是用软中断实现的。
如果被保护的共享资源只在一个tasklet或timer上下文访问,那么不需要任何自旋锁保护,因为同一个tasklet或timer只能在一个CPU上运行,即使是在SMP环境下也是如此。实际上tasklet在调用tasklet_schedule标记其需要被调度时已经把该tasklet绑定到当前CPU,因此同一个tasklet决不可能同时在其他CPU上运行。
timer也是在其被使用add_timer添加到timer队列中时已经被帮定到当前CPU,所以同一个timer绝不可能运行在其他CPU上。当然同一个tasklet有两个实例同时运行在同一个CPU就更不可能了。
如果被保护的共享资源只在两个或多个tasklet或timer上下文访问,那么对共享资源的访问仅需要用spin_lock和spin_unlock来保护,不必使用_bh版本,因为当tasklet或timer运行时,不可能有其他tasklet或timer在当前CPU上运行。
如果被保护的共享资源只在一个软中断(tasklet和timer除外)上下文访问,那么这个共享资源需要用spin_lock和spin_unlock来保护,因为同样的软中断可以同时在不同的CPU上运行。
如果被保护的共享资源在两个或多个软中断上下文访问,那么这个共享资源当然更需要用spin_lock和spin_unlock来保护,不同的软中断能够同时在不同的CPU上运行。
如果被保护的共享资源在软中断(包括tasklet和timer)或进程上下文和硬中断上下文访问,那么在软中断或进程上下文访问期间,可能被硬中断打断,从而进入硬中断上下文对共享资源进行访问,因此,在进程或软中断上下文需要使用spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。
而在中断处理句柄中使用什么版本,需依情况而定,如果只有一个中断处理句柄访问该共享资源,那么在中断处理句柄中仅需要spin_lock和spin_unlock来保护对共享资源的访问就可以了。
因为在执行中断处理句柄期间,不可能被同一CPU上的软中断或进程打断。但是如果有不同的中断处理句柄访问该共享资源,那么需要在中断处理句柄中使用spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。
在使用spin_lock_irq和spin_unlock_irq的情况下,完全可以用spin_lock_irqsave和spin_unlock_irqrestore取代,那具体应该使用哪一个也需要依情况而定,如果可以确信在对共享资源访问前中断是使能的,那么使用spin_lock_irq更好一些。
因为它比spin_lock_irqsave要快一些,但是如果你不能确定是否中断使能,那么使用spin_lock_irqsave和spin_unlock_irqrestore更好,因为它将恢复访问共享资源前的中断标志而不是直接使能中断。
当然,有些情况下需要在访问共享资源时必须中断失效,而访问完后必须中断使能,这样的情形使用spin_lock_irq和spin_unlock_irq最好。
需要特别提醒读者,spin_lock用于阻止在不同CPU上的执行单元对共享资源的同时访问以及不同进程上下文互相抢占导致的对共享资源的非同步访问,而中断失效和软中断失效却是为了阻止在同一CPU上软中断或中断对共享资源的非同步访问。
参考资料
Kernel Locking Techniques,http://www.linuxjournal.com/article/5833
Redhat 9.0 kernel source tree
kernel.org 2.6.12 source tree
Linux 2.6内核中新的锁机制--RCU(Read-Copy Update),
http://www.ibm.com/developerworks/cn/linux/l-rcu/
Unreliable Guide To Locking.