【ARM AMBA AXI 入门 7 - AXI 协议中的独占访问 使用背景介绍】


请阅读【ARM AMBA AXI 总线 文章专栏导读】

文章目录

  • ARM 独占访问
    • 1.1. 什么是独占访问
      • 1.1.1 独占访问背景
      • 1.1.2 独占访问指令LDREX/STREX
      • 1.1.3 独立监视器
    • 1.2 spin_lock 与独占访问
      • 1.2.1 spin lock 机制
      • 1.2.2 spin lock 函数调用流程
      • 1.2.3 Arm64 spin lock实现

上篇文章:ARM AMBA AXI 入门 6 - AXI3 协议中的锁定访问之AxLOCK信号
下篇文章:ARM AMBA AXI 入门 8 - AXI 协议中 RID/ARID/AWID/WID 信号

ARM 独占访问

1.1. 什么是独占访问

什么是独占访问?就是处理器对某个内存地址的数据,在某个时间段内享有独有的访问。

1.1.1 独占访问背景

为什么要有独占访问,或者说何时需要独占访问呢
举个简单例子,假设在银行的服务器上,一个进程负责处理某个用户的账户余额,如果别的进程也来修改这段的数据,那么就需要一定的机制保证这段数据不会乱掉。直观的想法,就是给这段数据加上“”,只有“”的拥有者才有访问权限。当拥有者访问完毕后,需要去掉“”,这样别的处理器/进程可以继续访问。

1.1.2 独占访问指令LDREX/STREX

LDREXSTREX 独占访问指令是将单纯的更新内存的原子操作分成了两个独立的步骤:
1)LDREX用来读取内存中的值,并标记对该段内存的独占访问:

LDREX Rx, [Ry]

上面的指令意味着,读取寄存器Ry指向的4字节内存值,将其保存到Rx寄存器中,同时标记对Ry指向内存区域的独占访问。

如果执行LDREX指令的时候发现已经被标记为独占访问了,并不会对指令的执行产生影响。

2)而STREX在更新内存数值时,会检查该段内存是否已经被标记为独占访问,并以此来决定是否更新内存中的值:

STREX Rx, Ry, [Rz]

如果执行这条指令的时候发现已经被标记为独占访问了,则将寄存器Ry中的值更新到寄存器Rz指向的内存,并将寄存器Rx设置成0。指令执行成功后,会将独占访问标记位清除。

而如果执行这条指令的时候发现没有设置独占标记,则不会更新内存,且将寄存器Rx的值设置成1

一旦某条STREX指令执行成功后,以后再对同一段内存尝试使用STREX指令更新的时候,会发现独占标记已经被清空了,就不能再更新了,从而实现独占访问的机制。

1.1.3 独立监视器

【ARM AMBA AXI 入门 7 - AXI 协议中的独占访问 使用背景介绍】_第1张图片
一共有两种类型的独占监视器:

  • 每一个处理器内部都有一个本地监视器(Local Monitor);
  • 在整个系统范围内还有一个全局监视器(Global Monitor)。

如果要对非共享内存区中的值进行独占访问,只需要涉及本处理器内部的本地监视器就可以了;
而如果要对共享内存区中的内存进行独占访问,除了要涉及到本处理器内部的本地监视器外,由于该内存区域可以被系统中所有处理器访问到,因此还必须要由全局监视器来协调。

对于本地监视器来说,它只标记了本处理器对某段内存的独占访问,在调用LDREX指令时设置独占访问标志,在调用STREX指令时清除独占访问标志。

而对于全局监视器来说,它可以标记每个处理器对某段内存的独占访问。也就是说,当一个处理器调用LDREX访问某段共享内存时,全局监视器只会设置针对该处理器的独占访问标记,不会影响到其它的处理器。当在以下两种情况下,会清除某个处理器的独占访问标记:

  • 当该处理器调用LDREX指令,申请独占访问另一段内存时;
  • 当别的处理器成功更新了该段独占访问内存值时。

