Java Solaris 加入Sun中国技术社区 我的社区 注册说明
 
JDK 6.0 API 中文版
 
 
 
 
Java API 文档中文版
Atomic SPARC:使用 SPARC 原子操作指令
 
By Richard Marejka, 4/20/08  

SPARC 架构是一种 RISC 处理器,它的最初形式是 Sun Microsystems 于 1986 年推出的 Sun-4/260 系统。从此之后,该处理器得到了不断的改进,以适应不断变化的用户需求。

如今的 SPARC 处理器旨在寻求高性能和高效率之间的平衡。其实现方法通常为在一个处理器中整合多个内核,或者在一个系统中并入多个处理器。这些系统在执行多流程和多线程作业时可以实现极佳的效果。并行度和效率的实现少不了 原子操作指令。这些指令为实现 Solaris 操作系统和 SPARC 之间高度并行化提供了不可或缺的同步基础。

本文将简要介绍 SPARC 内存模型和原子操作指令,然后将实现一系列可在 Solaris OS 上使用的 IBM AIX 接口。本文假设读者熟悉汇编语言编程。本文所提供的示例旨在演示 SPARC 原子操作指令和内存模型的使用。我们还提供了一个小型库,可帮助程序员将基于 IBM AIX 的源代码移植到 Solaris OS / SPARC 平台。

内存模型

SPARC Version 9(SPARC v9)规范定义了三种内存模型,包括最小限制和最大限制:

  • 宽松内存顺序(Relaxed Memory Order,RMO):内存引用无顺序限制(除维持处理器一致性需求之外)。若需要顺序引用,则开发人员必须使用 membar 指令 明确指定。
  • 部分存储顺序(Partial Store Order,PSO):它具有 RMO 的所有需求,并且装入根据时间排序,而原子装入存储则根据装入排序。需要使用 membar 指令对存储和原子装入存储排序。
  • 总存储顺序(Total Store Order,TSO):它具有 PSO 的所有需求,并且存储根据时间排序,而原子装入存储则根据装入和存储排序。

SPARC 架构提供多种内存模型的原因如下:其一,实现可以调度内存操作,以实现更高的性能;其二,程序员可以使用共享内存创建同步原语。限制较少的内存模型-- RMO 和 PSO -- 担负通过处理器增强应用程序性能的较大任务。

理想 SPARC v9 处理器的结构如图 1 所示。

SPARC 处理器
图 1. SPARC v9 处理器

 

处理器的发布(Issue)单元从内存读取指令,并按程序顺序将它们发送出去。程序顺序 是由应用程序控制流决定的顺序,它假设各指令的执行是相互独立且连续的。

重排序(Reorder)单元收集这些发出的指令,并将它们分发给执行(Execute)单元。重排序单元允许实现重新排列指令,通过并行执行指令提高效率。重排序单元只限于维持程序顺序。

执行单元执行指令,并将结果写入缓冲单元。

缓冲单元调度写入内存操作。缓冲单元的作用是确保执行单元不受内存写入操作造成的延时影响。如果保存了前一次存储地址的内容,缓冲单元还可以响应装入内存地址的请求。这引入了潜在的不一致性:内存写入请求可以保存在缓冲单元中,然后,发布单元重新载入缓冲单元,执行单元再进入修改,并在缓冲单元写入内存之前再次生成写入请求。虽然这在理论上会造成单处理器系统进程间的不一致,但这种不一致性并未实际发生,因为进程上下文切换包括通过缓冲单元刷新内存。

在多处理器系统中,缓冲单元有时会造成不一致性。当各处理器修改了共享内存,但未写入内存时,便会造成不一致性 —— 也就是说,当经过修改的值出现在多个处理器的缓冲单元中时。图 3 展示了一个多处理器系统。

SPARC 多处理器系统
图 2. SPARC v9 多处理器系统

 

 

Membar 指令

SPARC v9 架构包括 membar 指令。术语 membar 表示收缩内存阻碍。membar 拥有两个变量:

  • Ordering 允许程序员控制处理器的装入和存储顺序。
  • Sequencing 允许程序员控制内存操作的顺序和完成。

membar 指令将对某个处理器的指令流排序。出现在 membar 指令前的装入和存储将根据 membar 后的参数排序。原子操作指令排序类似于装入和存储,因为它们执行两种操作。指令拥有四种位编码的排序关系:

  • #LoadLoad
  • #StoreLoad
  • #LoadStore
  • #StoreStore

各种 #XY 关系的语义如下:“在程序顺序中,所有出现在 membar 之前的 X 操作先于其后的 Y 操作完成。”通过使用按位 or 运算符,我们可以创建更加复杂的排序需求。

