ARM linux 的原子操作分析

linux ARM的原子操作源文件位于 linux/arch/arm/include/asm/atomic.h

linux源码宏展开

最开始由如下宏定义,linux的各种宏太复杂了,分析起来有点费劲

#define ATOMIC_OP(op, c_op, asm_op)					\  <-------------------|
static inline void atomic_##op(int i, atomic_t *v)			\                      |
{									\                      |
	unsigned long tmp;						\                      |
	int result;							\                      |
									\                      |
	prefetchw(&v->counter);						\                      |
	__asm__ __volatile__("@ atomic_" #op "\n"			\                      |
"1:	ldrex	%0, [%3]\n"						\                      |
"	" #asm_op "	%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");							\                      |
}                                                                                              |
                                                                                               |
#define ATOMIC_OPS(op, c_op, asm_op)					\ <-----|              |
	ATOMIC_OP(op, c_op, asm_op)					\       |--------------|
	ATOMIC_OP_RETURN(op, c_op, asm_op)					|
			                                                        |
ATOMIC_OPS(add, +=, add)		-----------------------------------------

最终最开始的函数static inline void atomic_##op(int i, atomic_t *v),替换op为add, asm_op为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");							
}

又根据GCC内联汇编的替换规则,result,tmp,v->counter,&v->counter,i按照出现的顺序分别替换%0-%4,操作数被逐个替换:

static inline void atomic_add(int i, atomic_t *v)
{									
	unsigned long tmp;	
	int result;
									
	prefetchw(&v->counter);						
	__asm__ __volatile__(
"1:	ldrex	result, [&v->counter]\n"						
"	add	result, result, i\n"					
"	strex	tmp, result, [&v->counter]\n"						
"	teq	tmp, #0\n"						
"	bne	1b"							
	: "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)
	: "r" (&v->counter), "Ir" (i)					
	: "cc");							
}

单独提取汇编部分

1:
	ldrex	result, [&v->counter]
	add	result, result, i
	strex	tmp, result, [&v->counter]
	teq	tmp, #0
	bne	1b

翻译成C语言:

	do{
		result = v->counter;				//ldrex	result, [&v->counter]
		result = result + i;				//add	result, result, i
		tmp = (v->counter = result赋值失败?1:0);	//strex	tmp, result, [&v->counter]
	}while(tmp != 0);					//teq	tmp, #0;bne	1b

比较怪的一句是tmp = (v->counter = result赋值失败?1:0);,当然这不是合法的C语句,但是它表达的就是这个意思。它会检测v->counter = result这句赋值操作,如果成功赋值则返回0给tmp,否则返回1。这里重点是要理解ldrexstrex这两条汇编是什么意思。

LDREX和STREX指令

从ARMv6架构开始,ARM处理器提供了Exclusive accesses同步原语,用于对内存访问的同步, 包含两条指令:

LDREX Rd [Rs]
STREX R0 Rd [Rs]

LDREX和STREX指令,将对一个内存地址的原子操作拆分成两个步骤,一起完成对内存的原子操作。可以理解为执行LDREX Rd [Rs]指令会标记对[Rs]这个内存地址的访问是独占状态(exclusive state)。而执行STREX R0 Rd [Rs]指令会让先前处于独占状态的内存地址[Rs]转变为正常状态,并且设置R0为0。若执行STREX R0 Rd [Rs]指令时,内存地址[Rs]是正常状态,则指令的存储动作会失败,并且R0置为1。

ARM内部为了实现这个功能,还有不少复杂的情况要处理。在ARM系统中,内存有两种不同且对立的属性,即共享(Shareable)和非共享(Non-shareable)。共享意味着该段内存可以被系统中不同处理器访问到,这些处理器可以是同构的也可以是异构的。而非共享,则相反,意味着该段内存只能被系统中的一个处理器所访问到,对别的处理器来说不可见。

为了实现独占访问,ARM系统中还特别提供了所谓独占监视器(Exclusive Monitor)的东西,其结构大致如下:
ARM linux 的原子操作分析
这里面包含两种类型的monitor,局部和全局的。对于非共享的内存,仅关心local monitor即可,而共享的内存则要local也global都需要关心。

每个monitor是一个状态机,包含exclusive态和open态。LDREX操作会让对应的monitor变为exclusive态,而如果monitor现在为exclusive态,这时执行STREX操作会让monitor变为open态,并且存储内存这个动作执行成功,否则monitor状态不变,存储行为也失败。

ARM linux 的原子操作分析

上一篇:Linux之source命令


下一篇:linux--9week