Chinaunix首页 | 论坛 | 博客
  • 博客访问: 864010
  • 博文数量: 61
  • 博客积分: 958
  • 博客等级: 准尉
  • 技术积分: 2484
  • 用 户 组: 普通用户
  • 注册时间: 2011-05-21 13:36
文章分类
文章存档

2020年(2)

2019年(1)

2018年(5)

2017年(7)

2015年(2)

2014年(4)

2012年(10)

2011年(30)

分类: LINUX

2011-11-11 16:57:56

摘要:本文主要讲述linux如何处理ARM cortex A9多核处理器的内核同步部分。主要包括自旋锁介绍。
 

法律声明LINUX3.0内核源代码分析》系列文章由谢宝友()发表于http://xiebaoyou.blog.chinaunix.net,文章中的LINUX3.0源代码遵循GPL协议。除此以外,文档中的其他内容由作者保留所有版权。谢绝转载。

 
1.1 自旋锁
1.1.1      什么是锁

原子变量适用于在多核之间对单一共享变量进行互斥访问。但是要保护多个变量,并且这些变量之间有逻辑关系时,原子变量就不适用了。例如:常见的双向链表。假设有三个链表节点A、B、C。需要将节点B插入节点A、C之间。如果CPU A刚好将A节点的后向指针指向B,但是还没有将B的后向指针指向C。此时CPU B要遍历链表,这将会一个灾难性的后果。

解决这个问题的方法当然是用锁。只要稍微熟悉多线程编程的读者,都应当熟练的使用过用户态的锁,如pthread_mutex_lockpthread_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操作完成之完成。

理解了锁的基本概念,才能真正的理解自旋锁及其他锁原语。

1.1.2      自旋锁实现

在中断处理函数中,不允许阻塞。自旋锁和信号量的最大区别就是:可以用于中断上下文。在获取不到锁的时候,它就使用一个循环,等待锁被释放。

自旋锁由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_initspin_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);

}

 

/**

 * A9spin_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();

}

1.1.3      使用方法

自旋锁用于保护多核之间的数据同步。某个核获得自旋锁后,申请同一自旋锁的CPU就自旋等待,直到锁被释放。因此,要求自旋锁保护的数据尽量少,执行的代码段尽量短。实际上,当系统中的CPU个数超过1024个时,使用自旋锁需要非常小心。

如果在开中断的情况下使用使用锁,需要特别小心。因为中断随时可能中断正在运行的程序,如果中断打断的是一段处于自旋锁保护的代码,那么它可能使得其他申请同一自旋锁的CPU也陷入停顿。

为此,一般需要在申请自旋锁以前关闭中断。内核提供了spin_lock_irqsavespin_unlock_irqrestore函数对供使用。这一对函数的实现也很有趣。读者可以自行阅读相关代码。

1.1.4      可抢占的自旋锁

有时,我们在内核代码中看到直接调用raw_spin_lock的地方。而没有使用spin_lock。从前面我们的分析可以看到,二者实质上是一回事,为什么内核要直接调用raw_spin_lock呢?

这是因为,实时补丁修改了spin_lock,使得spin_lock可以抢占了。这增加了实时性,但是却使得自旋锁的语义发生了颠覆性的改变。我们前面讲的是spin_lock的原始语义。在实时内核中,仍然有些上下文不允许调度,此时需要使用原始的自旋锁语义,即调用raw_spin_lock

ü  在调度函数中

ü  在真正的中断上下文(实时补丁将中断线程化了,但是仍然有部分代码运行在真正的中断上下文中)

阅读(3632) | 评论(0) | 转发(5) |
给主人留下些什么吧!~~