现在的位置: 首页 > 综合 > 正文

浅析arm(kernel-2.6.13)自旋锁与信号量

2018年04月16日 ⁄ 综合 ⁄ 共 5216字 ⁄ 字号 评论关闭


在单处理器上,自旋锁仅仅当作一个设置内核抢占的开关。如果内核抢占也不存在,那么自旋锁会在编译时被完全剔除出内核

我的是s3c2440 armv4的单核。

这个内核被配置为可抢占的。

# Kernel Features

#

CONFIG_PREEMPT=y


自旋锁的实现:

spin_lock的定义如下:

#define spin_lock(lock)  _spin_lock(lock)

而_spin_lock():

#define _spin_lock(lock) \

do { \

 preempt_disable(); \

 _raw_spin_lock(lock); \

 __acquire(lock); \

} while(0)

#define _raw_spin_lock(lock) do { (void)(lock); } while(0)

可见他只是个抢占的开关

而在armv6或更高的版本多smp中

spinlock_t结构如下:

typedef struct {

 volatile unsigned int lock;

#ifdef CONFIG_PREEMPT

 unsigned int break_lock;

#endif

} spinlock_t;

下面分析原始加锁函数_raw_spin_lock():

static inline void _raw_spin_lock(spinlock_t *lock)

{

 unsigned long tmp;

 __asm__ __volatile__(

num1:"1: ldrex %0, [%1]\n"  //取lock->lock放在 tmp里,并且设置&lock->lock这个内存地址为独占访问。

num2:" teq %0, #0\n"  //测试lock_lock是0吗?影响标志位z

num3:" strexeq %0, %2, [%1]\n"  //如果lock_lock是0,并且是独占访问这个内存,就向lock->lock里写入1,并向tmp返回0,同时清除独占标记

num4:" teqeq %0, #0\n"  //如果lock_lock是0,并且strexeq返回了0,表示加锁成功,返回。

num5:" bne 1b"  //如果上面的条件(1:lock->lock里不为0,2:)有一个不符合,就在原地打转

num6: : "=&r" (tmp) //%0:输出放在tmp里,可以是任意寄存器

 : "r" (&lock->lock), "r" (1)   //%1:取&lock->lock放在任意寄存器,%2:任意寄存器放入1

 : "cc");

 smp_mb();

}


可以发现,第一个完成ldrex-strexeq指令对的进程(就是第一个完成strexeq指令的进程)可以得到自旋锁,其他cpu核将原地转。


信号量的实现:

struct semaphore {

 atomic_t count;

 int sleepers;

 wait_queue_head_t wait;

};

typedef struct { volatile int counter; } atomic_t;


/*

 * This is ugly, but we want the default case to fall through.

 * "__down" is the actual routine that waits...

 */

static inline void down(struct semaphore * sem)

{

 might_sleep();

 __down_op(sem, __down_failed);

}

#define __down_op(ptr,fail)  

 ({    

 __asm__ __volatile__(  

 "@ down_op\n"   
" mrs ip, cpsr\n" //首先禁止中断 

" orr lr, ip, #128\n"  

" msr cpsr_c, lr\n"  

" ldr lr, [%0]\n" //获取信号量 

" subs lr, lr, %1\n" //自减

" str lr, [%0]\n" //存起来

" msr cpsr_c, ip\n" //开中断

" movmi ip, %0\n" //看看是变成负数了没?是负数表示现在没有信号量。
" blmi " #fail  

 :   //无输出 

 : "r" (ptr), "I" (1) //ptr放在任意REG,%1:是个立即数1 

 : "ip", "lr", "cc"); //这些REG可能遭破坏

 smp_mb();   

 })


下面是v6或以上的版本

#if __LINUX_ARM_ARCH__ >= 6

#define __down_op(ptr,fail) \

 ({   \

 __asm__ __volatile__( \

 "@ down_op\n"  \

"1: ldrex lr, [%0]\n" \ //获取sem的第一个成员count,放在lr

" sub lr, lr, %1\n" \ //自减1

" strex ip, lr, [%0]\n" \ //放回去,ip存放操作结果,是成功还是失败

" teq ip, #0\n" \ //如果成功

" bne 1b\n"  \ //成功就继续,不成功就是说ldrex-strex没有组成原子操作,count可能被其他进程篡改(tampered)了。所以在来一边。

" teq lr, #0\n" \ //为下面的测试做准备

" movmi ip, %0\n" \ //内部过程调用暂时寄存器,取得count的地址,如果count是负数的话。

" blmi " #fail  \ //跳过去?(我不清楚他是怎么跳过去的,希望得到指点:-),如果不小于0,就是说还可以用,就不用sleep了。

 :   \ //无输出

 : "r" (ptr), "I" (1) \ //%0:sem指针ptr,放在任意寄存器里,大概是r0里吧?,%1:是个立即数

 : "ip", "lr", "cc"); \ //可能会改变的寄存器

 smp_mb();  \

 })

不管怎么样,他应该跳到了下面的那个标号处。

