原子操作:就是在执行某一操作时不被打断。
linux原子操作问题来源于中断、进程的抢占以及多核smp系统中程序的并发执行。
对于临界区的操作可以加锁来保证原子性,对于全局变量或静态变量操作则需要依赖于硬件平台的原子变量操作。
因此原子操作有两类:一类是各种临界区的锁,一类是操作原子变量的函数。
对于arm来说,单条汇编指令都是原子的,多核smp也是,因为有总线仲裁所以cpu可以单独占用总线直到指令结束,多核系统中的原子操作通常使用内存栅障(memory barrier)来实现,即一个CPU核在执行原子操作时,其他CPU核必须停止对内存操作或者不对指定的内存进行操作,这样才能避免数据竞争问题。但是对于load update store这个过程可能被中断、抢占,所以arm指令集有增加了ldrex/strex这样的实现load update store的原子指令。
但是linux种对于c/c++程序(一条c编译成多条汇编),由于上述提到的原因不能保证原子性,因此linux提供了一套函数来操作全局变量或静态变量。
假设原子变量的底层实现是由一个汇编指令实现的,这个原子性必然有保障。但是如果原子变量的实现是由多条指令组合而成的,那么对于SMP和中断的介入会不会有什么影响呢?我在看ARM的原子变量操作实现的时候,发现其是由多条汇编指令(ldrex/strex)实现的。在参考了别的书籍和资料后,发现大部分书中对这两条指令的描诉都是说他们是支持在SMP系统中实现多核共享内存的互斥访问。但在UP系统中使用,如果ldrex/strex和之间发生了中断,并在中断中也用ldrex/strex操作了同一个原子变量会不会有问题呢?就这个问题,我认真看了一下内核的ARM原子变量源码和ARM官方对于ldrex/strex的功能解释,总结如下:
一、ARM构架的原子变量实现结构
对于ARM构架的原子变量实现源码位于:arch/arm/include/asm/atomic.h
其主要的实现代码分为ARMv6以上(含v6)构架的实现和ARMv6版本以下的实现。
该文件的主要结构如下:
- #if __LINUX_ARM_ARCH__ >= 6
- ......(通过ldrex/strex指令的汇编实现)
- #else /* ARM_ARCH_6 */
- #ifdef CONFIG_SMP
- #error SMP not supported on pre-ARMv6 CPUs
- #endif
- ......(通过关闭CPU中断的C语言实现)
- #endif /* __LINUX_ARM_ARCH__ */
- ......
- #ifndef CONFIG_GENERIC_ATOMIC64
- ......(通过ldrexd/strexd指令的汇编实现的64bit原子变量的访问)
- #else /* !CONFIG_GENERIC_ATOMIC64 */
- #include <asm-generic/atomic64.h>
- #endif
- #include <asm-generic/atomic-long.h>
这样的安排是依据ARM核心指令集版本的实现来做的:
(1)在ARMv6以上(含v6)构架有了多核的CPU,为了在多核之间同步数据和控制并发,ARM在内存访问上增加了独占监测(Exclusive monitors)机制(一种简单的状态机),并增加了相关的ldrex/strex指令。请先阅读以下参考资料(关键在于理解local monitor和Global monitor):
(2)对于ARMv6以前的构架不可能有多核CPU,所以对于变量的原子访问只需要关闭本CPU中断即可保证原子性。
对于(2),非常好理解。
但是(1)情况,我还是要通过源码的分析才认同这种代码,以下我仅仅分析最具有代表性的atomic_add源码,其他的API原理都一样。如果读者还不熟悉C内嵌汇编的格式,请参考《ARM GCC 内嵌汇编手册》
二、内核对于ARM构架的atomic_add源码分析
- /*
- * ARMv6 UP 和 SMP 安全原子操作。 我们是用独占载入和
- * 独占存储来保证这些操作的原子性。我们可能会通过循环
- * 来保证成功更新变量。
- */
- static inline void atomic_add(int i, atomic_t *v)
- {
- unsigned long tmp;
- int result;
- __asm__ __volatile__("@ atomic_add\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");
- }
源码分析:
注意:根据内联汇编的语法,result、tmp、&v->counter对应的数据都放在了寄存器中操作。如果出现上下文切换,切换机制会做寄存器上下文保护。
(1)ldrex %0, [%3]
意思是将&v->counter指向的数据放入result中,并且(分别在Local monitor和Global monitor中)设置独占标志。
(2)add %0, %0, %4
result = result + i
(3)strex %1, %0, [%3]
意思是将result保存到&v->counter指向的内存中,此时 Exclusive monitors会发挥作用,将保存是否成功的标志放入tmp中。
(4) teq %1, #0
测试strex是否成功(tmp == 0 ??)
(5)bne 1b
如果发现strex失败,从(1)再次执行。
通过上面的分析,可知关键在于strex的操作是否成功的判断上。而这个就归功于ARM的Exclusive monitors和ldrex/strex指令的机制。以下通过可能的情况分析ldrex/strex指令机制。(请阅读时参考4.2.12. LDREX 和 STREX)
1、UP系统或SMP系统中变量为非CPU间共享访问的情况
此情况下,仅有一个CPU可能访问变量,此时仅有Local monitor需要关注。
假设CPU执行到(2)的时候,来了一个中断,并在中断里使用ldrex/strex操作了同一个原子变量。则情况如下图所示:
- A:处理器标记一个物理地址,但访问尚未完毕
- B:再次标记此物理地址访问尚未完毕(与A重复)
- C:进行存储操作,清除以上标记,返回0(操作成功)
- D:不会进行存储操作,并返回1(操作失败)
也就是说,中断例程里的操作会成功,被中断的操作会失败重试。
2、SMP系统中变量为CPU间共享访问的情况
此情况下,需要两个CPU间的互斥访问,此时ldrex/strex指令会同时关注Local monitor和Global monitor。
(i)两个CPU同时访问同个原子变量(ldrex/strex指令会关注Global monitor。)
- A:将该物理地址标记为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。
- B:标记此物理地址为CPU1独占访问,并清除CPU1对其他任何物理地址的任何独占访问标记。
- C:没有标记为CPU0独占访问,不会进行存储,并返回1(操作失败)。
- D:已被标记为CPU1独占访问,进行存储并清除独占访问标记,并返回0(操作成功)。
也就是说,后执行ldrex操作的CPU会成功。
(ii)同一个CPU因为中断,“嵌套”访问同个原子变量(ldrex/strex指令会关注Local monito)
- A:将该物理地址标记为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。
- B:再次标记此物理地址为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。
- C:已被标记为CPU0独占访问,进行存储并清除独占访问标记,并返回0(操作成功)。
- D:没有标记为CPU0独占访问,不会进行存储,并返回1(操作失败)。
也就是说,中断例程里的操作会成功,被中断的操作会失败重试。
(iii)两个CPU同时访问同个原子变量,并同时有CPU因中断“嵌套”访问改原子变量(ldrex/strex指令会同时关注Local monitor和Global monitor)
虽然对于人来说,这种情况比较BT。但是在飞速运行的CPU来说,BT的事情随时都可能发生。
- A:将该物理地址标记为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。
- B:标记此物理地址为CPU1独占访问,并清除CPU1对其他任何物理地址的任何独占访问标记。
- C:再次标记此物理地址为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。
- D:已被标记为CPU0独占访问,进行存储并清除独占访问标记,并返回0(操作成功)。
- E:没有标记为CPU1独占访问,不会进行存储,并返回1(操作失败)。
- F:没有标记为CPU0独占访问,不会进行存储,并返回1(操作失败)。
当然还有其他许多复杂的可能,也可以通过ldrex/strex指令的机制分析出来。从上面列举的分析中,我们可以看出:ldrex/strex可以保证在任何情况下(包括被中断)的访问原子性。所以内核中ARM构架中的原子操作是可以信任的。