SPARC Version 8(SPARC v8)stbar 指令是 membar 的一个子集,它等价于具有 #StoreStore 排序关系的 membar

根据系统所依赖的内存模型,程序员必须明确插入内存排序指令,以保证程序的正确性。举例来说,SPARC 使用存储无符号字节(stub)指令汇编代码以释放锁,如下所示:

unlock_ldstub:
nop ! TSO or
membar #StoreStore ! PSO or
membar #StoreStore | #LoadStore ! RMO
stub %g0,[%o0]
retl
nop

 

请记住,锁将保护变量不被修改(从请求锁之后到释放锁之前)。变量的修改表示程序对内存的写操作。在 PSO 模式 中,#StoreStore 将造成对变量的存储操作根据锁存储排序。如果未指定顺序,则在将变量更新到内存中之前,锁可能处于空闲状态。

出于防御目的的编程在开发时将假定使用限制最小的模型(RMO),因为使用其他两种 SPARC 内存模型都会正确执行。防御学校还将所有同步原语收集到公共系统库中。

原子操作指令

通常,SPARC 机器指令在执行到完成的过程中不会中断。这包括装入和存储的内存访问指令。在多处理器多核系统中,两个或多个处理器使用相同内存地址执行指令,这需要确保它们以未定义的顺序执行。这将保证内存的一致性,但是其操作顺序是未定义的,操作完成之后的内存内容也是未定义的。

原子操作指令的作用类似于装入和存储,它扩展了“无间断”需求,在其中包括了两种操作。这些指令允许创建多线程和协作式多进程应用程序,利用当今高性能系统提供折并发性。

SPARC Version 9(v9)拥有三种原子操作指令:

装入存储无符号字节:ldstub

装入存储无符号字节(ldstub)指令是 Solaris OS 最初用于实现互斥锁(mutex)和其他线程同步原语的原子原语。该指令将 0xff 写入字节,并返回寄存器中的原始值。要请求锁,调 用程序需使用 ldstub 并检查所返回的值。如果返回值为零,则请求锁并且锁字节将包含 0xff。如果返回值为 0xff,则锁仍然属于之前的调用程序。然后,调用程序将执行一些适应算法,一直等到锁可用 —— 也就是说,它的值变为或回到零。

此处给出了该指令的算法,使用类似于 C 的伪代码实现:

int8_t
ldstub( int8_t *lock_byte ) {
int8_t old_value;

atomic {
old_value = *lock_byte;
*lock_byte = 0xff;
}

return( old_value );
}

 

ldstub 是典型的测试和设置指令。该指令的缺点在于 一致数量(consensus number) 为 2,因此不能在等待空闲时处理超过 2 个的竞争进程。

交换寄存器与内存: swap

交换寄存器与内存(swap—)指令将交换内存与寄存器中的内容。根据 SPARC v9 手册,swap 指令是不适用的,并且程序员应该使用 casxa 指令作为替代。

它的算法如下所示:

int32_t
swap( int32_t *word, int32_t new_value ) {
int32_t old_value;

atomic {
old_value = *word;
*word = new_value;
}

return( old_value );
}

 

类似于 ldstub 指令,swap 的一致数量也为 2。

比较和交换:cas

SPARC v9 手册在 8.4.6.1 部分引入了最新的原子指令:比较和交换(cas)。该指令使用内存地址和两个寄存器。它比较内存与寄存器中的内容。如果它们相等,则指令将交换内存与第二个寄存器中的内容。

该指令的一致数量为无穷大:它可以通过等待-空闲的方式处理无限个竞争进程。您可以使用 cas 执行这种锁空闲(lock-free)操作:链表管理就是一个典型的例子。术语 lock-free 可能有点用词不当。进程仍然使用锁,但锁在硬件中取代了比较常用的互斥锁。

cas 的算法如下所示:

int64_t
cas64( int64_t *word, int64_t test_value, int64_t new_value ) {
int64_t old_value;

atomic {
old_value = *word;

if ( *word == test_value )
*word= new_value;
}

return( old_value );
}

 

要判断交换的发生,可以比较第二个寄存器的返回值与第一个寄存器所使用的测试值。它的伪代码如下所示:

if ( cas64( &cas_word, testV, newV ) == testV )
/* swap took place */

 

在编写 SPARC 汇编时,可以使用一个简单的性能增强工具。

Solaris OS 接口

Solaris OS 提供了许多接口供并发和多处理器系统使用。这些 Solaris 线程接口 最早出现在 Solaris 2.4 中(1993 年 3 月)。Solaris 2.5 增加了 POSIX 线程接口(1995 年 6 月)。它们经常被称作 Pthread 接口,并且最初出现在 3T 手册部分中。在 Solaris 10 中,Solaris 和 POSIX 线程接口记录在 Multithreaded Programming Guide 中。

