Linux内核同步机制

前言

在进行单机并发编程操作时,计算机一般是通过内部互联总线,采用读写共享内存的方式完成对共享数据的访问。而为了提高处理性能,芯片厂商在硬件层面加了许多优化,然而不同的处理器架构采取的优化方法也不尽相同,这就导致可能会出现不同的内存乱序访问行为,对编译器的处理会造成很大影响,同时也会给软件程序员带来相当大的困扰。下面给出一个示例:

static int A = 0, B = 0, C = 0 ;
static void thread_cpu1(void)
{
        C = 1;
        A = 1;
}
static void thread_cpu2(void)
{
        while(A == 0) ;
        B = 1 ;
}
static void thread_cpu3(void)
{
		while(B == 0) ;
        printf("A = %d, C = %d\n", A, B);
}

如上所示,三个处理器(P1,P2,P3)共享 A,B,C 三个初始化为 0 的变量,那么最终的输出结果会是什么呢?可能大多数人心中的答案是A == 1 && C == 1,这是最符合直觉的答案,但其实A == 0 && C == 1,A == 1 && C == 0,A == 1 && C == 0,这些答案都是有可能的。而这就跟处理器之间是否能看见变量的最新修改值有关了。

看见:当一个处理器(PA)对共享变量(V)的本地副本进行了修改,而另一个处理器(PB)用于保存相应变量的缓存行因此而失效或被更新,则称 PB 看见了 PA 对变量 V 的修改

A == 0 && C == 1 为例,当 P1 上的赋值语句 C = 1 及 A = 1 完成之后,P2 看见 P1 对 A 的修改,从而跳出循环并执行 B = 1,接下来 P3 看见 P2 对 B 的修改,从而跳出循环并打印 A,C。在打印 A,C 时,存在这样的可能,也即 P3 看见了 P1 对 C 的修改,而没看见 P1 对 A 的修改,从而打印出 A == 0 && C == 1

也就是说,当你以为 C = 1 执行完之后,所有处理器都应该看见 C 的最新值,但实际可能并不是这样,有的处理器确实看不到最新值导致结果和预期不一样。而内存一致性模型就是为了解决这个问题的。

内存一致性模型简介

内存一致性模型(Memory Consistency Model)是用来描述多处理器(多线程)对共享存储器访问行为的规范,在不同的内存一致性模型里,多线程对共享存储器的访问行为有非常大的差别。这些差别会严重影响程序的执行逻辑,甚至会造成软件逻辑问题。

内存一致性模型分类

内存一致性关注对内存的读写操作访问,有此可得到 4 种不同的指令组合,依次为 Store-Load、Store-Store、Load-Store、Load-Load,通过允许调整这些指令组合执行的顺序,可以获得不同的内存一致性模型。目前有多种内存一致性模型,从上到下模型的限制由强到弱,依次为:

  • 顺序一致性(Sequential Consistency,简写 SC)模型
  • 完全存储定序(Total Store Order,简写 TSO)模型
  • 部分存储定序(Part Store Order,简写 PSO)模型
  • 宽松存储(Relax Memory Order,简写 RMO)模型

SC 模型

SC 模型也称为强定序模型,CPU 按照程序代码的指令次序来执行所有的 Store 与 Load 指令,并且从其它 CPU 和内存的角度来看,感知到的数据变化也完全是按照指令执行的次序。在这种模型下,程序不需要使用任何内存屏障,但是性能会比较差。为了提升系统的性能,不同处理器架构或多或少对这种强一致性模型进行了放松,允许对某些指令组合进行重排序。

重排序:是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

TSO 模型

TSO 模型允许对 Store-Load 指令组合进行重排序,即如果第一条指令是 Store 指令,第二条指令是 Load 指令,那么在程序执行时,Load 指令可能先于 Store 指令执行。TSO 模型的硬件实现,通常是在 CPU 和 Cache 之间引入了 Store Buffer,Store Buffer 只有 Store(即内存写)指令会使用到,并且 Store Buffer 以 FIFO(先进先出)的方式处理写入的数据。这种情况下,Store-Store 指令组合仍能按照顺序执行,但 Load 指令则可能会先于 Store 指令完成执行。x86 架构采用的就是 TSO 模型。

PSO 模型

PSO 模型是在 TSO 模型的基础上,进一步放宽内存访问限制,允许对 Store-Store 指令组合进行重排序。从硬件实现的角度来看,就是允许了 CPU 以非 FIFO 的方式处理 Store Buffer 中的指令。

RMO 模型

