关于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来操作该变量即可。