多年以来,通过不断扩展,Pthread 接口仍然能够保持与 POSIX 的兼容,并添加了许多不属于 POSIX 标准的功能。线程库至少重新编写过一次。这次重编协调了 Solaris OS 与 POSIX 接口的一致,将接口集成到 libc中,并增强了性能。

大多数供应商都采用了 POSIX 线程接口,因此在各供应商的平台之间移植只需重新编译线程接口即可。但是,简单的重新编译并不能解决所有问题。如果应用程序使用特定于供应商的同步原语,则移植应用程序就不再是重新编译能够完成的工作。在某些情况下,这可能非常简单。举例来说,Solaris OS 与 POSIX 线程接口之间可以实现完全一致的接口映射。但是,也有一些接口并不是那么容易实现。

IBM AIX Interfaces

自 3.2 版本于 1992 年发布之后,IBM AIX 平台提供了一些特定于供应商的同步原语。Jacques Talbot 撰写的白皮书 "Turning the AIX Operating System Into an MP-Capable OS" 详细介绍了 PoewerPC 处理器的背景和 AIX 操作系统的同步。USENIX 1995 Technical Conference Proceedings 的 Potpourri II 部分提供该白皮书。

相关的同步原语如下:

#include   <sys/atomic_op.h>

int fetch_and_add( atomic_p word_addr, int value );
uint fetch_and_or( atomic_p word_addr, int mask );
uint fetch_and_and( atomic_p word_addr, int mask );
void _clear_lock( atomic_p word_addr, int value );
boolean_t _check_lock( atomic_p word_addr, int old_val, int new_val );
boolean_t compare_and_swap( atomic_p word_addr, int *old_val_addr, int new_val );
boolean_t test_and_set( atomic_p word_addr, int mask );

 

了解了 SPARC 原子操作指令和 SPARC 汇编的基础知识之后,您可以为 Solaris OS 实现 7 种 AIX 同步原语。这样可以实现一个兼容库,用于将 AIX 应用程序移植到 Solaris 平台。该库针对运行在 32 位模式的 Solaris / SPARC v9 平台实现。程序员社区将提供 SPARC 64 位、AMD 和 Intel 实现。

本节中的各个接口都将使用类似于 C 的伪代码编写,并在 SPARC 汇编中实现。所有接口都是叶子例程,因此可以利用叶子过程优化,请参阅 SPARC Architecture Manual v8(PDF)的 D.5 和 H.1.2 部分。

fetch_and_addfetch_and_orfetch_and_and 接口

fetch_and_OP 接口全部采用相同的算法,并根据需要取代特定的操作。SPARC 平台并不直接对内存操作:所有操作都基于寄存器,但装入、存储和原子操作指令是例外。这要求 fetch_and_OP 在循环中使用 cas 指令。当循环计算并得出预期结果之后,cas 将尝试执行更新。如果更新失败,则循环将继续执行。

算法如下所示:

int
fetch_and_OP( atomic_p word_addr, int value ) {
int result;

atomic {
result = *word_addr;
*word_addr OP= value;

}
return( result );
}

 

在 SPARC 汇编中,算法应为:

fetch_and_OP:
ld [%o0],%g1 ! load the current value
loop: OP %g1,%o1,%o2 ! compute the desired result
cas [%o0],%g1,%o2 ! try to CAS it into place
cmp %g1,%o2
bne,a,pn %icc,loop ! CAS failed, try again
mov %o2,%g1 ! save current value for next iteration
retl
mov %o2,%o0 ! return the old value

 

完成三种接口还需要将替代 OP 占位符操作的 andoradd

注意,我们使用了两个条件转移特性:废除和预言。如果条件未跳转,废除 ",a" 将忽略延时槽中的 mov 指令。预言 ",pn" 将暗示未采取条件转移的处理器。也就是说,该处理器应假定低竞争。

此外,cas 指令的返回值还可用于增强性能。cas 的返回值将使用第二个寄存器读取内存内容,而不是采用装入的方式。

_clear_lock_check_lock 接口

这两个接口将以原子的方式更新锁内容。_clear_lock 接口只将锁设置为指定值:其本质是运行时初始化。_check_lock 接口将根据条件更新锁内容。_check_lock 接口是比较和交换类型的操作。

算法如下所示:

void
_clear_lock( atomic_p word_addr, int value ) {
*word_addr = value;
return;
}

boolean_t
_check_lock( atomic_p *word_addr, int old_val, int new_val ) {
int result;

atomic {
if ( *word_addr == old_val )
*word_addr = new_val;
result = FALSE;
} else
result = TRUE;
}
return( result );
}

 

