14.3.2 x86架构下的原子操作
Nginx要在源代码中实现对整型的原子操作,自然必须通过内联汇编语言直接操作硬件才能做到,本节以基于x86的SMP多核架构为例来看看Nginx是如何实现这两个基本的原子操作的(由于参考着x86架构下的实现即可以简单地推导出其他架构下的实现,故其他架构下的原子操作实现方法不再一一说明)。
使用GCC编译器在C语言中嵌入汇编语言的方式是使用asm关键字,如下所示。
asmvolatile(汇编语句部分
:输出部分
/可选/
:输入部分
/可选/
:破坏描述部分
/可选/
);
以上加入的volatile关键字用于限制GCC编译器对这段代码做优化。
这段内联的汇编语言包括4个部分。
(1)汇编语句部分
引号中所包含的汇编语句可以直接用占位符%来引用C语言中的变量(最多10个,%0~%9)。
下面简单介绍一下随后用到的两个汇编语句,先来看看cmpxchgl r,[m]这个语句,Nginx源代码中对这一汇编语句有一段伪代码注释,如下所示。
//如果eax寄存器中的值等于m
if(eax==[m]){
//将z f标志位设为1
zf=1;
//将m值设为r
[m]=r;
//如果eax寄存器中的值不等于m
}else{
//zf标志位设为0
zf=0;
//将eax寄存器中的值设为m
eax=[m];
}
从上面这段伪代码可以看出,cmpxchgl r,[m]语句首先会用m比较eax寄存器中的值,如果相等,则把m的值设为r,同时将zf标志位设为1;否则将zf标志位设为0。
再看一个语句sete[m],它正好配合着上面的cmpxchgl语句使用,这里不妨简单地认为它的作用就是将zf标志位中的0或者1设置到m中。
(2)输出部分
这部分可以将寄存器中的值设置到C语言的变量中。
(3)输入部分
可以将C语言中的变量设置到寄存器中。
(4)破坏描述部分
通知编译器使用了哪些寄存器、内存。
简单了解了GCC如何内联汇编语言后,下面来看看ngx_atomic_cmp_set方法的实现,如下所示。
static ngx_inline ngx_atomic_uint_t
ngx_atomic_cmp_set(ngx_atomic_t*lock,ngx_atomic_uint_t old,
ngx_atomic_uint_t set)
{
u_char res;
//在C语言中嵌入汇编语言
asmvolatile(
//多核架构下首先锁住总线
"lock;"
//将lock的值与eax寄存器中的old相比较,如果相等,则置lock的值为set
"cmpxchgl%3,%1;"
//cmpxchgl的比较若是相等,则把zf标志位1写入res变量,否则res为0
"sete%0;"
:"=a"(res):"m"(*lock),"a"(old),"r"(set):"cc","memory");
return res;
}
现在简单地说明一下上述代码,在嵌入汇编语言的输入部分,"m"(lock)表示lock变量是在内存中,操作lock时直接通过内存(不使用寄存器)处理,而"a"(old)表示把old变量写入eax寄存器中,"r"(set)表示把set变量写入通用寄存器中,这些都是在为cmpxchgl语句做准备。"cmpxchgl%3,%1"相当于"cmpxchgl setlock"(含义参照上面介绍过的伪代码)。这3行汇编语句的意思如下:首先锁住总线防止多核的并发执行,接着判断原子变量lock与old值是否相等,若相等,则把lock值设为set,同时设res为1,方法返回;若不相等,则设res为0,方法返回。
在了解ngx_atomic_fetch_add方法前,再介绍一个汇编语句xaddl。下面先来看看Nginx对"xaddl r,[m]"语句做的伪码注释,如下所示。
temp=[m];
[m]+=r;
r=temp;
可以看到,xaddl执行后[m]值将为r和[m]之和,而r中的值为原[m]值。现在看看ngx_atomic_fetch_add方法是如何实现的,如下所示。
static ngx_inline ngx_atomic_int_t
ngx_atomic_fetch_add(ngx_atomic_t*value,ngx_atomic_int_t add)
{
asmvolatile(
//首先锁住总线
"lock;"
//value的值将会等于原先value值与add值之和,而add为原*value值
"xaddl%0,%1;"
:"+r"(add):"m"(*value):"cc","memory");
return add;
}
可见,ngx_atomic_fetch_add将使得value原子变量的值加上add,同时返回原先value的值。