泰晓科技 -- 聚焦 Linux - 追本溯源,见微知著!
网站地址:https://tinylab.org

基于泰晓RISC-V实验箱的Linux公开课
请稍侯

RISC-V 原子指令介绍

Wu Zhangjin 创作于 2022/03/23

Author: Pingbo Wen Date: 2022/03/23 Project: RISC-V Linux 内核剖析 Video: 原子指令与用法介绍 - 直播回放

RISC-V 把原子指令划分成单独的指令扩展,命名为 ‘A’。该扩展中主要包含两部分,一个 LR/SC 指令,另外一个是 AMO 指令。

LR/SC 指令

LR 指令是 Load Reserved 的缩写,读取保留;SC 指令是 Store Conditional 的缩写,条件存储。

LR 指令格式及其变种如下:

lr.{w/d}.{aqrl} rd, (rs1)

lr 指令是从内存地址 rs1 中加载内容到 rd 寄存器。然后在 rs1 对应地址上设置保留标记(reservation set)。其中 w/d 分别对应 32 位/64 位版本。

SC 指令格式及其变种如下:

sc.{w/d}.{aqrl} rd, rs2, (rs1)

sc 指令在把 rs2 值写到 rs1 地址之前,会先判断 rs1 内存地址是否有设置保留标记,如果设置了,则把 rs2 值正常写入到 rs1 内存地址里,并把 rd 寄存器设置成 0,表示保存成功。如果 rs1 内存地址没有设置保留标记,则不保存,并把 rd 寄存器设置成 1 表示保存失败。不管成功还是失败,sc 指令都会把当前 hart 保留的所有保留标记全部清除。其中 w/d 分别对应 32 位/64 位版本。

为了方便理解,我们用伪代码重新描述一下 sc.d 指令:

if (is_reserved(rs1)) {
    *rs1 = rs2
    rd = 0
} else
    rd = 1
clean_reservation_set(cur_hart)

注意在 RISC-V 的 Spec 中只规定了 sc 指令如果失败了,会往 rd 寄存器中写入非零值,并不一定是 1。

对于 lr/sc 指令,要求 rs1 寄存器中的地址是按宽度对齐的,比如 lr.w 要求 4 字节对齐,sc.d 要求 8 字节对齐。否则会触发非对齐异常。

如果在 sc 指令之前,当前 hart 观察到了对应内存地址被其他 hart 写了,则 sc 指令会失败。相当于保留标记失效了。如果对应内存地址被外部设备(非 hart)或者总线写了,外部设备需要主动把写范围内的保留标记清除,不在写入范围的字节不需要清除保留标记。

利用 LR/SC 指令,我们可以实现 CAS(Compare and swap) 操作,代码如下:

# Expected old value in a1, new value in a2
retry:
    lr.w a3, (a0)
    bne a3, a1, fail
    sc.w a3, a2, (a0)
    bnez a3, retry
    success code after CAS ...
fail:
    fail code after CAS ...

RISC-V 对 LR 和 SC 之间的指令是有限制的,一个是 LR 和 SC 之间最大只能包含 16 个指令,另外这些指令只能使用基础整数指令集(指令集 “I”,不包含内存访问指令,跳转指令,fence 和 system 指令)。具体可以参考 Spec 10.3 章节。如果违反了这些限制,LR/SC 指令的效果是不受约束的,可能在一些芯片实现上能保证原子性,在另外一些芯片实现上不能保证。

AMO 指令

AMO 是 Atomic Memory Operation 的缩写。AMO 指令有如下几个:

AMO 指令格式说明
AMOSWAPamoswap.{w/d}.{aqrl} rd, rs2, (rs1)原子交换指令,rd = *rs1, *rs1 = rs2
AMOADDamoadd.{w/d}.{aqrl} rd, rs2, (rs1)原子加法指令,rd = *rs1, *rs1 += rs2
AMOANDamoand.{w/d}.{aqrl} rd, rs2, (rs1)原子按位与指令,rd = *rs1, *rs1 &= rs2
AMOORamoor.{w/d}.{aqrl} rd, rs2, (rs1)原子按位或指令,rd = *rs1, *rs1 |= rs2
AMOXORamoxor.{w/d}.{aqrl} rd, rs2, (rs1)原子按位异或指令,rd = *rs1, *rs1 ^= rs2
AMOMAXamomax.{w/d}.{aqrl} rd, rs2, (rs1)原子有符号取最大值指令,rd = rs1, *rs1 = max(rs1, rs2)
AMOMAXUamomaxu.{w/d}.{aqrl} rd, rs2, (rs1)原子无符号取最大值指令,rd = rs1, *rs1 = maxu(rs1, rs2)
AMOMINamomin.{w/d}.{aqrl} rd, rs2, (rs1)原子有符号取最小值指令,rd = rs1, *rs1 = min(rs1, rs2)
AMOMINUamominu.{w/d}.{aqrl} rd, rs2, (rs1)原子无符号取最小值指令,rd = rs1, *rs1 = minu(rs1, rs2)

和 LR/SC 指令类似,所有的 AMO 指令都要求 rs1 寄存器的地址是按宽带对齐的,否则会触发异常。

内存访问顺序(Memory Order)

RISC-V 和 ARM 类似,内存模型都是弱内存模型(relax memory model),这意味着,在不加额外限制的情况下,内存访问指令并不会完全按照指令顺序执行。RISC-V 有一个 FENCE 指令,可以用来显式添加内存顺序限制。为了提高效率,RISC-V 为每个原子指令都预留 aq/rl 两个比特位,从而可以很方便在原子指令上施加额外的内存顺序限制。原子指令是用来在不同 hart 之间做同步用的,而内存访问顺序强调的是同一个 hart 内的执行顺序。

在上面介绍 LR/SC 和 AMO 指令时,每个指令后面都带有一个 “aqrl” 的可选后缀。aq 是 acquire 的缩写,rl 是 release 的缩写。LR/SC 和 AMO 指令就是通过这两个后缀来添加额外的内存顺序限制。具体定义如下:

AcquireRelease含义
00没有顺序限制
01该指令前序所有访问存储的指令的结果必须在该指令执行之前被观察到
10该指令后序所有访问存储的指令必须等该指令执行完成后才开始执行
11该指令前序所有访问存储的指令的结果必须在该指令执行之前被观察到,该指令后序所有访问存储的指令必须等该指令执行完成后才开始执行

为了加深理解,我们可以看如下示意图:

RISCV AQRL 示意图

aq 标志位会限制所有后面内存访问指令,rl 标志位会限制所有前面内存访问指令,而 aqrl 是前两者的效果叠加。分别使用 aq 和 rl 标志位,可以人为的划定一个范围,把这两者之间的内存访问指令框起来。

虽然 RISC-V 为每个原子指令都预留了 aq/rl 比特位,但对于一些特定指令,aq/rl 不能随便设置。比如 lr.d.rl 和 sc.d.aq 指令是没有实际意义的,RISC-V 并没有直接禁止这种用法,但这种指令没有预期的原子访问效果。

在软件层面,除了使用 FENCE 和 AQRL 手段来限制内存访问顺序之外,还有一种手段可以用,那就是依靠数据依赖链来间接限制内存访问顺序。C++ 内存模型定义中,给这种方法命名为 memory_order_consume。我们可以用一段 c 代码简单说明一下:

int *a, *b, *c;
*b = *a + 1;
*c = *b + 1;

因为第三行代码依赖第二行代码的结果,所以第三行代码执行顺序不可能放到第二行代码之前,这就是数据链依赖。



Read Album:

Read Related:

Read Latest: