linux kernel同步之原子操作
内核同步之原子操作
在多线程编程中(无论多核还是单核),对同一内存访问时,如果没有同步机制,那么程序的执行结果和预期结果可能就不一致。比如:
线程1 |
线程2 |
read |
read |
|
modify |
modify |
write |
write |
|
|
|
线程1和线程2读取同一块内存上的值到寄存器中,然后修改这个值,线程2先于线程1将其修改后的值写回到内存中。
这样的结果会导致,线程2的修改操作会被线程1的修改所覆盖,这不是我们所期望的(考虑是个统计量,会导致来自线程2的有效统计数据丢失)。
为了防止上述的不一致,内核引入了多种同步机制,其中原子变量及原子操作为其中一种方式。
本文,我们来学习ARM下这个原子变量以及操作是如何保证数据的同步的。
首先给出内核对原子变量类型的定义:
include/linux/types.h
原子变量类型atomic_t是一个数据结构,只有一个int型变量。
对于原子变量的普通的读写:
atomic_read(v)直接从原子变量的内存中读走,不使用cache中的值。
atomic_set(v, i)这个设置变量的值,这个api并不能保证原子操作,一般只用于初始化。
当原子变量初始化后,后续的写操作需要保证排他性,对于ARM是如何实现的?
我们先来看ARM6之后的版本(ARM6之后是SMP多核系统)实现。
ARM6之后的写操作的排他性是由硬件来实现的,主要是使用ldrex/strex指令成对使用来实现,
ARM6之后的core中有两种monitor,一种是local monitor, 另一种是global monitor,ldrex/strex指令需要用到这两个monitor.
我们来看一下这两个指令:
loadEx指令:
loadEx指令首先取当前processor id,加载内存地址上的数据到Rd, 然后判断内存地址是否是所有processer共享的(Shared(Rn)), 如果是则将内存物理地址,processor id信息mark 到global monitor(MarkExclusiveGlobal(physical_address, processor_id, 1))。
最后将当前processor id信息mark到local monitor(MarkExclusiveLocal(processor_id))。
storeEx指令:
storeEx指令使用Rd来表示store是否成功,0表示成功,1表示失败。
storeEx首先获取当前processor id,然后判断当前processor是否mark local monitor了没(IsExclusiveLocal(processor_id)).如果当前processor在storeEx之前没有做过loadEx操作,即IsExclusiveLocal(processor_id) == false,那么直接返回失败。所以,storeEx操作的前提是loadEx.
然后判断内存地址是否processor之间共享的:
如果是共享的,则判断这个内存的物理地址,当前processor id是否被mark了global monitor,是的话,则将值写入内存,设置Rd为0,并清除Global monitor中该地址&processor信息。如果没有被mark global monitor,则返回失败(Rd=1)。
如果不是共享的,则将值写入内存,并设置Rd为0
最后清除local monitor中本processor id信息(ClearExclusiveLocal(processor_id)).
总结一下:
使用loadEx/storeEx指令对来改变同一多核共享内存中的值,必须先使用loadEX加载多核共享内存中的值,然后使用storeEx将新的值写入,loadEx/storeEx操作是在同一个核上操作,不同的核无法操作同一共享内存。这样就保证了多核共享内存写数据的排他性。
如果访问的内存地址是本地可见,其他核不可见的,那么,也必须是loadEx/storeEX成对操作。
对于单核系统,不使用loadEx/storeEx方式。
上述描述的操作是硬件操作的过程,如果在loadEx核storeEx操作之间发生了中断,即loadEx完成后中断到来,且中断程序本地也去修改这个内存地址处的数据,或者在中断返回后调度了其他进程到当前核上来执行,且这个新调度来的进行也去修改这个内存地址处的数据或者其他核上的程序也去修改这个内存地址处的数据,会怎么样呢?。
这种情况下,如果多核共享内存数据被同一个Processor的中断程序或其他进程抢先写入,那么当被中断的程序继续执行loadEx时,由于global中清除了这个物理地址信息,那么当前的程序就无法写入,Rd为1.这时,需要程序处理一下,比如重新再执行loadEx/strEx.
我们用下面的图来清晰描述这个过程。
图1:同核下进程和中断程序修改同一块内存
图2:同核下不同进程修改同一块内存
图3:不同核下的不同进程修改同一块内存
linux kernel的原子写操作函数使用的就是这个LDREX/STREX指令对,下面是代码实现:
可见,atomic_add()/atomic_sub()需要判断strex指令的返回值tmp是否为0,不是0则跳转到标号1处再来一遍。
上面描述的是ARM7以及之后的多核ARM版本的实现,我们接下来看看ARM7之前的单核版本的实现。
单核版本的ARM对原子操作的实现采用禁中断的方式,中断禁了之后,中断不会发生,中断程序不会运行,没有了中断,依赖中断的调度程序也不会运行,即系统不会再调度其他进程执行了。这个时候直接操作内存。