从零开始的ARMv8操作系统内核实践 4 实现一个简单的自旋锁
Arm内存模型
在SMP,弱内存序的系统中, 为了实现多核间的同步,通常硬件需要实现两类硬件特性
- 互斥访问 2. 内存屏障
通过互斥访问, 可以实现核心间的同步; 通过内存屏障, 确保核心在解锁前对于内存的访问操作使得其他核心可见.
比如, 存在两个核心, 分别是Core0和Core1, Core0持有一个互斥锁, 写入数据后随即释放互斥锁, 而Core1在等待Core0持有的互斥锁, 在获得锁后读取Core0写入到内存的数据.
因为弱内存模型不保证对于核心间观察到的读写顺序是一致的, 所以即使Core0释放了锁后, Core1成功获得锁, 但由于缓存系统不一致的问题, Core1 可能仍然不能读取到 Core0 在解锁前的写入的数据.
Arm架构锁的实现
在通过锁实现核心间同步的时候, 通常会使用互斥地操作标志位实现.
在Arm架构上, 互斥访问有三类指令, 分别是
- Load Exclusive (LDXR): LDXR W|Xt, [Xn]
- Store Exclusive (STXR): STXR Ws, W|Xt, [Xn]
- Clear Exclusive access monitor
Arm架构实现互斥访问的原理与乐观锁的实现类似.
首先, 对于内存上指定地址的数据作为锁的标志位, 执行互斥读(LDXR)操作, 将指定位置的数据读入到寄存器中, 同时, 在互斥监视器(Local Exclusive Monitor)上标记这个位置内存的物理地址被此核心互斥访问中.
然后, 对读入的数据做一些计算, 比如计算是否非零等等
最后, 执行互斥写回指令(STXR), 将数据写回指定位置.
互斥写回指令是这样的:
首先, 检查互斥监视器上对应地址的标志是否为当前核心, 如果是执行写入, 并将Ws置零; 否则取消写入, 将Ws置为非零值, 报告写入不成功
下面是一个简单自旋锁实现:
void acquire_spin_lock(struct spinlock *lock){
while (lock->locked ||
__atomic_test_and_set(&lock->locked, __ATOMIC_ACQUIRE)
);
__sync_synchronize(); // 对于汇编 dmb ish 发起一个内存屏障 保证可见性
}
对应的Arm汇编
acquire_spin_lock(spinlock*): // @acquire_spin_lock(spinlock*)
mov w8, #1
.LBB0_1: // =>This Loop Header: Depth=1
ldrb w9, [x0]
cbnz w9, .LBB0_1
.LBB0_2: // Parent Loop BB0_1 Depth=1
ldaxrb w9, [x0]
stxrb w10, w8, [x0]
cbnz w10, .LBB0_2
cbnz w9, .LBB0_1
dmb ish
ret
代码逻辑:
首先, 检查标志位lock->locked, 若其已锁定, 开始自旋重复检查标志位; 如果还未锁定, 执行原子性的"testandset", 如果成功set, 代表此核心获得指定的自旋锁, 跳出循环, 执行"dmb ish"指令确保内存读写的可见性(见下面的"内存屏障章节"), 随后返回
内存屏障
前面提到, Armv8是一种弱内存模型(Weak Memory Ordering)架构,为了使CPU按照我们期望的顺序访问内存,我们需要使用显式的汇编指令提示CPU. 在Armv8平台上,共有三种屏障
- ISB Instruction Synchronization Barrier 此指令会清除指令流水线,只有它前面的指令执行完成后,才会执行之后的指令
- DMB Data Memory Barrier 此指令会阻止有关数据存储的指令重排,也就是说,所有在DMB之前的有关存取内存的指令(不包括取指令),对其他核心来说,都是可见的.
- DSB Data Synchronization Barrier 与DMB类似,但是它的限制更为严格.不仅仅是数据存取的指令,在DSB之后的所有指令都要等待之前的数据存储完成.
One-way barriers
除了上面的三个指令, Armv8还提供两种单向屏障, 限制前面的指令被重排到后面, 或者后面的指令重排到前面. 这两类指令相对于上述的双向屏障, 限制更弱, 性能也就更强.
- LDAR Load-acquire 对于相同的内存区域, 在此指令后的存取内存指令, 不得被重排至前面
- STLR Store-release 对于相同的内存区域, 所有在此指令之前的内存访问指令, 不得被重排至后面
自旋锁的实现
void acquire_spin_lock(struct spinlock *lock){
while (lock->locked ||
__atomic_test_and_set(&lock->locked, __ATOMIC_ACQUIRE)
);
__sync_synchronize();
}
对于上面提到的自旋锁, 还存在死锁的可能, 需要额外的设计.
当核心持有一个自旋锁时, 如果它再次进入中断, 而在中断处理程序中同样需要这把锁, 那么这个核心就会陷入死锁. 为了解决这个问题, 我们可以加入一个限制, 那就是: 当核心持有自旋锁的时候, 关闭中断
为了实现这个逻辑, 添加一对函数push_off pop_off
其中, push_off 标记当前核心持有锁的数量 +1 并关闭中断;
pop_off 标记当前核心持有锁的数量-1 如果核心不再持有自旋锁时, 开启中断
那么, 我们上锁的代码就要改为
void acquire_spin_lock(struct spinlock *lock){
push_off();
while (lock->locked ||
__atomic_test_and_set(&lock->locked, __ATOMIC_ACQUIRE)
);
__sync_synchronize();
}
对应的, 解锁代码:
void release_spin_lock(struct spinlock *lock){
if(!is_current_cpu_holding_spin_lock(lock)){
panic("release_spin_lock: the lock (%s) held by %lu can't be released by %lu \n",lock->name,lock->cpu->cpuid,cpuid());
}
lock->cpu = NULL;
__sync_synchronize();
__sync_lock_release(&lock->locked);
pop_off();
}

浙公网安备 33010602011771号