Linux自旋锁使用场景分析
自旋锁
spinlock
同一时刻只能被一个内核代码路径持有,如果有另外一个内核代码试图获取一个已经被持有的spinlock
,那么该内核代码路径需要一直自旋忙等到,直到锁持有者释放了该锁。如果该锁没有被别人持有,那么可以立即获得该锁。特性如下:
-
忙等待的锁机制。操作系统总锁的机制分为两类,一类是忙等待,另一类是睡眠等待。
spinlock
属于前者,当无法获取spinlock
锁时会不断尝试,直到获取锁为止。 -
同一时刻只能有一个内核代码路径可以获得该锁。
-
要求
spinlock
锁持有者尽快完成临界区的执行任务。如果临界区执行时间过程,在锁外面忙等待的CPU
比较浪费,特别是spinlock
临界区里不能睡眠。 -
spinlock
锁可以在中断上下文中使用。
场景分析
仅在进程上下文中使用自旋锁
- A进程在某段代码中访问共享资源S
- B进程在某段代码中也需要访问共享资源S
假设在A访问共享资源S的过程中发生了中断,中断唤醒了沉睡中的,优先级更高的B,在中断返回现场的时候,发生了进程切换。B程序开始执行,并且同样需要访问共享资源S。如果这时候没有锁保护,那么就会出现两个线程进行竞争资源的情况,导致程序出错。
加上spinlock
后,A在进入临界区之前获取了spinlock
,同时,A在访问共享资源S的时候发生了中断,中断中还是唤醒了优先级更高的进程B,那么B在访问临界区之前仍然会获取spinlock
,这时由于A占有了spinlock
,所以B进程会永久的等待spinlock
,即发生了死锁。
结合上面的场景分析,自旋锁在同一cpu
上必须要做的操作就是,关闭本地CPU上的进程抢占。不管是在单核还是多核场景下,关闭本地cpu进程抢占都是必不可少的。spinlock关闭本地抢占后,上述中断场景下,发生中断后,由于不允许发生进程抢占,所以中断结束后还是会回到进程A执行。
如果A和B运行在不同cpu
上,那么场景就简单很多了,虽然A获取了spinlock
导致B在自旋获取spinlock
消耗资源。但A操作完共享资源S后会立刻释放资源,B进程就能获取到spinlock
了。
进程上下文和中断中使用自旋锁
- A进程在某段代码中访问共享资源S
- 外设X的中断处理函数也会访问共享资源S
在这样的场景下,还可以用spinlock
保护共享资源S吗?
假设A在进入临界区之前获取了spinlock
,随后外设X的中断来临,且在当前cpu
上处理,即打断了spinlock
临界区的操作。这时候在X中断中去获取spinlock
就会一直自旋,因为spinlock
已经被A获取了,所以这时候就会死锁。
结合上述分析,在A进入临界区获取spinlock
时,需要关闭本地cpu
中断。即使用spin_lock_irq/spin_unlock_irq
或者spin_lock_irqsave/spin_unlock_irqrestore
接口。
如果A和外设X中断服务函数在不同cpu
上,那么首先,A使用spin_lock_irqsave
不会影响X中断中获取自旋锁的行为。但是多核情况下,中断可能可以保证每次都在一个cpu
上执行,但是进程会在不同cpu
间切换运行。所以在设计驱动时还是要使用spin_lock_irqsave
接口来防止出现死锁情况。
进程上下文和中断下半部中使用自旋锁
大多数时候,驱动代码在实现的时候,中断上半部仅仅清除了硬件中断状态,随机进行下半部处理。这里说的下半部是指softirq
和tasklet
。
其实场景和中断情况类似,当中断处理完退出(irq_exit
)时检查软中断并执行时,如果发生了spinlock
竞争情况,也会出现死锁情况。
但是仅仅因为下半部中spinlock
的使用,就把本地中断禁止,就未免得不偿失了。所以Linux
又设计了禁止下半部的spinlock
接口。即spin_lock_bh/spin_unlock_bh
。
补充一点软中断的说明,spin_lock_irq
是硬件/架构相关的操作。因为软中断其实就是软件依托中断上下文进行处理的行为,所以spin_lock_bh
仅仅就是增加了当前task
的thread_info
数据结构的preemt_count
中软中断的计数。
preempt_count
变量中包含了抢占计数,软中断计数,硬中断计数。其中软中断计数占了8bit
。但是Linux
其实是不支持软中断嵌套的。所以软中断计数其实只有0和1,即只占了1Bit
。这里Linux
做了点手脚,禁止软中断时,在软中断计数的Bit1
开始增加。用于区分是在软中断处理过程中还是禁止软中断调度(disable_bh
)。
中断上下文之间的竞争
同一种中断handler
之间在单核和smp多核上都不会并行执行,这是linux kernel
的特性。
如果不同中断handler
需要使用spinlock
保护共享资源,对于新的内核,所有handler
都是关闭中断的,因此使用spinlock
不需要关闭中断的配合。
bottom half
又分成softirq
和tasklet
,同一种softirq
会在不同的CPU上并发执行,因此如果某个驱动中的softirq
的handler
中会访问某个全局变量,对该全局变量是需要使用spinlock
保护的,不用配合disable CPU
中断或者bottom half
。
tasklet
更简单,因为同一种tasklet
不会多个CPU
上并发。
如果是中断上半部和下半部共用一个spinlock
来保护资源。那么下半部使用spinlock
时,还是需要spin_lock_irq()
。因为软中断环境下,中断又重新使能了,这时候软中断还是有可能会被中断打断,并出现死锁情况的。更何况,软中断负载特别大的情况下,软中断会被延后到`ksoftirqd线程中进行,即有概率在进程上下文中被执行。
总结
spin_lock()
的时候,禁止内核抢占
如果涉及到中断上下文的访问,spinlock
需要和禁止本 CPU
上的中断联合使用:spin_lock_irq/spin_unlock_irq
、spin_lock_irqsave/spin_unlock_irqrestore
涉及 half bottom 使用:spin_lock_bh / spin_unlock_bh