自旋锁
内核当发生访问资源冲突的时候,可以有两种锁的解决方案选择:
- 一个是原地等待
- 一个是挂起当前进程,调度其他进程执行(睡眠)
Spinlock 是内核中提供的一种比较常见的锁机制,自旋锁是“原地等待”的方式解决资源冲突的,即,一个线程/进程获取了一个自旋锁后,另外一个线程/进程期望获取该自旋锁,获取不到,只能够原地“打转”(忙等待)。由于自旋锁的这个忙等待的特性,注定了它使用场景上的限制 —— 自旋锁不应该被长时间的持有(消耗 CPU 资源)。
但是Linux中,普通的spinlock由于不带额外的语义,用起来反而要非常小心,尤其不要误解了忙等待的含义。
在Linux kernel中执行的代码大体分normal和interrupt context两种。tasklet/softirq可以归为normal因为他们可以进入等待;nested interrupt是interrupt context的一种特殊情况,当然也是interrupt context。Normal级别可以被interrupt抢断,interrupt会被另一个interrupt抢断,但不会被normal中断。各个 interrupt之间没有优先级关系,只要有可能,每个interrupt都会被其他interrupt中断。
搞清楚优先级后,我们就需要明确忙等待的含义了,忙等待是指当前线程/进程在获取不到锁后不会主动放弃cpu,也就是我不给,但是由于线程/进程之间存在调度优先级,其他高优先级的线程/进程是可以抢占cpu的,所以不是说忙等待不是一直都能占用cpu的。
那这种情况会带来什么样的问题呢?
看下面这段伪代码,先考虑只有单cpu的情况:
extern spinlock_t lock;
spin_lock(&lock);
// run...
spin_unlock(&lock);
他能正常工作吗?答案是有可能。在某些情况下,这段代码可以正常工作,但如果发生如下调用关系:
// in normal run level
extern spinlock_t lock;
spin_lock(&lock);
// run...
-->>// interrupted by IRQ ...
// in IRQ
spin_lock(&lock);
// run...
spin_unlock(&lock);
<<--
spin_unlock(&lock);
喔,我们在normal级别下获得了一个spinlock,正当我们想做什么的时候,我们被interrupt打断了,CPU转而执行interrupt level的代码,它也想获得这个lock,于是“死锁”发生了!解决方法很简单,看看我们第二次尝试:
extern spinlock_t lock;
cli; // disable interrupt on current CPU
spin_lock(&lock);
// run... -->> //IRQ off
spin_unlock(&lock);
sti; // enable interrupt on current CPU
在获得spinlock之前,我们先把当前CPU的中断禁止掉,然后获得一个lock;在释放lock之后再把中断打开。这样,我们就防止了死锁。事实上,Linux提供了一个更为快捷的方式来实现这个功能:
extern spinlock_t lock;
spin_lock_irq(&lock);
// run...
spin_unlock_irq(&lock);
如果没有nested interrupt,所有这一切都很好。
加上nested interrupt,我们再来看看这个例子:
// code 1
extern spinlock_t lock1;
spin_lock_irq(&lock);
// run..
spin_unlock_irq(&lock);
// code 2
extern spinlock_t lock2;
spin_lock_irq(&lock2);
// run...
spin_unlock_irq(&lock2);
Code 1和code 2都可运行在interrupt下,我们很容易就可以想到这样的运行次序():
Code 1 Code 2
extern spinlock_t lock1;
spin_lock_irq(&lock1); //这里会关闭中断
//函数调用到 -->> code2 -->>
extern spinlock_t lock2;
spin_lock_irq(&lock2);
// run...
spin_unlock_irq(&lock2);//这里会打开中断
//注意:这里可能会运行不正常了,因为对于code1而已,我现在是需要关闭中断运行的
// run...
spin_unlock_irq(&lock1);
问题是在第二个spin_unlock_irq后这个CPU的中断已经被打开,“死锁”的问题又会回到我们身边!
解决方法是我们在每次关闭中断前纪录当前中断的状态,然后恢复它而不是直接把中断打开。
Code 1 Code 2
extern spinlock_t lock1;
unsigned long flags;
local_irq_save(flags);
spin_lock(&lock1);
//函数调用到 -->> code2 -->>
extern spinlock_t lock2;
unsigned long flags;
local_irq_save(flags);
spin_lock(&lock2);
// run...
spin_unlock(&lock2);
local_irq_restore(flags);
// run...
spin_unlock(&lock1);
local_irq_restore(flags);
Linux同样提供了更为简便的方式:
Code 1 Code 2
extern spinlock_t lock1;
unsigned long flags;
spin_lock_irqsave(&lock1, flags);
//函数调用到 -->> code2 -->>
extern spinlock_t lock2;
unsigned long flags;
spin_lock_irqsave(&lock2, flags);
// run...
spin_unlock_irqrestore(&lock2, flags);
// run...
spin_unlock_irqrestore(&lock1, flags);
说了这么多基于单cpu的情况,那么多cpu呢?其实多cpu反而无需考虑这么多,因为多cpu之间没有优先级抢占问题,不会带来死锁。
总结:
- 如果被保护的共享资源只在进程上下文访问和软中断上下文访问,那么当在进程上下文访问共享资源时,可能被软中断打断,从而可能进入软中断上下文来对被保护的共享资源访问,因此对于这种情况,对共享资源的访问必须使用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上软中断或中断对共享资源的非同步访问