RMO 模型是最为宽松的内存一致性模型,它在 PSO 模型的基础上进一步放宽了访存指令的执行限制,其不仅允许 Store-Load、Store-Store 指令组合重排序,还进一步允许了 Load-Load、Load-Store 指令组合重排序,只要是地址无关的指令,在读写访问的时候都可以打乱所有 load/store 的顺序。而我们接下来要研究的 RISC-V 架构采用的就是 RMO 模型。

RISC-V 内存一致性模型

2015 年左右,普林斯顿团队发现 RISC-V 最初采用的内存一致性规范存在一定的缺陷,自 2015 年 12 月起,RISC-V 基金会一直与普林斯顿团队以及其他 MCM 专家合作,帮助加强 RISC-V ISA 规范。官方于 2019 年发布的 《The RISC-V Instruction Set Manual, Volume I: User-Level ISA》 明确了 RISC-V 的内存一致性模型,将其称为 “RVWMO” (RISC-V Weak Memory Ordering)。

RVWMO 主要遵循 RMO 模型,旨在提供可以灵活地构建高性能可扩展程序的设计,同时提供一个易于处理的编程模型,禁止过于复杂的重排序情况,方便软件程序员的开发利用。RVWMO 明确规定了 13 条规则和 3 条公理,下面我们来看一下官方的定义:

定义与规则

RVWMO 内存模型是根据全局内存序(global memory order)定义的,一般来说,多线程程序有许多种可能的执行顺序,每个执行顺序都有自己相应的全局内存序,规定明确指出任何满足所有内存模型约束的执行都是合法的执行(就内存模型而言)。

RISC-V 用 hart(hardware thread)表示硬件执行线程或执行单元

全局序即所有并发线程在内存系统中形成的最终内存访问顺序,各个线程对这个全局序的观察都是一致的(除了 store buffer 带来的“写后读”情况)

内存操作的程序序(program order)反映了生成每次加载和存储的指令在该 hart 的动态指令流中的逻辑布局顺序;内存访问指令会生成内存操作,即 load/store/load&&store 操作。保留程序序(preserved program order)指在全局程序序下的符合规定程序序的子集。如果内存操作 a,b 都是常规内存访问(非 I/O 访问),且在程序序中 a 领先于 b,并且满足 RVWMO 指出的 13 项条件之一,即满足在保留程序序中,a 领先于 b。下面我们来分析这 13 项条件:在这里插入图片描述

RULE 1

定义:如果指令 A,B(store 指令)访问相同地址(或者有地址重叠),那么 A 必须在全局内存序上领先于 B。示例如下:

		hart1    				   hart2
A1:	lw t1,0(s0)				B1:	sw t2,0(s0)
A2:	sw t1,0(s1)				B2:	sw t3,0(s1)

因为 B 指令进行的是内存写(store)操作,那么对于并行的 A,B 来说,如果 A 指令不能在 B 指令的结果被所有处理器看见之前读入该地址(s0)的值,那么 A 指令读到的值,就可能与 B 写入相同地址(s0)的值发生冲突。同理,A 指令进行的内存写操作,也必须先于 B 指令被看见,否则写入时也会发生冲突。

因此这条规则也可以称之为写不超前规则,对所有的 store 指令来说,程序序在它们之前的指令在全局序上也要在它们之前,否则就会产生冲突。

RULE 2

定义:对同一地址(或者有地址重叠)的两个内存读(load 指令)A,B 指令也需要保证顺序,但是有两个例外:

  1. 都是读同一个写操作返回的值

  2. B 指令读的是在 A,B 程序序之间的一条写指令写入的值(这条写指令将数据存在了 store buffer 里)

示例如下:

	hart1    				   hart2
E:  li s0,1					F:	li s0,2
A:	lw t1,0(s0)				B:	lw t2,0(s0)
C:	sw t1,0(s1)				D:	sw t3,0(s1)

当两个 hart 对同一个地址写入数据时,如果不保证顺序,则 A,B 指令读到的结果有 4 种可能,这无疑是糟糕的,所以需要保持指令在程序中的顺序。但是如果我们将 F 指令修改为与 E 指令相同,那这时 A,B 指令其实读的都是同一个写操作的值,所以不会发生混乱,也就不需要保持 A,B 的顺序,这就是规定中的第一种特例。又或者我们在 A 指令之后(程序序),B 指令之前加入 F,这时 B 可以从 store buffer 里确定自己读的值,并且经过检查后这个值也会被写入内存,不会造成冲突,于是 B 也可以在全局内存序上领先于 A,这就是第二种特例。除这两种特例以外,其他的同一地址读都需要保证全局内存有序。

