天天看点

关于int全区变量读写的原子性

关于int全区变量读写的原子性

    关于int变量的读写是否原子性网上有很多讨论,貌似不同平台不同,这里自己做实现在arm9平台测试。 这里要注意原子性并非指一条汇编才原子,实际上即使一次赋值编译成几条汇编依然可以是原子的,只要保证该内存不产生中间值,只有原值和目标值两种状态则就是原子的。对一个int变量赋值是否要进入临界区呢?

以下基于arm920t cpu Sourcery G++ arm-none-eabi-gcc 编译器测试int原子性:

1、正常四字节对齐的int变量和非四字节对齐的char变量

typedef struct {
    char c1;
    char c2;
// atomic_t i1;
    int i1;
} str_t;
volatile str_t Gstr ;
int main(void)
{
    Gstr.c1 = 1;
    Gstr.c2 = 2;
    Gstr.i1 = 0x12345678;
    while (1);
    return 0;
}
           
int main(void)
{
    8000: e52db004 push {fp} ; (str fp, [sp, #-4]!)
    8004: e28db000 add fp, sp, #0
    Gstr.c1 = 1;
    8008: e59f1030 ldr r1, [pc, #48] ; 8040 <main+0x40>
    800c: e3a03001 mov r3, #1
    8010: e1a02003 mov r2, r3
    8014: e1a03001 mov r3, r1
    8018: e5c32000 strb r2, [r3]
    Gstr.c2 = 2;
    801c: e59f101c ldr r1, [pc, #28] ; 8040 <main+0x40>
    8020: e3a03002 mov r3, #2
    8024: e1a02003 mov r2, r3
    8028: e1a03001 mov r3, r1
    802c: e5c32001 strb r2, [r3, #1]
    Gstr.i1 = 0x12345678;
    8030: e59f3008 ldr r3, [pc, #8] ; 8040 <main+0x40>
    8034: e59f2008 ldr r2, [pc, #8] ; 8044 <main+0x44>
    8038: e5832004 str r2, [r3, #4]
    while (1);
    803c: eafffffe b 803c <main+0x3c>
    8040: 00010060 .word 0x00010060
    8044: 12345678 .word 0x12345678
           

从以上汇编看,在对齐的int写操作是原子的(   8038: e5832004 str r2, [r3, #4] ),仅一条str赋值指令。 char型可以通过strb对字节操作的指令一次完成,无论是否对齐都是单指令完成,故也是原子的。(strb的内存操作可以以字节对齐)

2、非四字节对齐的int型变量赋值

</pre><pre name="code" class="cpp">typedef struct {
    char c1;
    int i1;
} __attribute__((__packed__)) str_t;

volatile str_t Gstr ;

int main(void)
{
    Gstr.c1 = 1;
    Gstr.i1 = 0x12345678;
    while (1);
    return 0;
}
    Gstr.i1 = 0x12345678;
    801c:	e59f3024 ldr	r3, [pc, #36]	; 8048 <main+0x48>
    8020:	e5932000 ldr	r2, [r3]
    8024:	e20210ff and	r1, r2, #255	; 0xff
    8028:	e59f201c ldr	r2, [pc, #28]	; 804c <main+0x4c>
    802c:	e1812002 orr	r2, r1, r2
    8030:	e5832000 str	r2, [r3]
    8034:	e5932004 ldr	r2, [r3, #4]
    8038:	e3c220ff bic	r2, r2, #255	; 0xff
    803c:	e3822012 orr	r2, r2, #18
    8040:	e5832004 str	r2, [r3, #4]
    while (1);
    8044:	eafffffe b	8044 <main+0x44>
    8048:	00010068 .word	0x00010068
    804c:	34567800 .word	0x34567800
           

可见当int变量非四字节对齐时,将无法单指令完成所有赋值,分两步赋值,这样就产生了中间值,即非原子。 ldr str指令要求操作地址是4字节对齐的。 (PS:从一个非对齐的int的赋值看,转成汇编需要这么多的操作, 所以平时一些要求高效率的代码要考虑内存对齐问题,默认编译器都是会定义对齐内存的 )

3、非四字节对齐int变量读取。

typedef struct {
    char c1;
    int i1;
} __attribute__((__packed__)) str_t;
volatile str_t Gstr ;
int rd;
int main(void)
{
    rd = Gstr.i1;
    while (1);
    return 0;
}
    rd = Gstr.i1;
    8008: e59f3024 ldr r3, [pc, #36] ; 8034 <main+0x34>
    800c: e5932000 ldr r2, [r3]
    8010: e1a02422 lsr r2, r2, #8
    8014: e5933004 ldr r3, [r3, #4]
    8018: e20330ff and r3, r3, #255 ; 0xff
    801c: e1a03c03 lsl r3, r3, #24
    8020: e1833002 orr r3, r3, r2
    8024: e1a02003 mov r2, r3
    8028: e59f3008 ldr r3, [pc, #8] ; 8038 <main+0x38>
    802c: e5832000 str r2, [r3]
    while (1);
    8030: eafffffe b 8030 <main+0x30>
    8034: 00010054 .word 0x00010054
    8038: 0001005c .word 0x0001005c
           

从编译结果看, 非对齐int读取也是非原子的,i1被分为两部分,i1的内存被访问两次,这样中间有可能被修改:  800c: e5932000 ldr r2, [r3]      8014: e5933004 ldr r3, [r3, #4]    读r3和r3+4 都是i1的内存,相当于分两次访问同一变量就有可能被修改内存。

  总结:int类型在对齐时读写是原子的(编译默认是对齐的),在非对齐时读写不是原子的。     可参考linux中的atomic.h查看原子操作的实现。可以自己实现一个atomic函数集,当希望一个变量的操作原子性时,使用atomic来操作该变量即可。