请阅读【ARM AMBA AXI 总线 文章专栏导读】
上篇文章:ARM AMBA AXI 入门 6 - AXI3 协议中的锁定访问之AxLOCK信号
下篇文章:ARM AMBA AXI 入门 8 - AXI 协议中 RID/ARID/AWID/WID 信号
什么是独占访问?就是处理器对某个内存地址的数据,在某个时间段内享有独有的访问。
为什么要有独占访问,或者说何时需要独占访问呢?
举个简单例子,假设在银行的服务器上,一个进程负责处理某个用户的账户余额,如果别的进程也来修改这段的数据,那么就需要一定的机制保证这段数据不会乱掉。直观的想法,就是给这段数据加上“锁”,只有“锁”的拥有者才有访问权限。当拥有者访问完毕后,需要去掉“锁”,这样别的处理器/进程可以继续访问。
LDREX
和 STREX
独占访问指令是将单纯的更新内存的原子操作分成了两个独立的步骤:
1)LDREX
用来读取内存中的值,并标记对该段内存的独占访问:
LDREX Rx, [Ry]
上面的指令意味着,读取寄存器Ry指向的4字节内存值,将其保存到Rx寄存器中,同时标记对Ry指向内存区域的独占访问。
如果执行LDREX指令的时候发现已经被标记为独占访问了,并不会对指令的执行产生影响。
2)而STREX在更新内存数值时,会检查该段内存是否已经被标记为独占访问,并以此来决定是否更新内存中的值:
STREX Rx, Ry, [Rz]
如果执行这条指令的时候发现已经被标记为独占访问了,则将寄存器Ry中的值更新到寄存器Rz指向的内存,并将寄存器Rx设置成0。指令执行成功后,会将独占访问标记位清除。
而如果执行这条指令的时候发现没有设置独占标记,则不会更新内存,且将寄存器Rx
的值设置成1
。
一旦某条STREX
指令执行成功后,以后再对同一段内存尝试使用STREX
指令更新的时候,会发现独占标记已经被清空了,就不能再更新了,从而实现独占访问的机制。
如果要对非共享内存区中的值进行独占访问,只需要涉及本处理器内部的本地监视器就可以了;
而如果要对共享内存区中的内存进行独占访问,除了要涉及到本处理器内部的本地监视器外,由于该内存区域可以被系统中所有处理器访问到,因此还必须要由全局监视器来协调。
对于本地监视器来说,它只标记了本处理器对某段内存的独占访问,在调用LDREX指令时设置独占访问标志,在调用STREX指令时清除独占访问标志。
而对于全局监视器来说,它可以标记每个处理器对某段内存的独占访问。也就是说,当一个处理器调用LDREX
访问某段共享内存时,全局监视器只会设置针对该处理器的独占访问标记,不会影响到其它的处理器。当在以下两种情况下,会清除某个处理器的独占访问标记:
LDREX
指令,申请独占访问另一段内存时;对于第二种情况,也就是说,当独占内存访问内存的值在任何情况下,被任何一个处理器更改过之后,所有申请独占该段内存的处理器的独占标记都会被清空。
另外,更新内存的操作不一定非要是STREX
指令,任何其它存储指令都可以。但如果不是STREX
的话,则没法保证独占访问性。
在 ARMv8 以前的 32 位系统中使用 LDREX
和 STREX
指令,从 ARMv8 起,它们被改名成了 LDXR
和 STXR
。LDREX
和 STREX
指令操作本质上是很多 CPU 核去抢某个内存变量的独占访问。
在自旋锁、互斥锁以及读写锁等内核机制中都有LDREX
和STREX
指令的使用。
linux 中针对每一个 spin lock 会有两个计数。分别是next
和owner
,初始值为0
。
typedef struct {
union {
unsigned int slock; //slock所占内存区域覆盖owner和next
struct __raw_tickets {
unsigned short owner;
unsigned short next;
} tickets;
};
} arch_spinlock_t;
如果进程A申请锁时,首先会判断next
和owner
的值是否相等。如果相等就代表锁可以申请成功,否则原地自旋。直到owner
和next
的值相等才会退出自旋。
next
加1
,此时owner
值为0
,next
值为1
。next
的值到局部变量tmp(tmp = 1
)中。由于next
和owner
值不相等,因此进程B原地自旋读取owner
的值,判断owner
和tmp
是否相等,直到相等退出自旋状态。当然next
的值还是加1
,变成2
。owner
的值加1
,那么此时B进程的owner
和tmp的值都是1
,因此B进程获得锁。owner
的值加1
。owner
和next
都等于2
,代表没有进程持有锁。next 就是一个记录申请锁的次数,而owner是持有锁进程的计数值。
spin_lock
操作中,关闭了抢占,也就是其他进程无法再来抢占当前进程了;spin_lock
函数中,关键逻辑需要依赖于体系结构的实现,也就是arch_spin_lock函数;spin_unlock
函数中,关键逻辑需要依赖于体系结构的实现,也就是arch_spin_unlock
函数; 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");
}
基本的代码逻辑的描述都已经嵌入代码中,这里需要特别说明的有两个知识点:
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