linux原子变量
‘壹’ 新手求教linux下的原子操作该怎么写
linux中关于原子操作
2016年08月02日
- 一.整型原子操作定义于#include<asm/atomic.h>分为 定义,获取,加减,测试,返回。void atomic_set(atomic_t *v,int i); //设置原子变量v的值为iatomic_t v = ATOMIC_INIT(0); //定义原子变量v,并初始化为0;atomic_read(atomic_t* v); //返回原子变量v的值;void atomic_add(int i, atomic_t* v); //原子变量v增加i;void atomic_sub(int i, atomic_t* v); void atomic_inc(atomic_t* v); //原子变量增加1;void atomic_dec(atomic_t* v);int atomic_inc_and_test(atomic_t* v); //先自增1,然后测试其值是否为0,若为0,则返回true,否则返回false;int atomic_dec_and_test(atomic_t* v); int atomic_sub_and_test(int i, atomic_t* v); //先减i,然后测试其值是否为0,若为0,则返回true,否则返回false;注意:只有自加,没有加操作int atomic_add_return(int i, atomic_t* v); //v的值加i后返回新的值;int atomic_sub_return(int i, atomic_t* v); int atomic_inc_return(atomic_t* v); //v的值自增1后返回新的值;int atomic_dec_return(atomic_t* v);二.位原子操作定义于#include<asm/bitops.h>分为 设置,清除,改变,测试void set_bit(int nr, volatile void* addr); //设置地址addr的第nr位,所谓设置位,就是把位写为1;void clear_bit(int nr, volatile void* addr); //清除地址addr的第nr位,所谓清除位,就是把位写为0;void change_bit(int nr, volatile void* addr); //把地址addr的第nr位反转;int test_bit(int nr, volatile void* addr); //返回地址addr的第nr位;int test_and_set_bit(int nr, volatile void* addr);//测试并设置位;若addr的第nr位非0,则返回true; 若addr的第nr位为0,则返回false;int test_and_clear_bit(int nr, volatile void* addr);//测试并清除位;int test_and_change_bit(int nr, volatile void* addr);//测试并反转位;上述操作等同于先执行test_bit(nr,voidaddr)然后在执行xxx_bit(nr,voidaddr)
- 举个简单例子:为了实现设备只能被一个进程打开,从而避免竞态的出现static atomic_t scull_available = ATOMIC_INIT(1);//init atomic在scull_open 函数和scull_close函数中:int scull_open(struct inode *inode, struct file *filp){ struct scull_dev *dev; // device information dev = container_of(inode->i_cdev, struct scull_dev, cdev); filp->private_data = dev;// for other methods if(!atomic_dec_and_test(&scull_available)){ atomic_inc(&scull_available); return -EBUSY; } return 0; // success}int scull_release(struct inode *inode, struct file *filp){ atomic_inc(&scull_available); return 0;}
#if__LINUX_ARM_ARCH__>=6
......(通过ldrex/strex指令的汇编实现)
#else/*ARM_ARCH_6*/
#ifdef CONFIG_SMP
#errorSMPnotsupportedonpre-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>
/*
*ARMv6 UP 和 SMP 安全原子操作。 我们是用独占载入和
*独占存储来保证这些操作的原子性。我们可能会通过循环
*来保证成功更新变量。
*/
static inline void atomic_add(inti,atomic_t*v)
{
unsigned long tmp;
intresult;
__asm__ __volatile__("@ atomic_add "
"1: ldrex %0, [%3] "
" add %0, %0, %4 "
" strex %1, %0, [%3] "
" teq %1, #0 "
" bne 1b"
:"=&r"(result),"=&r"(tmp),"+Qo"(v->counter)
:"r"(&v->counter),"Ir"(i)
:"cc");
}
A:将该物理地址标记为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。
B:标记此物理地址为CPU1独占访问,并清除CPU1对其他任何物理地址的任何独占访问标记。
C:再次标记此物理地址为CPU0独占访问,并清除CPU0对其他任何物理地址的任何独占访问标记。
D:已被标记为CPU0独占访问,进行存储并清除独占访问标记,并返回0(操作成功)。
E:没有标记为CPU1独占访问,不会进行存储,并返回1(操作失败)。
F:没有标记为CPU0独占访问,不会进行存储,并返回1(操作失败)。
原子操作:就是在执行某一操作时不被打断。
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版本以下的实现。
该文件的主要结构如下:
这样的安排是依据ARM核心指令集版本的实现来做的:
(1)在ARMv6以上(含v6)构架有了多核的CPU,为了在多核之间同步数据和控制并发,ARM在内存访问上增加了独占监测(Exclusive monitors)机制(一种简单的状态机),并增加了相关的ldrex/strex指令。请先阅读以下参考资料(关键在于理解local monitor和Global monitor):
1.2.2.Exclusive monitors
4.2.12.LDREX和STREX
(2)对于ARMv6以前的构架不可能有多核CPU,所以对于变量的原子访问只需要关闭本CPU中断即可保证原子性。
对于(2),非常好理解。
但是(1)情况,我还是要通过源码的分析才认同这种代码,以下我仅仅分析最具有代表性的atomic_add源码,其他的API原理都一样。如果读者还不熟悉C内嵌汇编的格式,请参考《ARM GCC内嵌汇编手册》
二、内核对于ARM构架的atomic_add源码分析
源码分析:
注意:根据内联汇编的语法,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操作了同一个原子变量。则情况如下图所示:
虽然对于人来说,这种情况比较BT。但是在飞速运行的CPU来说,BT的事情随时都可能发生。
当然还有其他许多复杂的可能,也可以通过ldrex/strex指令的机制分析出来。从上面列举的分析中,我们可以看出:ldrex/strex可以保证在任何情况下(包括被中断)的访问原子性。所以内核中ARM构架中的原子操作是可以信任的。
‘贰’ linux 2.6内核有关原子变量的定义问题
原子意味着不可分割,所谓原子操作就是对变量的读写不能被打断的操作。
举个简单点儿的例子:
1. 假如在一个i386体系架构上;
2. 如果有一个进程要将一个int型的变量改成0x12345678;
3. 另一个进程也希望把这同一个变量改成0x87654321。
4. 如果这个变量的地址没有4字节对齐,那么cpu要改写它的值的话需要两次总线操作。
那么(假设下面的场景,即下列事件先后发生):
1. 第一个进程刚把高字节写入(x=0x1234xxxx)内存(xxxx表示不确定);
2. 第二进程就抢占了第一个进程的运行,把第一个进程改了一半的变量改成0x87654321。(当然他也需要两次总线操作,但我们假设他的优先级比第一个进程高)
3. 第二个进程结束运行后,第一个进程又得到了调度,它并不知道自己对变量的操作被另一个进程打断过,所以他会继续更改变量的低字节。
所以,最后这个变量的值就是0x87655678,显然这是两个进程都不想要得到的结果。通过上面的分析你应该知道问题的关键就在于对存储空间的访问被打断了造成的。所以在内核中定义了一系列的原子操作来保证对变量的操作是“原子”的。这种互斥不是高级语言能实现的,必须用汇编,而且依赖于体系架构。对i386来说就是在读写变量的时候先把总线锁住,你可以仔细看看ATOMIC_INIT这个宏的定义。
‘叁’ 原子操作的Linux
-----------------------------------------------------------
原子操作大部分使用汇编语言实现,因为c语言并不能实现这样的操作。
* 在x86的原子操作实现代码中,定义了LOCK宏,这个宏可以放在随后的内联汇编指令之前。如果是SMP,LOCK宏被扩展为lock指令;否则被定义为空 -- 单CPU无需防止其它CPU的干扰,锁内存总线完全是在浪费时间。
#ifdef CONFIG_SMP
#define LOCK "lock ; "
#else
#define LOCK ""
#endif
* typedef struct { volatile int counter; } atomic_t;
在所有支持的体系结构上原子类型atomic_t都保存一个int值。在x86的某些处理器上,由于工作方式的原因,原子类型能够保证的可用范围只有24位。volatile是一个类型描述符,要求编译器不要对其描述的对象作优化处理,对它的读写都需要从内存中访问。
* #define ATOMIC_INIT(i) { (i) }
用于在定义原子变量时,初始化为指定的值。如:
static atomic_t count = ATOMIC_INIT⑴;
* static __inline__ void atomic_add(int i,atomic_t *v)
将v指向的原子变量加上i。该函数不关心原子变量的新值,返回void类型。
‘肆’ linux 为什么要使用原子操作
有些操作必须要具有原子性,否则会出现问题.
就拿一个变量自增来说,比如
i++,一般经历了3步
从内存到寄存器
寄存器的值+1
从寄存器到内存
,如果有这样一个语句:
inti=0;
i++;
i++;
如果不能保证原子性,在多核的系统中,这两个i++可能同时运行,这样就可能造成这个语句的结果最后,i等于1.
在操作系统里面,必须要保证一些操作一次性完成,不被打断
‘伍’ Linux中的原子变量如何取地址,如何给定义的原子变量赋指定的地址
首先请确定你要做原子操作的对象是谁?是一个地址,还是地址指向的数据?
如果把数据做为原子对象,直接对数据进行原子操作即可,数据的指针不用做原子操作。