分类: LINUX
2013-08-23 16:44:43
原文地址:浅析arm(kernel-2.6.13)自旋锁与信号量 作者:mclovein
浅析arm(kernel-2.6.13)自旋锁与信号量
在单处理器上,自旋锁仅仅当作一个设置内核抢占的开关。如果内核抢占也不存在,那么自旋锁会在编译时被完全剔除出内核。
我的是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。
from:http://blog.mcuol.com/User/emblinux/Article/13762_1.htm
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
...