对于第二种情况,也就是说,当独占内存访问内存的值在任何情况下,被任何一个处理器更改过之后,所有申请独占该段内存的处理器的独占标记都会被清空。

另外,更新内存的操作不一定非要是STREX指令,任何其它存储指令都可以。但如果不是STREX的话,则没法保证独占访问性。

1.2 spin_lock 与独占访问

ARMv8 以前的 32 位系统中使用 LDREXSTREX 指令,从 ARMv8 起,它们被改名成了 LDXRSTXRLDREXSTREX指令操作本质上是很多 CPU 核去抢某个内存变量的独占访问。

在自旋锁、互斥锁以及读写锁等内核机制中都有LDREXSTREX指令的使用。

1.2.1 spin lock 机制

linux 中针对每一个 spin lock 会有两个计数。分别是nextowner,初始值为0

typedef struct {
	union {
		unsigned int slock;       //slock所占内存区域覆盖owner和next
		struct __raw_tickets {
			unsigned short owner;
			unsigned short next;
		} tickets;
	};
} arch_spinlock_t;

如果进程A申请锁时,首先会判断nextowner的值是否相等。如果相等就代表锁可以申请成功,否则原地自旋。直到ownernext的值相等才会退出自旋。

  • 假设完成锁的初始化之后,进程A申请锁成功,然后将next1,此时owner值为0next值为1
  • 此时进程B也来申请锁,保存next的值到局部变量tmp(tmp = 1)中。由于nextowner值不相等,因此进程B原地自旋读取owner的值,判断ownertmp是否相等,直到相等退出自旋状态。当然next的值还是加1,变成2
  • 进程A释放锁,此时会将owner的值加1,那么此时B进程ownertmp的值都是1,因此B进程获得锁。
  • B进程释放锁后,将owner的值加1
  • 最后ownernext都等于2,代表没有进程持有锁。

next 就是一个记录申请锁的次数,而owner是持有锁进程的计数值。

1.2.2 spin lock 函数调用流程

【ARM AMBA AXI 入门 7 - AXI 协议中的独占访问 使用背景介绍】_第2张图片

  • spin_lock 操作中,关闭了抢占,也就是其他进程无法再来抢占当前进程了;
  • spin_lock 函数中,关键逻辑需要依赖于体系结构的实现,也就是arch_spin_lock函数;
  • spin_unlock 函数中,关键逻辑需要依赖于体系结构的实现,也就是arch_spin_unlock函数;

1.2.3 Arm64 spin lock实现

linux/arch/arm64/include/asm/spinlock.h

static inline void arch_spin_lock(arch_spinlock_t *lock)
{
	unsigned int tmp;
	arch_spinlock_t lockval, newval;
 
	asm volatile(
	/* Atomically increment the next ticket. */
	ARM64_LSE_ATOMIC_INSN(
	/* LL/SC */
"	prfm	pstl1strm, %3\n"   //和 preloading cache 相关的操作,主要是为了性能考虑
"1:	ldaxr	%w0, %3\n"         //将 slock 的值保存在 lockval 这个临时变量中
"	add	%w1, %w0, %w5\n"       //将 spin lock 中的 next 加一
"	stxr	%w2, %w1, %3\n"    // lock = newval
"	cbnz	%w2, 1b\n",        //是否有其他PE的执行流插入?有的话,重来
	/* LSE atomics */
"	mov	%w2, %w5\n"
"	ldadda	%w2, %w0, %3\n"
	__nops(3)
	)
	/* Did we get the lock? */
"	eor	%w1, %w0, %w0, ror #16\n" // lockval 中的 next 域就是自己的号码牌,判断是否等于owner
"	cbz	%w1, 3f\n"                //如果等于,持锁进入临界区
	/*
	 * No: spin on the owner. Send a local event to avoid missing an
	 * unlock before the exclusive load.
	 */
"	sevl\n"
"2:	wfe\n"                         // 否则进入spin
"	ldaxrh	%w2, %4\n"             // (A): 其他cpu唤醒本cpu,获取当前owner值
"	eor	%w1, %w2, %w0, lsr #16\n"  // 自己的号码牌是否等于owner?
"	cbnz	%w1, 2b\n"             // 如果等于,持锁进入临界区,否者回到2,即继续 spin
	/* We got the lock. Critical section starts here. */
"3:"                               // 成功获取锁,
	: "=&r" (lockval), "=&r" (newval), "=&r" (tmp), "+Q" (*lock)
	: "Q" (lock->owner), "I" (1 << TICKET_SHIFT)
	: "memory");
}
static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
	unsigned long tmp;
 
	asm volatile(ARM64_LSE_ATOMIC_INSN(
	/* LL/SC */
	"	ldrh	%w1, %0\n"
	"	add	%w1, %w1, #1\n"
	"	stlrh	%w1, %0",
	/* LSE atomics */
	"	mov	%w1, #1\n"
	"	staddlh	%w1, %0\n"
	__nops(1))
	: "=Q" (lock->owner), "=&r" (tmp)
	:
	: "memory");
}

