Linux内核原子操作 原理
2013-06-28 16:03
169 查看
摘自:http://ilinuxkernel.com/?p=1158
本文基linux内核版本 2.6.32-131.17.1.el6.i686源码。
1、原子读、赋值
00016: /**
00017: *atomic_read-readatomicvariable
00018: * @v:pointeroftypeatomic_t
00019: *
00020: *Atomicallyreadsthevalueof @v.
00021: */
00022: staticinlineint atomic_read(constatomic_t*v)
00023: {
00024: returnv->counter;
00025: }
00026:
00027: /**
00028: *atomic_set-setatomicvariable
00029: * @v:pointeroftypeatomic_t
00030: * @i:requiredvalue
00031: *
00032: *Atomicallysetsthevalueof @vto @i.
00033: */
00034: staticinlinevoid atomic_set(atomic_t*v,inti)
00035: {
00036: v->counter= i;
00037: }
00038:
2、原子加减
00039: /**
00040: *atomic_add-addintegertoatomicvariable
00041: * @i:integervaluetoadd
00042: * @v:pointeroftypeatomic_t
00043: *
00044: *Atomicallyadds @ito @v.
00045: */
00046: staticinlinevoid atomic_add(inti,atomic_t*v)
00047: {
00048: asmvolatile(LOCK_PREFIX“addl%1,%0″
00049: :“+m” (v->counter)
00050: :“ir”(i));
00051: }
00052:
00053: /**
00054: *atomic_sub-subtractintegerfromatomicvariable
00055: * @i:integervaluetosubtract
00056: * @v:pointeroftypeatomic_t
00057: *
00058: *Atomicallysubtracts @ifrom @v.
00059: */
00060: staticinlinevoid atomic_sub(inti,atomic_t*v)
00061: {
00062: asmvolatile(LOCK_PREFIX“subl%1,%0″
00063: :“+m” (v->counter)
00064: :“ir”(i));
00065: }
00066:
3、原子加减1
00085:
00086: /**
00087: *atomic_inc-incrementatomicvariable
00088: * @v:pointeroftypeatomic_t
00089: *
00090: *Atomicallyincrements @vby1.
00091: */
00092: staticinlinevoid atomic_inc(atomic_t*v)
00093: {
00094: asmvolatile(LOCK_PREFIX“incl%0″
00095: :“+m” (v->counter));
00096: }
00097:
00098: /**
00099: *atomic_dec-decrementatomicvariable
00100: * @v:pointeroftypeatomic_t
00101: *
00102: *Atomicallydecrements @vby1.
00103: */
00104: staticinlinevoid atomic_dec(atomic_t*v)
00105: {
00106: asmvolatile(LOCK_PREFIX“decl%0″
00107: :“+m” (v->counter));
00108: }
00109:
5、LOCK_PREFIX理解
在原子加减操作中,我们可以发现实现都基于关键指令LOCK_PREFIX。该宏定义在文件arch/x86/include/asm/alternative.h中。
00030: #ifdefCONFIG_SMP
00031: #defineLOCK_PREFIX\
00032: “.section.smp_locks,\”a\”\n”\
00033: _ASM_ALIGN“\n” \
00034: _ASM_PTR “661f\n”/*address*/ \
00035: “.previous\n” \
00036: “661:\n\tlock;”
00037:
00038: #else/*!CONFIG_SMP*/
00039: #defineLOCK_PREFIX“”
00040: #endif
也就是LOCK_PREFIX只有在SMP系统中才有效。在单CPU(UniProcessor)中,能够在单条指令中完成的操作都可以认为是“原子操作”,因为中断只发生在指令边缘。但在多处理器结构中(Symmetric Multi-Processor)就不同了,由于系统中有多个处理器独立运行,即使能在单条指令中完成的操作也有可能受到干扰。在X86平台生,CPU提供了在指令执行期间对总线加锁的手段。CPU上有一根引线#HLOCK pin连到北桥,如果汇编语言的程序中在一条指令前面加上前缀“LOCK”,经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK
pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。
LOCK_PREFIX宏扩展开后,就是在语句前加上lock前缀。
atomic_add():
/root/rpmbuild/BUILD/kernel-2.6.32-131.17.1.el6/linux-2.6.32-131.17.1.el6.i686/arch/x86/include/asm/atomic_32.h:48
*
* Atomically adds @i to @v.
*/
static inline void atomic_add(int i, atomic_t *v)
{
asm volatile(LOCK_PREFIX “addl %1,%0″
c041c21b: 8b 44 24 20 mov 0×20(%esp),%eax
c041c21f: f0 01 05 1c 8f af c0 lock add %eax,0xc0af8f1c
atomic_add_return():
/root/rpmbuild/BUILD/kernel-2.6.32-131.17.1.el6/linux-2.6.32-131.17.1.el6.i686/arch/x86/include/asm/atomic_32.h:182
if (unlikely(boot_cpu_data.x86 <= 3))
goto no_xadd;
#endif
/* Modern 486+ processor */
__i = i;
asm volatile(LOCK_PREFIX “xaddl %0, %1″
c041c226: bf 01 00 00 00 mov $0×1,%edi
c041c22b: f0 0f c1 3d 18 8f af lock xadd %edi,0xc0af8f18
c041c232: c0
6、其他原子操作
减操作后判断是否为0:atomic_sub_and_test()。
00067: /**
00068: *atomic_sub_and_test-subtractvaluefromvariableandtestresult
00069: * @i:integervaluetosubtract
00070: * @v:pointeroftypeatomic_t
00071: *
00072: *Atomicallysubtracts @ifrom @vandreturns
00073: *trueiftheresultiszero,orfalseforall
00074: *othercases.
00075: */
00076: staticinlineint atomic_sub_and_test(inti,atomic_t*v)
00077: {
00078: unsignedchar c;
00079:
00080: asmvolatile(LOCK_PREFIX“subl%2,%0;sete%1″
00081: :“+m” (v->counter),“=qm”(c)
00082: :“ir”(i):“memory”);
00083: returnc;
00084: }
递减后判断是否为0:atomic_dec_and_test()。
00110: /**
00111: *atomic_dec_and_test-decrementandtest
00113: *
00114: *Atomicallydecrements @vby1and
00115: *returnstrueiftheresultis0,orfalseforallother
00116: *cases.
00117: */
00118: staticinlineint atomic_dec_and_test(atomic_t*v)
00119: {
00120: unsignedchar c;
00121:
00122: asmvolatile(LOCK_PREFIX“decl%0;sete%1″
00123: :“+m” (v->counter),“=qm”(c)
00124: :: “memory”);
00125: returnc !=0;
00126: }
00127:
自增后判断是否为0:atomic_inc_and_test()。
00128: /**
00129: *atomic_inc_and_test-incrementandtest
00130: * @v:pointeroftypeatomic_t
00131: *
00132: *Atomicallyincrements @vby1
00133: *andreturnstrueiftheresultiszero,orfalseforall
00134: *othercases.
00135: */
00136: staticinlineint atomic_inc_and_test(atomic_t*v)
00137: {
00138: unsignedchar c;
00139:
00140: asmvolatile(LOCK_PREFIX“incl%0;sete%1″
00141: :“+m” (v->counter),“=qm”(c)
00142: :: “memory”);
00143: returnc !=0;
00144: }
00145:
相加后是否为负值 :atomic_add_negative()。
00146: /**
00147: *atomic_add_negative-addandtestifnegative
00148: * @v:pointeroftypeatomic_t
00149: * @i:integervaluetoadd
00150: *
00151: *Atomicallyadds @ito @vandreturnstrue
00152: *iftheresultisnegative,orfalsewhen
00153: *resultisgreaterthanorequaltozero.
00154: */
00155: staticinlineint atomic_add_negative(inti,atomic_t*v)
00156: {
00157: unsignedchar c;
00158:
00159: asmvolatile(LOCK_PREFIX“addl%2,%0;sets%1″
00160: :“+m” (v->counter),“=qm”(c)
00161: :“ir”(i):“memory”);
00162: returnc;
00163: }
00164:
相加后返回: atomic_add_return()。
00165: /**
00166: *atomic_add_return-addintegerandreturn
00168: * @i:integervaluetoadd
00169: *
00170: *Atomicallyadds @ito @vandreturns @i+ @v
00171: */
00172: staticinlineint atomic_add_return(inti,atomic_t*v)
00173: {
00174: int__i;
00175: #ifdefCONFIG_M386
00176: unsignedlongflags;
00177: if(unlikely(boot_cpu_data .x86<= 3))
00178: goto¯no_xadd;
00179: #endif
00180: /*Modern486+processor*/
00181: __i =i;
00182: asmvolatile(LOCK_PREFIX“xaddl%0,%1″
00183: :“+r”(i),“+m” (v->counter)
00184: :: “memory”);
00185: returni+__i;
00186:
00187: #ifdefCONFIG_M386
00188: no_xadd:/*Legacy386processor*/
00189: local_irq_save(flags);
00190: __i =atomic_read(v);
00191: atomic_set(v,i+__i);
00192: local_irq_restore(flags);
00193: returni+__i;
00194: #endif
00195: }? endatomic_add_return?
00196:
相减后返回:atomic_sub_return()。
00197: /**
00198: *atomic_sub_return-subtractintegerandreturn
00199: * @v:pointeroftypeatomic_t
00200: * @i:integervaluetosubtract
00201: *
00202: *Atomicallysubtracts @ifrom @vandreturns @v- @i
00203: */
00204: staticinlineint atomic_sub_return(inti,atomic_t*v)
00205: {
00206: returnatomic_add_return(-i,v);
00207: }
摘自:http://www.360doc.com/content/11/1014/00/1317564_156005675.shtml
所谓原子操作,就是“不可中断的一个或一系列操作”。
硬件级的原子操作:在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是“原子操作”,因为中断只发生在指令边缘。在多处理器结构中(Symmetric
Multi-Processor)就不同了,由于系统中有多个处理器独立运行,即使能在单条指令中完成的操作也有可能受到干扰。在X86平台生,CPU提供了在指令执行期间对总线加锁的手段。CPU上有一根引线#HLOCK pin连到北桥,如果汇编语言的程序中在一条指令前面加上前缀"LOCK",经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。对于其他平台的CPU,实现各不相同,有的是通过关中断来实现原子操作(sparc),有的通过CMPXCHG系列的指令来实现原子操作(IA64)。本文主要探讨X86平台下原子操作的实现。
软件级别的原子操作:软件级别的原子操作实现依赖于硬件原子操作的支持。
Linux内核提供了两组原子操作接口:一组是针对整数进行操作;另一组是针对单独的位进行操作。
1、原子整数操作
原子操作通常针对int或bit类型的数据,但是Linux并不能直接对int进行原子操作,而只能通过atomic_t的数据结构来进行。目前了解到的原因有两个。
一是在老的Linux版本,atomic_t实际只有24位长,低8位用来做锁。这是由于Linux是一个跨平台的实现,可以运行在多种 CPU上,有些类型的CPU比如SPARC并没有原生的atomic指令支持,所以只能在32位int使用8位来做同步锁,避免多个线程同时访问。(最新版SPARC实现已经突破此限制)。原子整数操作最常见的用途就是实现计数器。常见的用法是:
atomic_t use_cnt;
atomic_set(&use_cnt, 2);
atomic_add(4, &use_cnt);
atomic_inc(use_cnt);
在X86平台上,atomic_t定义如下:
view
plain
typedef struct {
int counter;
} atomic_t;
下面选取atomic_add来进行分析:
view
plain
static inline void atomic_add(int i, atomic_t *v)
{
asm volatile(LOCK_PREFIX "addl %1,%0"
: "+m" (v->counter)
: "ir" (i));
}
可以看到,atomic_add使用了gcc提供的内嵌汇编来实现,是用一个addl指令来实现增加操作。重点看一下LOCK_PREFIX宏,它就是上文提到的锁总线操作,也就是它保证了操作的原子性。LOCK_PREFIX定义如下:
view
plain
#define LOCK_PREFIX \
".section .smp_locks,\"a\"\n" \
" .align 4\n" \
" .long 661f\n" /* address */ \
".previous\n" \
"661:\n\tlock; "
展开后变成:
view
plain
.section .smp_locks,"a"
.align 4
.long 661f
.previous
661:
lock;
逐条解释如下:
.section .smp_locks,"a"
下面的代码生成到 .smp_locks 段里,属性为"a", allocatable
.align 4
四字节对齐
.long 661f
生成一个整数,值为下面的 661 标号的实际地址,f 表示向前引用,如果 661 标号出现
在前面,要写 661b。
.previous
代码生成恢复到原来的段,也就是 .text
661:
数字标号是局部标号,5.3 Symbol Names
lock;
开始生成指令,lock 前缀
这段代码汇编后,在 .text 段生成一条 lock 指令前缀 0xf0,在 .smp_locks 段生成四个字节的 lock 前缀的地址,链接的时候,所有的.smp_locks 段合并起来,形成一个所有 lock 指令地址的数组,这样统计 .smp_locks 段就能知道代码里有多少个加锁的指令被生成,猜测是为了调试目的。
搜索了一下,找到了相关引用处,当一个内核模块被加载时,会调用module_finalize函数:
view
plain
int module_finalize(const Elf_Ehdr *hdr,
const Elf_Shdr *sechdrs,
struct module *me)
{
const Elf_Shdr *s, *text = NULL, *alt = NULL, *locks = NULL,
*para = NULL;
char *secstrings = (void *)hdr + sechdrs[hdr->e_shstrndx].sh_offset;
for (s = sechdrs; s < sechdrs + hdr->e_shnum; s++) {
if (!strcmp(".text", secstrings + s->sh_name))
text = s;
if (!strcmp(".altinstructions", secstrings + s->sh_name))
alt = s;
if (!strcmp(".smp_locks", secstrings + s->sh_name))
locks= s;
if (!strcmp(".parainstructions", secstrings + s->sh_name))
para = s;
}
if (alt) {
/* patch .altinstructions */
void *aseg = (void *)alt->sh_addr;
apply_alternatives(aseg, aseg + alt->sh_size);
}
if (locks && text) {
void *lseg = (void *)locks->sh_addr;
void *tseg = (void *)text->sh_addr;
alternatives_smp_module_add(me, me->name,
lseg, lseg + locks->sh_size,
tseg, tseg + text->sh_size);
}
if (para) {
void *pseg = (void *)para->sh_addr;
apply_paravirt(pseg, pseg + para->sh_size);
}
return module_bug_finalize(hdr, sechdrs, me);
}
上面的代码说,如果模块有 .text
和 .smp_locks 段,就调这个来处理,做什么呢?
view
plain
void alternatives_smp_module_add(struct module *mod, char *name,
void *locks, void *locks_end,
void *text, void *text_end)
{
struct smp_alt_module *smp;
if (noreplace_smp)
return;
if (smp_alt_once) {
if (boot_cpu_has(X86_FEATURE_UP))
alternatives_smp_unlock(locks, locks_end,
text, text_end);
return;
}
........//省略无关代码
}
上面的代码说,如果是单处理器(UP),就调这个:
view
plain
static void alternatives_smp_unlock(u8 **start, u8 **end, u8 *text, u8 *text_end)
{
u8 **ptr;
char insn[1];
if (noreplace_smp)
return;
add_nops(insn, 1);
for (ptr = start; ptr < end; ptr++) {
if (*ptr < text)
continue;
if (*ptr > text_end)
continue;
text_poke(*ptr, insn, 1);
};
}
看到这里就能明白,这是内核配置了 smp,但是实际运行到单处理器上时,通过运行期间打补丁,根据 .smp_locks 里的记录,把 lock 指令前缀替换成 nop 以消除指令加锁的开销,这个优化真是极致了……,可能考虑很多用户直接使用的是配置支持 SMP 编译好的内核而特地对
x86/x64 做的这个优化。
2、原子位操作的实现
编写代码时,以如下的方式进行操作
unsigned long word = 0;
set_bit(0, &word); /*第0位被设置*/
set_bit(1, &word); /*第1位被设置*/
clear_bit(1, &word); /*第1位被清空*/
change_bit(0, &word); /*翻转第0位*/
为什么关注原子操作?
1)在确认一个操作是原子的情况下,多线程环境里面,我们可以避免仅仅为保护这个操作在外围加上性能开销昂贵的锁。
2)借助于原子操作,我们可以实现互斥锁。
3)借助于互斥锁,我们可以把一些列操作变为原子操作。
我们重点关注一下以下两个函数的实现:
view
plain
/**
* clear_bit - Clears a bit in memory
* @nr: Bit to clear
* @addr: Address to start counting from
*
* clear_bit() is atomic and may not be reordered. However, it does
* not contain a memory barrier, so if it is used for locking purposes,
* you should call smp_mb__before_clear_bit() and/or smp_mb__after_clear_bit()
* in order to ensure changes are visible on other processors.
*/
static inline void clear_bit(int nr, volatile void *addr)
{
asm volatile(LOCK_PREFIX "btr %1,%0" : ADDR : "Ir" (nr));
}
view
plain
/*
* clear_bit_unlock - Clears a bit in memory
* @nr: Bit to clear
* @addr: Address to start counting from
*
* clear_bit() is atomic and implies release semantics before the memory
* operation. It can be used for an unlock.
*/
static inline void clear_bit_unlock(unsigned nr, volatile void *addr)
{
barrier();
clear_bit(nr, addr);
}
第一个clear_bit函数比较好理解,和上面atomic系列的函数实现类似。但是注意到clear_bit_unlock函数中多了一个barrier函数,这是什么操作呢?
这就是有名的“内存屏障“或”内存栅栏“操作,先来补充一下这方面的知识。
可以看一下barrier的定义:
view
plain
#define barrier() __asm__ __volatile__("": : :"memory")
解释一下:__volatitle__是防止编译器移动该指令的位置或者把它优化掉。"memory",是提示编译器该指令对内存修改,防止使用某个寄存器中已经load 的内存的值。lock 前缀是让cpu 的执行下一行指令之前,保证以前的指令都被正确执行。
事实上,不止barrier,还有一个mb系列的函数也起着内存屏障的功能:
view
plain
#include <asm/system.h>
"void rmb(void);"
"void wmb(void);"
"void mb(void);"
这些函数在已编译的指令流中插入硬件内存屏障,具体的插入方法是平台相关的。rmb(读内存屏障)保证了屏障之前的读操作一定会在后来的读操作执行之前完成。wmb保证写操作不会乱序,mb 指令保证了两者都不会。这些函数都是 barrier 函数的超集。解释一下:编译器或现在的处理器常会自作聪明地对指令序列进行一些处理,比如数据缓存,读写指令乱序执行等等。如果优化对象是普通内存,那么一般会提升性能而且不会产生逻辑错误。但如果对I/O 操作进行类似优化很可能造成致命错误。所以要使用内存屏障,以强制该语句前后的指令以正确的次序完成。
其实在指令序列中放一个wmb 的效果是使得指令执行到该处时,把所有缓存的数据写到该写的地方,同时使得wmb 前面的写指令一定会在wmb后面 的写指令之前执行。
回到上面的函数,当clear_bit函数不用于实现锁的目的时,不用给它加上内存屏障(我的理解:不管是不是读到最新的数据,这一位就是要清零,不管加不加内存屏障,结果都是一样的);而当用于实现锁的目的时,必须使用clear_bit_unlock函数,其实现中使用了内存屏障,以此来确保此处的修改能在其他CPU上看到(我的理解:加锁操作就是为了在多个CPU间进行同步的目的,所以要避免寄存器优化,其他CPU每次都读内存这样才能看到最新的变化,这块不是太明白)。这种操作也叫做serialization,即在执行这条指令前,CPU必须要完成前面所有对memory的访问指令(read
and write),这样是为了避免编译器进行某些优化。
同样使用serialization操作的还有test_and_set_bit函数:
view
plain
/**
* test_and_set_bit - Set a bit and return its old value
* @nr: Bit to set
* @addr: Address to count from
*
* This operation is atomic and cannot be reordered.
* It also implies a memory barrier.
*/
static inline int test_and_set_bit(int nr, volatile void *addr)
{
int oldbit;
asm volatile(LOCK_PREFIX "bts %2,%1\n\t"
"sbb %0,%0" : "=r" (oldbit), ADDR : "Ir" (nr) : "memory");
return oldbit;
}
解释一下:
1)memory 强制gcc 编译器假设RAM 所有内存单元均被汇编指令修改,这样cpu 中的registers 和cache 中已缓存的内存单元中的数据将作废。cpu 将不得不在需要的时候重新读取内存中的数据。这就阻止了cpu 又将registers,cache 中的数据用于去优化指令,而避免去访问内存。
2)sbb $0,0(%%esp)表示将数值0 减到esp 寄存器中,而该寄存器指向栈顶的内存单元。减去一个0,esp 寄存器的数值依然不变。即这是一条无用的汇编指令。在此利用这条无价值的汇编指令来配合lock
指令,在__asm__,__volatile__,memory 的作用下,用作cpu 的内存屏障。
这种写法和前面的clear_bit_unlock中先写一个barrier函数,再写一个正常内嵌汇编函数的功能是一样的。
00208:
本文基linux内核版本 2.6.32-131.17.1.el6.i686源码。
1、原子读、赋值
00016: /**
00017: *atomic_read-readatomicvariable
00018: * @v:pointeroftypeatomic_t
00019: *
00020: *Atomicallyreadsthevalueof @v.
00021: */
00022: staticinlineint atomic_read(constatomic_t*v)
00023: {
00024: returnv->counter;
00025: }
00026:
00027: /**
00028: *atomic_set-setatomicvariable
00029: * @v:pointeroftypeatomic_t
00030: * @i:requiredvalue
00031: *
00032: *Atomicallysetsthevalueof @vto @i.
00033: */
00034: staticinlinevoid atomic_set(atomic_t*v,inti)
00035: {
00036: v->counter= i;
00037: }
00038:
2、原子加减
00039: /**
00040: *atomic_add-addintegertoatomicvariable
00041: * @i:integervaluetoadd
00042: * @v:pointeroftypeatomic_t
00043: *
00044: *Atomicallyadds @ito @v.
00045: */
00046: staticinlinevoid atomic_add(inti,atomic_t*v)
00047: {
00048: asmvolatile(LOCK_PREFIX“addl%1,%0″
00049: :“+m” (v->counter)
00050: :“ir”(i));
00051: }
00052:
00053: /**
00054: *atomic_sub-subtractintegerfromatomicvariable
00055: * @i:integervaluetosubtract
00056: * @v:pointeroftypeatomic_t
00057: *
00058: *Atomicallysubtracts @ifrom @v.
00059: */
00060: staticinlinevoid atomic_sub(inti,atomic_t*v)
00061: {
00062: asmvolatile(LOCK_PREFIX“subl%1,%0″
00063: :“+m” (v->counter)
00064: :“ir”(i));
00065: }
00066:
3、原子加减1
00085:
00086: /**
00087: *atomic_inc-incrementatomicvariable
00088: * @v:pointeroftypeatomic_t
00089: *
00090: *Atomicallyincrements @vby1.
00091: */
00092: staticinlinevoid atomic_inc(atomic_t*v)
00093: {
00094: asmvolatile(LOCK_PREFIX“incl%0″
00095: :“+m” (v->counter));
00096: }
00097:
00098: /**
00099: *atomic_dec-decrementatomicvariable
00100: * @v:pointeroftypeatomic_t
00101: *
00102: *Atomicallydecrements @vby1.
00103: */
00104: staticinlinevoid atomic_dec(atomic_t*v)
00105: {
00106: asmvolatile(LOCK_PREFIX“decl%0″
00107: :“+m” (v->counter));
00108: }
00109:
5、LOCK_PREFIX理解
在原子加减操作中,我们可以发现实现都基于关键指令LOCK_PREFIX。该宏定义在文件arch/x86/include/asm/alternative.h中。
00030: #ifdefCONFIG_SMP
00031: #defineLOCK_PREFIX\
00032: “.section.smp_locks,\”a\”\n”\
00033: _ASM_ALIGN“\n” \
00034: _ASM_PTR “661f\n”/*address*/ \
00035: “.previous\n” \
00036: “661:\n\tlock;”
00037:
00038: #else/*!CONFIG_SMP*/
00039: #defineLOCK_PREFIX“”
00040: #endif
也就是LOCK_PREFIX只有在SMP系统中才有效。在单CPU(UniProcessor)中,能够在单条指令中完成的操作都可以认为是“原子操作”,因为中断只发生在指令边缘。但在多处理器结构中(Symmetric Multi-Processor)就不同了,由于系统中有多个处理器独立运行,即使能在单条指令中完成的操作也有可能受到干扰。在X86平台生,CPU提供了在指令执行期间对总线加锁的手段。CPU上有一根引线#HLOCK pin连到北桥,如果汇编语言的程序中在一条指令前面加上前缀“LOCK”,经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK
pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。
LOCK_PREFIX宏扩展开后,就是在语句前加上lock前缀。
atomic_add():
/root/rpmbuild/BUILD/kernel-2.6.32-131.17.1.el6/linux-2.6.32-131.17.1.el6.i686/arch/x86/include/asm/atomic_32.h:48
*
* Atomically adds @i to @v.
*/
static inline void atomic_add(int i, atomic_t *v)
{
asm volatile(LOCK_PREFIX “addl %1,%0″
c041c21b: 8b 44 24 20 mov 0×20(%esp),%eax
c041c21f: f0 01 05 1c 8f af c0 lock add %eax,0xc0af8f1c
atomic_add_return():
/root/rpmbuild/BUILD/kernel-2.6.32-131.17.1.el6/linux-2.6.32-131.17.1.el6.i686/arch/x86/include/asm/atomic_32.h:182
if (unlikely(boot_cpu_data.x86 <= 3))
goto no_xadd;
#endif
/* Modern 486+ processor */
__i = i;
asm volatile(LOCK_PREFIX “xaddl %0, %1″
c041c226: bf 01 00 00 00 mov $0×1,%edi
c041c22b: f0 0f c1 3d 18 8f af lock xadd %edi,0xc0af8f18
c041c232: c0
6、其他原子操作
减操作后判断是否为0:atomic_sub_and_test()。
00067: /**
00068: *atomic_sub_and_test-subtractvaluefromvariableandtestresult
00069: * @i:integervaluetosubtract
00070: * @v:pointeroftypeatomic_t
00071: *
00072: *Atomicallysubtracts @ifrom @vandreturns
00073: *trueiftheresultiszero,orfalseforall
00074: *othercases.
00075: */
00076: staticinlineint atomic_sub_and_test(inti,atomic_t*v)
00077: {
00078: unsignedchar c;
00079:
00080: asmvolatile(LOCK_PREFIX“subl%2,%0;sete%1″
00081: :“+m” (v->counter),“=qm”(c)
00082: :“ir”(i):“memory”);
00083: returnc;
00084: }
递减后判断是否为0:atomic_dec_and_test()。
00110: /**
00111: *atomic_dec_and_test-decrementandtest
00113: *
00114: *Atomicallydecrements @vby1and
00115: *returnstrueiftheresultis0,orfalseforallother
00116: *cases.
00117: */
00118: staticinlineint atomic_dec_and_test(atomic_t*v)
00119: {
00120: unsignedchar c;
00121:
00122: asmvolatile(LOCK_PREFIX“decl%0;sete%1″
00123: :“+m” (v->counter),“=qm”(c)
00124: :: “memory”);
00125: returnc !=0;
00126: }
00127:
自增后判断是否为0:atomic_inc_and_test()。
00128: /**
00129: *atomic_inc_and_test-incrementandtest
00130: * @v:pointeroftypeatomic_t
00131: *
00132: *Atomicallyincrements @vby1
00133: *andreturnstrueiftheresultiszero,orfalseforall
00134: *othercases.
00135: */
00136: staticinlineint atomic_inc_and_test(atomic_t*v)
00137: {
00138: unsignedchar c;
00139:
00140: asmvolatile(LOCK_PREFIX“incl%0;sete%1″
00141: :“+m” (v->counter),“=qm”(c)
00142: :: “memory”);
00143: returnc !=0;
00144: }
00145:
相加后是否为负值 :atomic_add_negative()。
00146: /**
00147: *atomic_add_negative-addandtestifnegative
00148: * @v:pointeroftypeatomic_t
00149: * @i:integervaluetoadd
00150: *
00151: *Atomicallyadds @ito @vandreturnstrue
00152: *iftheresultisnegative,orfalsewhen
00153: *resultisgreaterthanorequaltozero.
00154: */
00155: staticinlineint atomic_add_negative(inti,atomic_t*v)
00156: {
00157: unsignedchar c;
00158:
00159: asmvolatile(LOCK_PREFIX“addl%2,%0;sets%1″
00160: :“+m” (v->counter),“=qm”(c)
00161: :“ir”(i):“memory”);
00162: returnc;
00163: }
00164:
相加后返回: atomic_add_return()。
00165: /**
00166: *atomic_add_return-addintegerandreturn
00168: * @i:integervaluetoadd
00169: *
00170: *Atomicallyadds @ito @vandreturns @i+ @v
00171: */
00172: staticinlineint atomic_add_return(inti,atomic_t*v)
00173: {
00174: int__i;
00175: #ifdefCONFIG_M386
00176: unsignedlongflags;
00177: if(unlikely(boot_cpu_data .x86<= 3))
00178: goto¯no_xadd;
00179: #endif
00180: /*Modern486+processor*/
00181: __i =i;
00182: asmvolatile(LOCK_PREFIX“xaddl%0,%1″
00183: :“+r”(i),“+m” (v->counter)
00184: :: “memory”);
00185: returni+__i;
00186:
00187: #ifdefCONFIG_M386
00188: no_xadd:/*Legacy386processor*/
00189: local_irq_save(flags);
00190: __i =atomic_read(v);
00191: atomic_set(v,i+__i);
00192: local_irq_restore(flags);
00193: returni+__i;
00194: #endif
00195: }? endatomic_add_return?
00196:
相减后返回:atomic_sub_return()。
00197: /**
00198: *atomic_sub_return-subtractintegerandreturn
00199: * @v:pointeroftypeatomic_t
00200: * @i:integervaluetosubtract
00201: *
00202: *Atomicallysubtracts @ifrom @vandreturns @v- @i
00203: */
00204: staticinlineint atomic_sub_return(inti,atomic_t*v)
00205: {
00206: returnatomic_add_return(-i,v);
00207: }
摘自:http://www.360doc.com/content/11/1014/00/1317564_156005675.shtml
所谓原子操作,就是“不可中断的一个或一系列操作”。
硬件级的原子操作:在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是“原子操作”,因为中断只发生在指令边缘。在多处理器结构中(Symmetric
Multi-Processor)就不同了,由于系统中有多个处理器独立运行,即使能在单条指令中完成的操作也有可能受到干扰。在X86平台生,CPU提供了在指令执行期间对总线加锁的手段。CPU上有一根引线#HLOCK pin连到北桥,如果汇编语言的程序中在一条指令前面加上前缀"LOCK",经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。对于其他平台的CPU,实现各不相同,有的是通过关中断来实现原子操作(sparc),有的通过CMPXCHG系列的指令来实现原子操作(IA64)。本文主要探讨X86平台下原子操作的实现。
软件级别的原子操作:软件级别的原子操作实现依赖于硬件原子操作的支持。
Linux内核提供了两组原子操作接口:一组是针对整数进行操作;另一组是针对单独的位进行操作。
1、原子整数操作
原子操作通常针对int或bit类型的数据,但是Linux并不能直接对int进行原子操作,而只能通过atomic_t的数据结构来进行。目前了解到的原因有两个。
一是在老的Linux版本,atomic_t实际只有24位长,低8位用来做锁。这是由于Linux是一个跨平台的实现,可以运行在多种 CPU上,有些类型的CPU比如SPARC并没有原生的atomic指令支持,所以只能在32位int使用8位来做同步锁,避免多个线程同时访问。(最新版SPARC实现已经突破此限制)。原子整数操作最常见的用途就是实现计数器。常见的用法是:
atomic_t use_cnt;
atomic_set(&use_cnt, 2);
atomic_add(4, &use_cnt);
atomic_inc(use_cnt);
在X86平台上,atomic_t定义如下:
view
plain
typedef struct {
int counter;
} atomic_t;
下面选取atomic_add来进行分析:
view
plain
static inline void atomic_add(int i, atomic_t *v)
{
asm volatile(LOCK_PREFIX "addl %1,%0"
: "+m" (v->counter)
: "ir" (i));
}
可以看到,atomic_add使用了gcc提供的内嵌汇编来实现,是用一个addl指令来实现增加操作。重点看一下LOCK_PREFIX宏,它就是上文提到的锁总线操作,也就是它保证了操作的原子性。LOCK_PREFIX定义如下:
view
plain
#define LOCK_PREFIX \
".section .smp_locks,\"a\"\n" \
" .align 4\n" \
" .long 661f\n" /* address */ \
".previous\n" \
"661:\n\tlock; "
展开后变成:
view
plain
.section .smp_locks,"a"
.align 4
.long 661f
.previous
661:
lock;
逐条解释如下:
.section .smp_locks,"a"
下面的代码生成到 .smp_locks 段里,属性为"a", allocatable
.align 4
四字节对齐
.long 661f
生成一个整数,值为下面的 661 标号的实际地址,f 表示向前引用,如果 661 标号出现
在前面,要写 661b。
.previous
代码生成恢复到原来的段,也就是 .text
661:
数字标号是局部标号,5.3 Symbol Names
lock;
开始生成指令,lock 前缀
这段代码汇编后,在 .text 段生成一条 lock 指令前缀 0xf0,在 .smp_locks 段生成四个字节的 lock 前缀的地址,链接的时候,所有的.smp_locks 段合并起来,形成一个所有 lock 指令地址的数组,这样统计 .smp_locks 段就能知道代码里有多少个加锁的指令被生成,猜测是为了调试目的。
搜索了一下,找到了相关引用处,当一个内核模块被加载时,会调用module_finalize函数:
view
plain
int module_finalize(const Elf_Ehdr *hdr,
const Elf_Shdr *sechdrs,
struct module *me)
{
const Elf_Shdr *s, *text = NULL, *alt = NULL, *locks = NULL,
*para = NULL;
char *secstrings = (void *)hdr + sechdrs[hdr->e_shstrndx].sh_offset;
for (s = sechdrs; s < sechdrs + hdr->e_shnum; s++) {
if (!strcmp(".text", secstrings + s->sh_name))
text = s;
if (!strcmp(".altinstructions", secstrings + s->sh_name))
alt = s;
if (!strcmp(".smp_locks", secstrings + s->sh_name))
locks= s;
if (!strcmp(".parainstructions", secstrings + s->sh_name))
para = s;
}
if (alt) {
/* patch .altinstructions */
void *aseg = (void *)alt->sh_addr;
apply_alternatives(aseg, aseg + alt->sh_size);
}
if (locks && text) {
void *lseg = (void *)locks->sh_addr;
void *tseg = (void *)text->sh_addr;
alternatives_smp_module_add(me, me->name,
lseg, lseg + locks->sh_size,
tseg, tseg + text->sh_size);
}
if (para) {
void *pseg = (void *)para->sh_addr;
apply_paravirt(pseg, pseg + para->sh_size);
}
return module_bug_finalize(hdr, sechdrs, me);
}
上面的代码说,如果模块有 .text
和 .smp_locks 段,就调这个来处理,做什么呢?
view
plain
void alternatives_smp_module_add(struct module *mod, char *name,
void *locks, void *locks_end,
void *text, void *text_end)
{
struct smp_alt_module *smp;
if (noreplace_smp)
return;
if (smp_alt_once) {
if (boot_cpu_has(X86_FEATURE_UP))
alternatives_smp_unlock(locks, locks_end,
text, text_end);
return;
}
........//省略无关代码
}
上面的代码说,如果是单处理器(UP),就调这个:
view
plain
static void alternatives_smp_unlock(u8 **start, u8 **end, u8 *text, u8 *text_end)
{
u8 **ptr;
char insn[1];
if (noreplace_smp)
return;
add_nops(insn, 1);
for (ptr = start; ptr < end; ptr++) {
if (*ptr < text)
continue;
if (*ptr > text_end)
continue;
text_poke(*ptr, insn, 1);
};
}
看到这里就能明白,这是内核配置了 smp,但是实际运行到单处理器上时,通过运行期间打补丁,根据 .smp_locks 里的记录,把 lock 指令前缀替换成 nop 以消除指令加锁的开销,这个优化真是极致了……,可能考虑很多用户直接使用的是配置支持 SMP 编译好的内核而特地对
x86/x64 做的这个优化。
2、原子位操作的实现
编写代码时,以如下的方式进行操作
unsigned long word = 0;
set_bit(0, &word); /*第0位被设置*/
set_bit(1, &word); /*第1位被设置*/
clear_bit(1, &word); /*第1位被清空*/
change_bit(0, &word); /*翻转第0位*/
为什么关注原子操作?
1)在确认一个操作是原子的情况下,多线程环境里面,我们可以避免仅仅为保护这个操作在外围加上性能开销昂贵的锁。
2)借助于原子操作,我们可以实现互斥锁。
3)借助于互斥锁,我们可以把一些列操作变为原子操作。
我们重点关注一下以下两个函数的实现:
view
plain
/**
* clear_bit - Clears a bit in memory
* @nr: Bit to clear
* @addr: Address to start counting from
*
* clear_bit() is atomic and may not be reordered. However, it does
* not contain a memory barrier, so if it is used for locking purposes,
* you should call smp_mb__before_clear_bit() and/or smp_mb__after_clear_bit()
* in order to ensure changes are visible on other processors.
*/
static inline void clear_bit(int nr, volatile void *addr)
{
asm volatile(LOCK_PREFIX "btr %1,%0" : ADDR : "Ir" (nr));
}
view
plain
/*
* clear_bit_unlock - Clears a bit in memory
* @nr: Bit to clear
* @addr: Address to start counting from
*
* clear_bit() is atomic and implies release semantics before the memory
* operation. It can be used for an unlock.
*/
static inline void clear_bit_unlock(unsigned nr, volatile void *addr)
{
barrier();
clear_bit(nr, addr);
}
第一个clear_bit函数比较好理解,和上面atomic系列的函数实现类似。但是注意到clear_bit_unlock函数中多了一个barrier函数,这是什么操作呢?
这就是有名的“内存屏障“或”内存栅栏“操作,先来补充一下这方面的知识。
可以看一下barrier的定义:
view
plain
#define barrier() __asm__ __volatile__("": : :"memory")
解释一下:__volatitle__是防止编译器移动该指令的位置或者把它优化掉。"memory",是提示编译器该指令对内存修改,防止使用某个寄存器中已经load 的内存的值。lock 前缀是让cpu 的执行下一行指令之前,保证以前的指令都被正确执行。
事实上,不止barrier,还有一个mb系列的函数也起着内存屏障的功能:
view
plain
#include <asm/system.h>
"void rmb(void);"
"void wmb(void);"
"void mb(void);"
这些函数在已编译的指令流中插入硬件内存屏障,具体的插入方法是平台相关的。rmb(读内存屏障)保证了屏障之前的读操作一定会在后来的读操作执行之前完成。wmb保证写操作不会乱序,mb 指令保证了两者都不会。这些函数都是 barrier 函数的超集。解释一下:编译器或现在的处理器常会自作聪明地对指令序列进行一些处理,比如数据缓存,读写指令乱序执行等等。如果优化对象是普通内存,那么一般会提升性能而且不会产生逻辑错误。但如果对I/O 操作进行类似优化很可能造成致命错误。所以要使用内存屏障,以强制该语句前后的指令以正确的次序完成。
其实在指令序列中放一个wmb 的效果是使得指令执行到该处时,把所有缓存的数据写到该写的地方,同时使得wmb 前面的写指令一定会在wmb后面 的写指令之前执行。
回到上面的函数,当clear_bit函数不用于实现锁的目的时,不用给它加上内存屏障(我的理解:不管是不是读到最新的数据,这一位就是要清零,不管加不加内存屏障,结果都是一样的);而当用于实现锁的目的时,必须使用clear_bit_unlock函数,其实现中使用了内存屏障,以此来确保此处的修改能在其他CPU上看到(我的理解:加锁操作就是为了在多个CPU间进行同步的目的,所以要避免寄存器优化,其他CPU每次都读内存这样才能看到最新的变化,这块不是太明白)。这种操作也叫做serialization,即在执行这条指令前,CPU必须要完成前面所有对memory的访问指令(read
and write),这样是为了避免编译器进行某些优化。
同样使用serialization操作的还有test_and_set_bit函数:
view
plain
/**
* test_and_set_bit - Set a bit and return its old value
* @nr: Bit to set
* @addr: Address to count from
*
* This operation is atomic and cannot be reordered.
* It also implies a memory barrier.
*/
static inline int test_and_set_bit(int nr, volatile void *addr)
{
int oldbit;
asm volatile(LOCK_PREFIX "bts %2,%1\n\t"
"sbb %0,%0" : "=r" (oldbit), ADDR : "Ir" (nr) : "memory");
return oldbit;
}
解释一下:
1)memory 强制gcc 编译器假设RAM 所有内存单元均被汇编指令修改,这样cpu 中的registers 和cache 中已缓存的内存单元中的数据将作废。cpu 将不得不在需要的时候重新读取内存中的数据。这就阻止了cpu 又将registers,cache 中的数据用于去优化指令,而避免去访问内存。
2)sbb $0,0(%%esp)表示将数值0 减到esp 寄存器中,而该寄存器指向栈顶的内存单元。减去一个0,esp 寄存器的数值依然不变。即这是一条无用的汇编指令。在此利用这条无价值的汇编指令来配合lock
指令,在__asm__,__volatile__,memory 的作用下,用作cpu 的内存屏障。
这种写法和前面的clear_bit_unlock中先写一个barrier函数,再写一个正常内嵌汇编函数的功能是一样的。
00208:
相关文章推荐
- 原子操作的实现原理
- Java并发机制底层——原子操作的实现原理
- java原子操作的原理 cas
- java原子操作的实现原理--转载
- java多线程-专题-聊聊并发(五)原子操作的实现原理
- GCC数值原子操作API原理及应用
- Linux内核同步原语之原子操作
- 原子操作的实现原理
- 聊聊并发(5):原子操作的实现原理
- java原子操作实现原理
- java并发编程学习(三) 原子操作的实现原理及java中如何实现原子操作
- Linux内核中锁机制之原子操作、自旋锁
- linux内核中的原子操作
- 【Linux】linux内核原子操作的实现
- 聊聊并发(五)――原子操作的实现原理
- Java实现原子操作的原理
- linux内核同步机制之原子操作(基于ARM)
- Linux内核中的原子操作的实现
- Java 原子操作的原理和问题
- Linux内核的同步机制:原子操作