RULE 3

定义:A 指令执行原子内存操作(AMO)或条件写(SC)操作,与 B 指令(load)访问的地址有重叠,那么 A,B 必须在全局内存序上有序。

这里 A 指令执行的原子内存操作,即一系列的内存读,并且后面也可能有内存写操作,不可被打断,因此如果与 B 指令有地址重叠,那么必须要保证全局内存顺序,否则原子性就可能被破坏掉,B 读到的值也就没办法确定。而对于条件写(SC 指令),通常和 LL(Load-linked)一起出现,LL 运算返回目的地址的当前变量值,之后进行判断,只有当上一次加载的值在此期间没有更新时,SC 指令才会在该内存位置保存新值,否则 SC 指令失败,这同样也是一种原子操作,所以若 B 指令在该操作期间读,也将不能确定读到的值。

所以对于原子操作来说,如果两条指令有地址重叠,那么一定要保证全局内存有序。

RULE 4

定义:fence 指令前的 A 指令在全局内存序上领先于 fence 指令后的 B 指令。

这里规则定义了前趋集和后继集,分别包括了常规内存读写和 I/O 读写,也就是说:

fence [r],[w]

意味着 r 指令(read)在全局序领先于 w 指令(write)。

fence 指令用于提供相同硬件线程上写指令内存与指令获取之间的显式同步,约束了可以乱序执行的指令范围,便于跨越内存种类明确约束访问顺序。

RULE 5-7

定义:为了保证宽松一致性,原子内存操作(AMOs)和 LR/SC 操作可以选择使用原语。

原语,一般是指由若干条指令组成的程序段,用来实现某个特定功能,在执行过程中不可被中断。这里规定的原语有两个:acquire 和 release,分别用.aq 和.rl 来表示。所有程序序在 acquire 之后的操作在全局序中也在其后,所有程序序在 release 之前的操作在全局序中也在其之前,且程序序 acquire 在 release 之前,全局序中也应保持 acquire 在 release 之前。

前两个规定指出了分别利用.aq 和.rl 进行内存顺序约束的方法,即.aq 约束指令在其后执行,.rl 约束指令在其之前执行,但是二者同时出现时,也就是说一个程序段出现了两个原语保护区段,为了保证不会出现重叠冲突,规定了.aq 必须在.rl 之前执行。

A:  .aq
B:  li s0,1
C:  lw t1,0(s0)
D:  sw t1,0(s1)
E:  .rl

上述示例就是利用.aq 和.rl 进行顺序约束,整个指令的执行顺序就是程序序,因此.aqrl 的约束是顺序一致(Sequential Consistency,SC)内存序。

RULE 8

定义:一个 LR(load-reserve)操作必须在与之成对的 SC 操作全局可见之前确定值。

LR:  rd,(rs1)      SC: rd,rs2,(rs1)

LR 指令是从内存地址 rs1 中加载内容到 rd 寄存器,然后在 rs1 对应地址上设置保留标记(reservation set),SC 指令在把 rs2 值写到 rs1 地址之前,会先判断 rs1 内存地址是否有设置保留标记,如果设置了,则把 rs2 值正常写入到 rs1 内存地址里,并把 rd 寄存器设置成 0,表示保存成功。如果 rs1 内存地址没有设置保留标记,则不保存,并把 rd 寄存器设置成 1 表示保存失败。

因此 SC 操作需要前置的 LR 操作结果,这就构成了全局内存顺序约束。

RULE 9-11

定义:如果 B 指令对 A 指令存在语法依赖(地址,数据,控制依赖),那么在全局内存序上,A 领先于 B。

语法依赖是指一个指令的源操作数和前一条指令的目的操作数在同一个寄存器里,因此这条指令必须等前一条指令执行完,将寄存器清空后才能装入自己需要的操作数。这就保证了 A,B 指令在全局内存有序。

根据寄存器存取内容,又可以将语法依赖分为地址依赖,数据依赖和控制依赖。如下所示:

A:lw t0,0(s0)          B:lw t1,(t0)

A 指令要把 s0 寄存器的值传给 t0,而 B 指令则要将 t0 这个地址里的内容传给 t1,这就构成了地址依赖。

倘若 B 指令改为 lw t1,t0,可知 B 指令将 t0(A 指令的目的操作数寄存器)存的数据传给了 t1,这就是数据依赖。

而控制依赖,则是在 B 指令中存在分支转换语句,例如 bne t0,t1 Exit,并且对于 Exit 里的语句,也应该等 A 指令执行完成才能执行。

RULE 12-13