asm(" .section .sched.text,\"ax\"  \n\

 .align 5    \n\

 .globl __down_failed   \n\

__down_failed:     \n\

 stmfd sp!, {r0 - r3, lr}  \n\

 mov r0, ip    \n\
 bl __down    \n\ //这儿之后,当前进程就睡下了。

 ldmfd sp!, {r0 - r3, pc}  \n\

      \n\

 .align 5    \n\

 .globl __down_interruptible_failed \n\

__down_interruptible_failed:   \n\

 stmfd sp!, {r0 - r3, lr}  \n\

 mov r0, ip    \n\

 bl __down_interruptible  \n\

 mov ip, r0    \n\

 ldmfd sp!, {r0 - r3, pc}  \n\

      \n\

 .align 5    \n\

 .globl __down_trylock_failed  \n\

__down_trylock_failed:    \n\

 stmfd sp!, {r0 - r3, lr}  \n\

 mov r0, ip   

 ...

 ...

void __sched __down(struct semaphore * sem)

{

 struct task_struct *tsk = current;

 DECLARE_WAITQUEUE(wait, tsk);

 tsk->state = TASK_UNINTERRUPTIBLE;

 add_wait_queue_exclusive(&sem->wait, &wait);

 spin_lock_irq(&semaphore_lock);

 sem->sleepers++;

 for (;;) {

  int sleepers = sem->sleepers;

  /*

   * Add "everybody else" into it. They aren't

   * playing, because we own the spinlock.

   */

  if (!atomic_add_negative(sleepers - 1, &sem->count)) { //看看可以有信号量可用了吗?唯一的出口。

   sem->sleepers = 0;

   break;

  }

  sem->sleepers = 1; /* us - see -1 above */

  spin_unlock_irq(&semaphore_lock);

  schedule();

  tsk->state = TASK_UNINTERRUPTIBLE;

  spin_lock_irq(&semaphore_lock);

 }

 spin_unlock_irq(&semaphore_lock);

 remove_wait_queue(&sem->wait, &wait);

 tsk->state = TASK_RUNNING;

 wake_up(&sem->wait);

}

小结:自旋锁会导致死循环,锁定期间不允许阻塞(即临界区里没有导致阻塞的函数),要求锁定的临界区要小,信号量允许临界区阻塞,可以适用于临界区大的情况。

- 寄存器 r12 是内部调用暂时寄存器 ip。它在过程链接胶合代码(例如,交互操作胶合代码)中用于此角色。在过程调用之间,可以将它用于任何用途。被调用函数在返回之前不必恢复 r12。

 



3.3.8  LDREX和STREX指令

n 独占加载和存储寄存器

指令语法格式如下所示:

LDREX{cond} Rt, [Rn {, #offset}]

STREX{cond} Rd, Rt, [Rn {, #offset}]

LDREXB{cond} Rt, [Rn]

STREXB{cond} Rd, Rt, [Rn]

LDREXH{cond} Rt, [Rn]

STREXH{cond} Rd, Rt, [Rn]

LDREXD{cond} Rt, Rt2, [Rn]

STREXD{cond} Rd, Rt, Rt2, [Rn]

说明:

con            可选的条件代码。

Rd              存放返回状态的目标寄存器。

Rt               要加载或存储的寄存器。

Rt2             进行双字加载或存储时要用到的第二个寄存器。

Rn              内存地址所基于的寄存器。

offset        要应用到Rn中的值的可选偏移量。offset只可用于Thumb?2指令中。如果省略offset,则认为偏移量为0。

n  LDREX(独占装载指令)

LDREX可从内存中加载数据。

如果物理地址有共享TLB属性,那么LDREX会将该物理地址标记为由当前处理器独占访问,并且会清除该处理器对其他任何物理地址的独占访问标签。否则,会标记为“执行处理器已经标记了一个物理地址,但访问尚未完毕”。

n  STREX(独占存储指令)

STREX可在一定条件下向内存中存储数据。条件具体如下:

·      如果物理地址没有共享TLB属性,执行处理器有一个已标记物理地址,但尚未访问完毕,那么将会进行存储,清除该标记,并向Rd中返回值0;

·      如果物理地址没有共享TLB属性,且执行处理器也没有已标记但尚未访问完毕的物理地址,那么将不会进行存储,而会向Rd返回值1;

·      如果物理地址有共享TLB属性,且已被标记为由执行处理器独占访问,则将进行存储,清除标签,并向Rd返回值0;

·      如果物理地址有共享TLB属性,但却没有标记为由执行处理器独占访问,则不会进行存储,而会向Rd中返回值1。

n 限制

offset不可用于ARM指令中;offset的值可为0~1020范围内的任意一个4的倍数。

r15不可用于Rd、Rt、Rt2或Rn。

对于STREX,Rd一定不能与Rt、Rt2或Rn为同一寄存器。

对于LDREX,Rt和Rt2不可为同一寄存器。

在ARM指令中,Rt必须是一个编号为偶数的寄存器,且不能为r14;同时Rt2必须为R(d+1)。

n 用法

利用LDREX和STREX可在多处理器和共享内存系统间实现进程间通信。

出于性能方面的考虑,请将相应LDREX指令和STREX指令间的指令数控制到最少。

n 注意

STREX指令中所用的地址必须要与近期执行次数最多的LDREX指令所用的地址相同。如果使用不同的地址,则STREX指令的执行结果将不可预知。

n 适用体系结构

ARM LDREX和STREX可用于ARMv6及更高版本中。

ARM LDREXB、LDREXH、LDREXD、STREXB、STREXD和STREXH可用于ARMv6K及更高版本中。

所有这些32位Thumb指令均可用于ARMv6T2和ARMv7,但LDREXD和STREXD不可用于ARMv7-M架构。

这些指令均无16位版本。

n 示例

MOV r1, #0x1

try

LDREX r0, [LockAddr]

CMP r0, #0

STREXEQ r0, r1, [LockAddr]

CMPEQ r0, #0

BNE try

...

抱歉!评论已关闭.