0、说明
很多时候可能会访问一个资源。比如,多个进程同时打开字符设备,通过模拟SPI读取数据的时候,同一时刻只能进行一个模拟时序,这个时候需要对资源的访问进行控制,保证同一时刻只有一个在执行,很多时候需要对临界资源进行保护,确保只有一个在执行中。
内核提供了很多同步互斥的锁,常用的有 原子操作,自旋锁,信号量,互斥量。
1、原子操作
如下,还有很多在arch\arm\include\asm\atomic.h中定义
atomic_add(i,v)
atomic_sub(i,v)
分析其中一个的实现
ATOMIC_OPS(add, +=, add)
将如上宏展开
#define ATOMIC_OP(add, +=, add) \
static inline void atomic_add(int i, atomic_t *v) \
{ \
unsigned long tmp; \
int result; \
\
prefetchw(&v->counter); \
__asm__ __volatile__("@ atomic_" #op "\n" \
"1: ldrex %0, [%3]\n" \
" add %0, %0, %4\n" \
" strex %1, %0, [%3]\n" \
" teq %1, #0\n" \
" bne 1b" \
: "=&r" (result), "=&r" (tmp), "+Qo" (v->counter) \
: "r" (&v->counter), "Ir" (i) \
: "cc"); \
}
独占指令ldrex、和strex是实现原子操作的基础,也是实现其他锁的基础。ldrex读取的时候将变量标记为独占,写入的时候判断独占标记,若标记在,则变量未被修改,可以正常修改使用。若标记不在则重新尝试。
以上为>=ARMV6,在ARMV6之前,不支持多个CPU,且非抢占,则直接关闭中断后对变量操作即可:
#define ATOMIC_OP(op, c_op, asm_op) \
static inline void atomic_##op(int i, atomic_t *v) \
{ \
unsigned long flags; \
\
raw_local_irq_save(flags); \
v->counter c_op i; \
raw_local_irq_restore(flags); \
}
2、自旋锁
自旋锁,名字上看自旋二字,有点while(1)的感觉,但比while(1)还狠。名字可以推断,请求自旋锁的时候不会休眠,会自旋在那里,直到获取成功,因此可以用于中断上下文中。
自旋锁的出现主要是为SMP环境设计的,只有在SMP环境下才会“自旋”起来。在不同平台不同架构下的实现完全不一样。
数据结构与函数
//定义锁
spinlock_t splock;
//初始化锁
spin_lock_init(&testlock);
//请求锁
spin_lock/spin_lock_irq/spin_lock_irqsave
//释放锁
spin_unlock/spin_unlock_irq/spin_unlock_restore
单核中的自旋锁
单核系统只有一个处理器,不存在严格意义上的并发,当进程A通过系统调用进入进程上下文中,获取临界区锁,在未释放锁之前,处于非抢占状态,其他进程无法得到执行,因此适合处理一些时间较短的事情。
spin_lock锁会禁止抢占,也就是进行不会被调度走,但是没有禁止中断,如果中断函数中使用了同一个锁,此时中断获取锁失败带来的自旋,会导致死锁。
因此出现了spin_lock_irq,来关闭中断。在临界区执行期间不会被中断程序所抢占。来unlock的时候总会开中断,那么问题来了,当首先请求了两个锁,此时释放了一个锁之后,中断就会被打开,而又可能导致第一个锁陷入中断。 当然中断中不使用同一个锁的时候,完全可以在临界区被中断,因为不会带来死锁的问题。
因此出现了spin_lock_irqsave,记录中断的状态,释放锁时总是回到之前的中断状态,于此同时带来的是性能上的消耗。
单核下并不存在多个进程并发一个锁资源的状态,因为自旋锁直接关闭了抢占,根本不给其他进程执行的机会。在不支持抢占的系统中,单处理器自旋锁直接是一个空函数。
多核自旋锁
与单核不同,进程B在访问临界资源的时候,可能会由于进程A拿着锁,导致进程B处于自旋状态,也就是等待锁的释放,从而达到真正意义上的并发锁效果。
自旋的特点,决定了临界区的任务不适合太多,不然自旋上花费的时间太多,而又禁止抢占是cpu资源的一种浪费。
内核实现类似于银行排队系统,next是分配给每个申请的单元,owner是当前获取锁的对象,当next和owner的时候可以进入临界区。
3、semaphore信号量
lock依赖于spinlock,count表明有多少资源,wait_list是等待队列,获取不到资源的加入到这这个链表,等待有资源的时候唤醒。
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
4、互斥量
特殊的信号量进行优化,count只有0、1两种情况。通过原子操作对值进行判断,如果一切顺利,走fastpath路径,如果不顺利走slowpath,在死循环中判断count值,直到自己获取到锁。
5、总结
各种锁底层都依赖 独占指令 ldrex、strex来实现,无非是对标志标量的运算和判断。
- spin_lock会禁止抢占,因此临界区处理任务不宜过多,不然浪费cpu资源
- 自旋锁不会休眠,因此可以用于中断上下文,但要防止死锁
- 根据临界区的位置合理选择合适的锁,带来性能上的最优
- 自旋锁适合多处理器之间的互斥