定义:对于 A,B 指令来说,如果在程序序中,B 在 M 指令后且 M 指令地址依赖 A 或者在同一 hart 下,B 返回的值是在它之前的写操作 M 写入的值且 M 指令地址或数据依赖于 A,那么全局内存序下,A 领先于 B。

也就是说,多条指令之间可能存在依赖关系,如果乱序的话,可能会破坏这些依赖,因此要保证多条指令的顺序,也可以称之为流水线依赖

A:  lw t0, (s0)
M:  sw t1, (t0)
B:  lw t2, (t0)

如上所示,M 地址依赖 A,所以 M 全局内存序在 A 之后,而 B 读的值是有 M 指令写入的,那么它应该等待 M 写入完毕,否则会造成冲突,因此 B 全局内存序在 M 之后,也就一定在 A 之后了。

其实最经常使用的规则是 rule1(写不超前)和 rule4-8 定义的 fence 指令,原子操作保证内存顺序执行,偶尔会使用 rule9-11 的语法依赖,而 rule12-13 几乎不怎么用,是为了保证操作和原子模型的合法。

内存模型公理

RISC-V 程序的执行遵循 RVWMO 内存一致性模型,前提是存在一个符合保留程序序的全局内存序,并且满足返回值公理、原子性公理和进度公理。下面我们分别介绍这三个公理。

返回值公理

定义:每个 load 操作读取到的每个字节都需要返回最新的 store 操作写入该字节的值,即读操作返回最近的对同一个地址写操作的值。

	li t0,1
	li t1,2
A:	sw t0,s0
B:	sw t1,s0
C:	lw t2,s0

由上述公理和规则可知,这段代码为顺序执行,那么 t2 的值就应该是 2,也就是 B 指令有 t1 写入的值,如果这是另一个 hart 又对 s0 写入新的值,那 t2 就是新写入的值,这就是返回值公理的体现。

原子性公理

定义:如果在同一 hart 下执行的原子内存操作(AMOs)读取了内存中的旧值,并且向内存中写入新值,这些操作之间不允许有其他 hart 对同一地址的操作;而对于成对的 LR/SC 操作来说,只有当 LR 设置了保留标记后,SC 才能继续内存操作,否则直接失败。

对于 AMO 来说,它本身就是原子的内存操作,而对于 LR/SC 来说,它的原子性体现在当目前这个 hart 保留有标记时,不允许其他 hart 对标记进行修改,只有执行完这对 LR/SC,才能执行其他的操作。

这条公理其实就是 rule5-8 的体现,是为了保证多拷贝原子性(multi-copy atomicity),有了这个保证,不同 hart 之间就不会发生数据冲突了,便于我们进行并发编程。

进度公理

定义:在全局内存序中,任何内存操作之前都不能有来自其他 hart 的无限序列的内存操作。

也就是说,一个 hart 写入的值必须在有限的时间内被其他 hart 看见,并且来自其他 hart 对这个值的 load 最终也能返回给它们。

         hart0							hart1                  hart2
     spinlock(operations on s0)    	  sw t1,s0               lw t0,s0

当 hart0 持有 spinlock 并且一直不释放时,由于 hart1 要向 s0 写入数据,它会等待 spinlock 的释放,这时根据进度公理,hart0 的操作必须在有限时间内结束,并且把资源释放,供给其他 hart 使用。如果 hart0 向 s0 写入了值,并且 hart2 需要 load 这个值时,公理保证了 hart0 会及时释放锁,然后将值保存在 s0 中,保证 hart2 可以进行内存读。

这条公理意味着更强的公平性,可以保证不同 hart 的序列都可以得到执行。

总结

RVWMO 模型提供了硬件与软件之间的一层接口,使软件程序员能根据这个模型实现并发编程,得益于它的宽松内存序。而 RISC-V 还提供了一种比 RVWMO 更严格的内存一致性模型,用于支持“全存储排序”,即 “Ztso” 扩展,主要是为了便于从 x86 体系结构向 RISC-V 迁移,提供了完全兼容 x86 架构的 RVTSO(RISC-V Total Store Ordering)内存模型。可以看出 RISC-V 正处在发展的黄金时期,与其他架构相比,具有强大的竞争力。

参考资料

  • https://riscv.org/wp-content/uploads/2018/05/14.25-15.00-RISCVMemoryModelTutorial.pdf
  • https://github.com/riscv/riscv-isa-manual
  • https://riscv.org/technical/specifications/
  • https://zhuanlan.zhihu.com/p/422848235
  • https://gitee.com/laokz/OS-kernel-test/tree/master/memorder
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值