基本的代码逻辑的描述都已经嵌入代码中,这里需要特别说明的有两个知识点:

  • (1)Load-Acquire/Store-Release指令的应用
    Load-Acquire/Store-Release指令是 ARMv8 的特性,在执行 load 和 store 操作的时候顺便执行了 memory barrier 相关的操作,在spinlock这个场景,使用 Load-Acquire/Store-Release指令代替DMB指令可以节省一条指令。

   上面代码中的(A)就标识了使用Load-Acquire指令的位置。Store-Release指令在哪里呢?在arch_spin_unlock中,这里就不贴代码了。Load-Acquire/Store-Release指令的作用如下:

  • Load-Acquire 可以确保系统中所有的 observer 看到的都是该指令先执行,然后是该指令之后的指令(program order)再执行

  • Store-Release 指令可以确保系统中所有的 observer 看到的都是该指令之前的指令(program order)先执行,Store-Release指令随后执行

  • (2) 第二个知识点是关于在arch_spin_unlock代码中为何没有SEV指令?
    PE(n)对 x 地址发起了exclusive 操作的时候,PE(n) 的 Global monitor 从 open access 迁移到 exclusive access状态,来自其他PE上针对 x (该地址已经被mark for PE(n)) 的store操作会导致PE(n) 的 global monitor 从 exclusive access 迁移到 open access状态,这时候, PE(n) 的 Event register会被写入event,就好象生成一个event,将该PE唤醒,从而可以省略一个SEV的指令。

  • +表示在嵌入的汇编指令中,该操作数会被指令读取(也就是说是输入参数)也会被汇编指令写入(也就是说是输出参数)。
  • =表示在嵌入的汇编指令中,该操作数会是 write only 的,也就是说只做输出参数。
  • I 表示操作数是立即数

上篇文章:ARM AMBA AXI 入门 6 - AXI3 协议中的锁定访问之AxLOCK信号
下篇文章:ARM AMBA AXI 入门 8 - AXI 协议中 RID/ARID/AWID/WID 信号

推荐阅读:
https://www.pianshen.com/article/3787193786/
https://aijishu.com/a/1060000000255771
https://blog.csdn.net/qq_36115224/article/details/123063389
https://pages.cs.wisc.edu/~remzi/OSTEP/Chinese/28.pdf
https://codeantenna.com/a/KdZ7zrznhj
https://www.pianshen.com/article/71321328666/
http://www.wowotech.net/kernel_synchronization/445.html
http://www.wowotech.net/kernel_synchronization/spinlock.html

你可能感兴趣的:(#,ARM,AMBA,AXI,系列,AXI,独占访问,spin_lock,exclusive,访问,SEV,DMB)