LA32R中的用户态整型指令
用LL/SC指令构建基本同步原语
现代操作系统中可能同时存在多个进程,每个进程内又可能包含多个同时执行的线程。一个线程正在操作的数据很可能被另一个线程访问1,这些能发起并发访问的线程主要有以下几种来源:
- 多处理器系统中,另一个处理器核上运行的线程;
- 同一个处理器核上,当前运行的线程遇到中断,在中断处理上下文中的线程;
- 同一个处理器核上,在中断处理后因为抢占调度而执行的其它线程。
当同一个数据被多个线程并发访问时,就可能出现数据竞争冒险。为了保证并发数据访问的正确性,需要引入同步和通信机制。通常用户程序开发者使用系统(软件)提供的同步原语来实现同步机制。我们这里只讨论经典的互斥同步机制。这种同步机制所用到的同步原语,其高效实现的关键是利用处理器硬件提供的原子访存硬件原语,即以原子方式读取并修改内存单元的功能。LA32R中是采用ll.w/sc.w指令对来支持实现原子访存硬件原语。下面我们以Linux中atomic_add_return()同步原语的实现来增加一些直观的认识。
atomic_add_return:
ll.w $t0, $a1, 0
add.w $t1, $t0, $a0
sc.w $t1, $a1, 0
beq $t1, $zero, atomic_add_return
add.w $a0, $t0, $a0
jr $ra
上面代码的核心部分是前4行构成的循环体。其中前3条构成一个Read-Modify-Write(RMW)操作序列,完成向指定内存位置加上一个值的功能。在这个操作序列中从ll.w读回数据到sc.w试图修改内存期间,如果有其它处理器核也试图修改同一内存位置,或者当前处理器核因中断或其它原因陷入异常处理了,那么sc.w不会执行修改内存的动作,同时其向$t1写入值0,表示未能成功地完成原子修改。那么beq $t1, $zero, atomic_add_return指令就会回跳至atomic_add_return标号处继续执行这个循环,直至内存的原子修改成功完成方才退出。
如果读者对其它同步原语的实现也想了解一下,可以查看内核源码中arch/loongarch/include/asm/atomic.h文件的内容。
通常的使用方式是用一对访问同一地址的ll.w和sc.w指令构成对于该地址的原子访存。这两条指令之间的虽然可以插入其它指令,但是要控制插入指令的数目,使得ll.w和sc.w指令对所在的循环体的取指行为尽量简单。ll.w和sc.w指令对之间的指令中尽量不要出现访存指令,除非程序设计人员能否确保执行过程中不会因为Cache替换、页表缺失等原因导致sc.w总是(或极大概率)不成功,造成出现死锁(或活锁)。ll.w和sc.w指令对的嵌套使用是强烈禁止的,因为一个处理器核上只能处理一个活跃的RMW操作序列,所以严格来说非最内层的ll.w和sc.w指令对的sc.w指令一定执行失败,这种代码不仅无意义而且极易构成死锁。至于说ll.w和sc.w的嵌套且交织使用,即形如ll.w A - ll.w B - sc.w A - sc.w B的指令序列,所有的sc.w指令都不会成功,是更加无意义的代码。
从指令集规范来看,ll.w指令可以后面不跟着配对的sc.w指令单独使用,但是作者自己尚未看到这样使用的场景。sc.w指令要想有可能成功完成,则一定要配合一个访问相同地址的ll.w指令。
最后再次告诫初学者,如果必须用ll.w和sc.w指令直接开发汇编代码,最好方式就是参考内核或C库中已有的各类同步原语的成熟实现“依样画葫芦”,能够最大程度的规避错误。
-
特别是有修改行为的访问。 ↩