由于这些接口各自的作用都是实现同步,因此它们需要一个内存屏障。它们的 SPARC 汇编如下所示:

_clear_lock:
membar #StoreStore|#LoadStore ! memory barrier (RMO)
st %o1,[%o0] ! store the word
retl
nop

_check_lock:
cas [%o0],%o1,%o2 ! try the CAS
cmp %o1,%o2
mov 0,%o0 ! assume it succeeded - return FALSE/0
movne %icc,1,%o0 ! may have failed - return TRUE/1
membar #LoadLoad|#LoadStore ! memory barrier (RMO)
retl
nop

 

需要注意,_check_lock 接口中使用了条件转移指令,尤其是 movne。借助 %icc,条件转移将使用整型条件代码将 1 移动到返回寄存器,仅当 cmp 指令检测到 cas 失败时。

compare_and_swap 接口

compare_and_swap 接口直接映射 SPARC v9 cas 指令,它有两个实现需求:返回值为 boolean_t,并且,如果未发生交换,则通过第二个参数返回初始值。

boolean_t
compare_and_swap( atomic_p word_addr, int *old_val_addr, int new_val ) {
int oldV = *old_val_addr;
boolean_t result;

atomic {
if ( *word_addr == oldV ) {
*word_addr = new_val;
result = TRUE;
} else {
*old_val_addr = word_addr;
result = FALSE;
}
}
return( result );
}

 

该接口的 SPARC 汇编将通过合适的参数及返回值管理直接映射到 cas 指令。

compare_and_swap:
ld [%o1],%g1 ! set the old value
cas [%o0],%g1,%o2 ! try the CAS
cmp %g1,%o2
be,a true
mov 1,%o0 ! return TRUE/1
mov 0,%o0 ! return FALSE/0
st %o2,[%o1] ! store existing value in memory
true: retl
nop

 

惟一需要注意的事项是返回处理过程,它需要通过条件转移实现。如果交换发生,则执行条件分支和 mov 1,%o0。如果未执行分支,则 Annul 位将造成第一个移动指令被忽略,并且将继续执行 mov 0,%o0 指令。

test_and_set 接口

此接口是一个位测试和设置操作。mask 的位 or 操作和内存内容是操作的测试部分。如果未设置 mask 中的位,则使用按位 or 操作将 mask 添加到结果中 —— 这是操作的设置部分。

boolean_t
test_and_set( atomic_p word_addr, int mask ) {
boolean_t result;

atomic {
if ( *word_addr & mask )
result = FALSE;
else {
*word_addr |= mask;
result = TRUE;
}
}
return( result );
}

 

实现将使用循环,但这并不是因为接口需要在返回之前成功 —— 这不是需求。需要循环的原因是 if ( ... )*word_addr |= mask 步骤之间的竞争条件。为向您演示,伪代码并没有 atomic 部分:

boolean_t
test_and_set( atomic_p word_addr, int mask ) {
boolean_t result;

loop {
int oldV = *word_addr;

if ( oldV & mask ) { /* test step */
result = FALSE;
break;
} else {
int newV = oldV | mask;

if ( cas32( word_addr, oldV, newV ) == oldV ) {
result = TRUE;
break;
} else /* *word_addr changed between test step and cas */
continue;
}
}
return( result );
}

 

上述表单假设 cas32 函数包括 atomic 部分。这是一种安全假设,因为函数可以采用以下形式:

cas32:
cas [%o0],%o1,%o2
retl
mov %o2,%o0

 

并且函数根据定义应为 atomic 特性。

第二种形式的 test_and_set 将密切映射到 SPARC 汇编,如下所示:

test_and_set:
ld [%o0],%g1 ! load the current value
loop: andcc %g1,%o1,%g0 ! test mask against value
bnz,a done
mov 0,%o0 ! return FALSE/0
or %g1,%o1,%o2 ! compute the desired result
cas [%o0],%g1,%o2 ! try the CAS
cmp %g1,%o2
be,a done
mov 1,%o0 ! return TRUE/1
ba loop ! try again
mov %o2,%g1 ! save current value for next iteration
done: retl
nop

 

 

结束语

本文提供了对 SPARC v9 处理器内存模型和原子操作指令的概述,它们与多处理器系统和共享内存应用程序关系密切。文章还有一些 IBM AIX 接口的 Solaris OS 实现,您可以使用它们将基于 AIX 应用程序移植到 Solaris OS。

致谢

java.sun.com 管理员 Jill Welch 和管理编辑 Christine Dorffi 对本文提供了莫大的帮助。

更多信息