原子操作
假定运行在两个CPU上的两个内核控制路径试图执行非原子操作同时“读-修改-写”同一存储器单元。首先,两个CPU都试图读同一单元,但是存储器仲裁器插手,只允许其中的一个访问而让另一个延迟。然而,当第一个读操作已经完成后,延迟的CPU从那个存储器单元正好读到同一个(旧)值。然后,两个CPU都试图向那个存储器单元写一新值,总线存储器访问再一次被存储器仲裁器串行化,最终,两个写操作都成功。但是,全局的结果是不对的,因为两个CPU写入同一(新)值。因此,两个交错的"读-修改-写"操作成了一个单独的操作。
避免由于“读-修改-写”指令引起竞争条件的最容易的办法,就是确保这样的操作在芯片级是原子的。任何一个这样的操作都必须一个单个指令执行,中间不能中断,且避免其他CPU访问同一存储单元。
在你编写C代码程序时,并不能保证编译器会为a++这样的操作使用一个原子指令。因此,Linux内核提供了一个专门的atomic_t类型和一些专门的函数和宏
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) 把i加到*v,如果结果为负,则返回1;否则,返回0
atomic_add_return(i,v) *v加i,返回*v的新值
atomic_sub_return(i,v) *v减i,返回*v的新值
#ifdef CONFIG_SMP #define LOCK "lock ; " #else #define LOCK "" #endif /** * atomic_add - add integer to atomic variable * @i: integer value to add * @v: pointer of type atomic_t * * Atomically adds @i to @v. */ static __inline__ void atomic_add(int i, atomic_t *v) { __asm__ __volatile__( LOCK "addl %1,%0" :"=m" (v->counter) :"ir" (i), "m" (v->counter)); }
在多核处理器系统中,每条指令都有一个lock字节的前缀。当控制单元检测到这个前缀时,就“锁定”内存总线,直到这条指令执行完为止。因此,当加锁的指令执行时,其他处理器不能访问这个内存单元。
Linux中的原子位处理函数
test_bit(nr,addr) 返回*addr的第nr位的值
set_bit(nr,addr) 设置*addr的第nr位的值
clear_bit(nr,addr) 清*addr的第nr位
change_bit(nr,addr) 转换*addr的第nr位
test_and_set_bit(nr,addr) 设置*addr的第nr位,并返回它的原值
test_and_clear_bit(nr,addr) 清*addr的第nr位,并返回它的原值
test_and_change_bit(nr,addr) 转换*addr的第nr位,并返回它的原值
atomic_clear_mask(mask,addr) 清除mask指定的*addr的所有位
atomic_set_mask(mask,addr) 设置mask指定的*addr的所有位
优化和内存屏障
当使用优化的编译器时,千万不要认为指令会严格按照它们在源码中出现的顺序执行。编译器可能重新安排汇编语言指令以使寄存器以最优的方式使用。
优化屏障(optimization barrier)原语保证编译程序不会混淆放在原语操作之前的汇编语言指令和放在原语操作之后的汇编语言指令。
在Linux中,优化屏障就是barrier()宏,它展开为asm volatile("":::"memory")。
指令asm告诉编译器程序要插入汇编语言片段。volatile关键字禁止编译器把asm指令与程序中的其他指令重新组合。memory关键字强制编译器假定RAM中的所有内存单元已经被汇编语言指令修改;因此,编译器不能使用存放在CPU寄存器中的内存单元的值来优化asm指令前的代码。
内存屏障(memory barrier)原语确保,在原语之后的操作开始执行之前,原语之前的操作已经完成。也即内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。
80x86处理器,下列种类的汇编语言是串行的,因为它们内存屏障的作用:
1、I/O端口进行操作的所有指令
2、有lock前缀的所有指令
3、写控制器、系统寄存器或调试寄存器的所有指令(如,cli和sti,用于修改eflags寄存器的IF标志的状态)
4、在Pentium 4微处理器中引入的汇编语言指令lfence、sfence和mfence
LFENCE Serializes load operations
SFENCE Serializes store operations
MFENCE Serializes load and store operations
它们分别有效地实现读内存屏障、写内存屏障和读-写内存屏障
Linux使用六个内存屏障原语。这些原语也被当作优化屏障,因为我们必须保证编译程序不在屏障前后移动汇编语言指令。
读内存屏障 仅仅作用于从内存读的指令
写内存屏障 仅仅作用于写内存的指令
内存屏障既用于多处理器系统,也用于单处理器系统。当内存屏障应该防止仅出现于多处理器系统上的竞争条件时,就使用smp_xxx()原语,在单处理器系统上,它们什么也不做。
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;addl $0,0(%%esp)汇编指令把0加到栈顶的内存单元,这条指令本身没有价值,但是,lock前缀使这条指令称为CPU的一个内存屏障。
Intel上的wmb()宏实际上更简单,因为它展开为barrier()。这是因为Intel处理器从来不对写内容访问重新排序,因此,没有必要在代码中插入一条串行化汇编指令。不过,这个宏禁止编译器重新组合指令。
在多核处理器上,在之前的原子操作中描述的所有原子操作都起到内存屏障的作用,因为他们使用了lock字节。