分类: LINUX
2014-08-09 23:22:16
法律声明:《LINUX3.0内核源代码分析》系列文章由谢宝友()发表于http://xiebaoyou.blog.chinaunix.net,文章中的LINUX3.0源代码遵循GPL协议。除此以外,文档中的其他内容由作者保留所有版权。谢绝转载。
原子变量适用于在多核之间对单一共享变量进行互斥访问。但是要保护多个变量,并且这些变量之间有逻辑关系时,原子变量就不适用了。例如:常见的双向链表。假设有三个链表节点A、B、C。需要将节点B插入节点A、C之间。如果CPU A刚好将A节点的后向指针指向B,但是还没有将B的后向指针指向C。此时CPU B要遍历链表,这将会一个灾难性的后果。
解决这个问题的方法当然是用锁。只要稍微熟悉多线程编程的读者,都应当熟练的使用过用户态的锁,如pthread_mutex_lock和pthread_mutex_unlock。锁就是为了防止多个线程同时进入某段代码,这似乎是一个再简单不过的东西。
但是锁还有其他重要的含义,获得锁和释放锁必须满足以下条件:
LOCK 操作保证:
ü LOCK之后的内存操作将在LOCK操作操作完成之后完成。
ü LOCK操作之前的内存操作可能在LOCK操作完成之后完成。
UNLOCK 操作保证:
ü UNLOCK之前的内存操作将在UNLOCK 操作完成前完成.
ü UNLOCK之后的操作可能在UNLOCK 操作完成前完成。
LOCK vs LOCK 保证:
ü 所有在另外的LOCK之前的 LOCK 操作,将在LOCK操作之前完成.
LOCK vs UNLOCK 保证:
ü 所有在UNLOCK操作之前的 LOCK 操作将在UNLOCK操作之前完成。
ü 所有在LOCK之前的 UNLOCK 操作将在LOCK操作之前完成.
失败的LOCK不能保证:
ü 几种LOCK变体操作可能失败,可能是由于不能立即获得锁,也可能是由于接收到一个非阻塞信号或者在等待锁可用时发生异常。失败的锁并不隐含任何类型的屏障。
以上这些规则适用于所有体系,是锁操作能够提供的最小担保。如果您想编写可移植的软件,那么只能假设这些保证,而不能期望有更多的保证。
当然,对于我们分析的ARM A9来说,锁操作有更多的保证。因为A9的内存屏障没有严格区分读屏障、写屏障、读写屏障。所有屏障都是用dsb指令实现的。举例来说,lock操作将提供下面的保证:
ü LOCK之后的内存操作将在LOCK操作操作完成之后完成。
ü LOCK操作之前的内存操作将在LOCK操作完成之前完成。
理解了锁的基本概念,才能真正的理解自旋锁及其他锁原语。
在中断处理函数中,不允许阻塞。自旋锁和信号量的最大区别就是:可以用于中断上下文。在获取不到锁的时候,它就使用一个循环,等待锁被释放。
自旋锁由spinlock_t结构定义。除去用于调试的代码以外,这个结构其实非常简单,就是一个raw_spinlock。
/**
* 自旋锁
*/
typedef struct raw_spinlock {
/**
* 对A9来说,就是volatile unsigned int lock;
* 这里定义volatile,是为了防止编译器优化对lock字段的读取。
* 强制编译出来的汇编代码从内存中读取变量的值。
*/
arch_spinlock_t raw_lock;
#ifdef CONFIG_GENERIC_LOCKBREAK
/**
* 表示当前是否有某个核正在申请自旋锁。未配置此选项。
*/
unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
/**
* magic是自旋锁的魔法数。当自旋锁结构被意外的修改后,可以被调试代码发现。
* owner_cpu是当前获取到锁的CPU编号。如果同一个CPU试图再次获取这个锁,内核将输出错误信息。
* owner是
*/
unsigned int magic, owner_cpu;
/**
* 当前获取自旋锁的进程task_struct结构。
*/
void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
/**
* 用于内存分配调试
*/
struct lockdep_map dep_map;
#endif
} raw_spinlock_t;
其实,最的自旋锁实现只需要一个字段,就是volatile unsigned int lock。
可以用DEFINE_SPINLOCK来定义一个静态声明的自旋锁。
如果您的自旋锁是动态申请的,那么可能在初始化代码中调用spin_lock_init,spin_lock_init将一个自旋锁初始化为未加锁状态。
获取自旋锁的函数是spin_lock,在理解了锁原语的含义后,理解其实现就简单了:
Spin_lock这个内联函数简单的调用raw_spin_lock。我们假设您配置了CONFIG_INLINE_SPIN_LOCK这个宏,那么它最终调用__raw_spin_lock。
在没有配置CONFIG_GENERIC_LOCKBREAK宏时,__raw_spin_lock宏的实现如下:
/**
* spin_lock的实际实现。
*/
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
/**
* 这里禁止抢占有三个目的:
* 1、禁止切换到其他进程,以避免在获取到自旋锁后,切换到另一个进程上下文申请同样的自旋锁以形成死锁。
* 2、禁止当前任务在获得锁后飘移到其他核上。
* 3、禁止编译优化。这是锁原语所需要的。
*/
preempt_disable();
/**
* 用于调试目的,一般未配置此调试选项。相当于空函数。
*/
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
/**
* 当调试时,调用do_raw_spin_trylock,否则调用do_raw_spin_lock
* 我们直接分析do_raw_spin_lock函数。
*/
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}
/**
* 申请自旋锁,此时已经禁止抢占。
* __acquires用于编译阶段,检查是否有未配对的使用锁。
*/
static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)
{
/**
* 调试目的,一般为空函数。
*/
__acquire(lock);
/**
* 调用各个体系结构自身的自旋锁实现。该函数才是我们分析的重点。
*/
arch_spin_lock(&lock->raw_lock);
}
/**
* A9的spin_lock实现,已经在spin_lock函数中禁止了抢占。
*/
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
unsigned long tmp;
__asm__ __volatile__(
"1: ldrex %0, [%1]\n"/* 将lock字段中的值加载到寄存器 */
" teq %0, #0\n"/* 将寄存器中的值与0比较 */
WFE("ne")
" strexeq %0, %2, [%1]\n"/* 如果lock字段为0,表示锁可用,则将1存入lock字段,表示本CPU已经成功获得锁 */
" teqeq %0, #0\n"/* 如果lock字段为0,表示我们已经试图向lock字段写入1.这样我们再次将strex指令的结果与0比较,看strex是否成功 */
" bne 1b"/* 如果lock字段不为0,或者我们在向lock字段写入1时,与其他核产生了冲突,写入失败,都需要跳转到循环开始处,重新获取锁。 */
: "=&r" (tmp)
: "r" (&lock->lock), "r" (1)
: "cc");
/**
* 运行到这里,说明我们已经成功向lock字段写入1,获取锁成功。
* 这里用一个smp读写屏障,是为了确保:
* 在获得锁以后,看得到前一个锁持有者在释放锁以前对要保护的数据的修改。
* 在获得锁以后,对要保护的数据不会先被其他核看到,其他核应当先看到我们对lock字段的修改。
*/
smp_mb();
}
释放锁由spin_unlock函数实现,它最终调用__raw_spin_unlock:
/**
* 释放自旋锁。
*/
static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
/**
* 调试代码
*/
spin_release(&lock->dep_map, 1, _RET_IP_);
/**
* do_raw_spin_unlock调用体系自定义的释放锁函数。
*/
do_raw_spin_unlock(lock);
/**
* 打开抢占
*/
preempt_enable();
}
do_raw_spin_unlock实质上是对arch_spin_unlock的一个简单封装函数。
/**
* 释放自旋锁
*/
static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
/**
* 这里使用内存屏障的作用是:
* 确保在调用spin_unlock之前,我们对要保护的数据的修改,可以早于lock字段之前被其他核看到。
* 同时也是一个编译优化屏障,避免编译器将锁保护的代码编译到spin_lock之后。
* 按照锁的语义,这里可以只用一个写屏障,但是对A9来说,写屏障也就是一个读写屏障。
*/
smp_mb();
/**
* 这里使用汇编强制向lock字段的内存中写入0值,以释放锁。
*/
__asm__ __volatile__(
" str %1, [%0]\n"
:
: "r" (&lock->lock), "r" (0)
: "cc");
/**
* 这里再次使用了dsb指令,其实也是一个屏障,即保证其他先看到对锁的修改,才能看到释放锁后面的语句的修改。
* 呜呼,使用这么多的屏障,对性能的损坏更大。所以自旋锁也不是一只好鸟啊。
*/
dsb_sev();
}
自旋锁用于保护多核之间的数据同步。某个核获得自旋锁后,申请同一自旋锁的CPU就自旋等待,直到锁被释放。因此,要求自旋锁保护的数据尽量少,执行的代码段尽量短。实际上,当系统中的CPU个数超过1024个时,使用自旋锁需要非常小心。
如果在开中断的情况下使用使用锁,需要特别小心。因为中断随时可能中断正在运行的程序,如果中断打断的是一段处于自旋锁保护的代码,那么它可能使得其他申请同一自旋锁的CPU也陷入停顿。
为此,一般需要在申请自旋锁以前关闭中断。内核提供了spin_lock_irqsave和spin_unlock_irqrestore函数对供使用。这一对函数的实现也很有趣。读者可以自行阅读相关代码。
有时,我们在内核代码中看到直接调用raw_spin_lock的地方。而没有使用spin_lock。从前面我们的分析可以看到,二者实质上是一回事,为什么内核要直接调用raw_spin_lock呢?
这是因为,实时补丁修改了spin_lock,使得spin_lock可以抢占了。这增加了实时性,但是却使得自旋锁的语义发生了颠覆性的改变。我们前面讲的是spin_lock的原始语义。在实时内核中,仍然有些上下文不允许调度,此时需要使用原始的自旋锁语义,即调用raw_spin_lock:
ü 在调度函数中
ü 在真正的中断上下文(实时补丁将中断线程化了,但是仍然有部分代码运行在真正的中断上下文中)