80x86指令:
- 进行0次或1次对齐内存访问的汇编指令是原子的;(如short地址必须是2的倍数、int的地址必须是4的倍数等)
- 读-修改-写之间没有其他处理器占用内存总线,则此序列操作是原子的;
- 操作码前缀是lock字节(0xf0)的“读-修改-写”汇编语言指令即使在多处理器系统中也是原子的。lock前缀在实际运行过程中,由控制单元检查,并当检查到该前缀时,将“锁定”内存总线,直到指令执行完毕。因此,当加锁的指令执行时,其他处理器不能访问该内存单元;
- 操作码前缀是rep(0xf2,0xf3)的汇编语言指令不是原子的,这条指令强行让控制单元多次重复执行相同指令。控制单元在执行新的循环之前要检查挂起的中断;
其中lock可作用指令范围如下:(转自百度知道:https://zhidao.baidu.com/question/1370295334412831499.html)
ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, XCHG
LOCK指令只有在目标操作数为内存地址时LOCK指令才会将该指令变为原子指令;如果目标操作数不为内存则会产生UD(Undefined Opcode,未定义的指令)错误。
另外值得注意的是,部分编译器会将LOCK指令合法的编译在非前面提到的指令前(例如:LOCK MOV [DATA],EAX),但是在运行程序时会同样产生UD错误。
题外话:XCHG的其中一个操作数为内存时会自动插入LOCK指令,使其所需要的周期变得很长。
在Linux中提供了atomic_t类型和一些函数与宏;这些函数和宏作用于atomic_t类型变量,并当做单独的、原子的汇编语言指令来使用。在多处理器系统中,每条这样的指令都有一个lock前缀;
Linux原子操作如下:(内存屏障)
函数 | 说明 |
atomic_read(v) | 返回*v |
atomic_set(v, i) | 把*v设置为i |
atomic_add(i, v) | *v+=i |
atomic_sub(i, v) | *v-=i |
atomic_sub_and_test(i, v) | *v-=i,如果结果为0,则返回1;否则返回0 |
atomic_inc(v) | *v+=1 |
atomic_dec(v) | *v-=1 |
atomic_dec_and_test(v) | *v-=1,如果结果为0,则返回1;否则返回0 |
atomic_inc_and_test(v) | *v+=1,如果结果为0,则返回1;否则返回0 |
atomic_add_negative(i, v) | *v+=i,如果结果为负,则返回1;否则返回0 |
atomic_inc_return(i, v) | *v+=1,返回*v新值 |
atomic_dec_return(i, v) | *v-=1,返回*v新值 |
atomic_add_return(i, v) | *v+=i,返回*v新值 |
atomic_sub_return(i, v) | *v-=i,返回*v新值 |
内存访问重排序,编译器优化可能将源代码中的指令顺序重排;
asm volatile("":::"memory");(内存屏障原语);在原语之后的操作执行之前,原语之前的操作已经执行完毕;告诉控制器,我对内存做了改变,不要优化
volatile字段禁止编译器把asm指令与程序中的其他指令重新组合。memory关键字强调编译器假定RAM中的所有内存单元已经被汇编语言指令修改。
Linux中的内存屏障:
mb() 适用于MP和UP的内存屏障;
rmb() 适用于MP和UP的读内存屏障;
wmb() 适用于MP和UP的写内存屏障;
smp_mb() 仅适用于MP的内存屏障
smp_rmb() 仅适用于MP的读内存屏障
smp_wmb() 仅适用于MP的写内存屏障
在80x86微处理器上,如果CPU支持lfence汇编指令,就把rmb展开为asm volatile("lfence"),否则就展开为asm volatile("lock; addl $0, 0(%%esp)":::"memory");lock前缀使这条指令成为优化屏障;
内存屏障或lock应用十分广泛,如spinlock, rw_lock等;