这篇记录下内存锁是如何实现的。
以下是spin_lock()函数的代码调用流程片段:
注意__raw_spin_lock这里调用了preept_disable()函数关闭了cpu抢占。这个函数不用关心LOCK_CONTENDED宏的作用,知道这里调用do_raw_spin_lock()函数就可以,参数是lock。
锁的结构介绍:
从__spin_lock_debug开始初见端倪,先尝试for循环4096×HZ次加锁,这里并没有挂死,400多万次的尝试加锁很快的。加锁失败后调用arch_spin_lock进行正式加锁,这里才是关键,才会导致cpu挂死。
static inline unsigned int arch_spin_trylock(arch_spinlock_t *lock)
{
int tmp, tmp2, tmp3;
int inc = 0x10000;
__asm__ __volatile__ (
" .set push # arch_spin_trylock \n"
" .set noreorder \n"
" \n"
"1: ll %[ticket], %[ticket_ptr] \n"
" srl %[my_ticket], %[ticket], 16 \n"
" andi %[now_serving], %[ticket], 0xffff \n"
" bne %[my_ticket], %[now_serving], 3f \n"
" addu %[ticket], %[ticket], %[inc] \n"
" sc %[ticket], %[ticket_ptr] \n"
" beqz %[ticket], 1b \n"
" li %[ticket], 1 \n"
"2: \n"
" .subsection 2 \n"
"3: b 2b \n"
" li %[ticket], 0 \n"
" .previous \n"
" .set pop \n"
: [ticket_ptr] "+" GCC_OFF_SMALL_ASM() (lock->lock),
[ticket] "=&r" (tmp),
[my_ticket] "=&r" (tmp2),
[now_serving] "=&r" (tmp3)
: [inc] "r" (inc));
smp_llsc_mb();
return tmp;
}
try lock函数, 通过【bne %[my_ticket], %[now_serving], 3f】这个代码可以看出如果家解锁不对称直接调到标签3,标签3会将【li %[ticket], 0】函数会将ticket作为返回值返回,那么父函数for循环会一直循环多次。
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
int my_ticket;
int tmp;
int inc = 0x10000;
__asm__ __volatile__ (
" .set push # arch_spin_lock \n"
" .set noreorder \n"
" \n"
"1: ll %[ticket], %[ticket_ptr] \n"
" addu %[my_ticket], %[ticket], %[inc] \n"
" sc %[my_ticket], %[ticket_ptr] \n"
" beqz %[my_ticket], 1b \n"
" srl %[my_ticket], %[ticket], 16 \n"
" andi %[ticket], %[ticket], 0xffff \n"
" bne %[ticket], %[my_ticket], 4f \n"
" subu %[ticket], %[my_ticket], %[ticket] \n"
"2: \n"
" .subsection 2 \n"
"4: andi %[ticket], %[ticket], 0xffff \n"
" sll %[ticket], 5 \n"
" \n"
"6: bnez %[ticket], 6b \n"
" subu %[ticket], 1 \n"
" \n"
" lhu %[ticket], %[serving_now_ptr] \n"
" beq %[ticket], %[my_ticket], 2b \n"
" subu %[ticket], %[my_ticket], %[ticket] \n"
" b 4b \n"
" subu %[ticket], %[ticket], 1 \n"
" .previous \n"
" .set pop \n"
: [ticket_ptr] "+" GCC_OFF_SMALL_ASM() (lock->lock),
[serving_now_ptr] "+m" (lock->h.serving_now),
[ticket] "=&r" (tmp),
[my_ticket] "=&r" (my_ticket)
: [inc] "r" (inc));
smp_llsc_mb();
}
该函数不仔细分析了,如果连续调用了两次的spin_lock会导致该汇编一直循环在【6: bnez %[ticket], 6b】,导致死锁。
首先这里参考了两篇文章:
1、https://www.ibm.com/developerworks/cn/linux/l-cn-spinlock_mips/index.html
2、https://blog.csdn.net/mrwangwang/article/details/20526785
C语言是无法做到原子操作的,这个只能借助CPU来完成,每种架构的CPU实现方案不一样,x86就自带原子操作的汇编,mips的实现就稍微复杂一下,通过两条汇编指令LL和SC来完成原子操作。
首先我们要支持对于多核处理器,每个核心都有自己独立的一套寄存器,这点很重要。
要明确多核心下对共享数据的冲突访问的发生过程。核心A和核心B都有独立的寄存器Ra和Rb,加入A和B同时读取同一块内存地址中的数据到寄存器Ra和Rb中,Ra+=1,此时Rb也开始进行Rb+=1,最后二者都将数据刷到内存中。但是注意一点,我们是要要内存中的数据进行两次加的操作,就如果引用计数,两个核心都引用了这个内存的数据,但结果上来看该内存的数据仅仅加了1。这就是问题所在。
LL 指令的功能是从内存中读取一个字,以实现接下来的 RMW(Read-Modify-Write) 操作;SC 指令的功能是向内存中写入一个字,以完成前面的 RMW 操作。LL/SC 指令的独特之处在于,它们不是一个简单的内存读取/写入的函数,当使用 LL 指令从内存中读取一个字之后,比如 LL d, off(b),处理器会记住 LL 指令的这次操作(会在 CPU 的寄存器中设置一个不可见的 bit 位),同时 LL 指令读取的地址 off(b) 也会保存在处理器的寄存器中。接下来的 SC 指令,比如 SC t, off(b),会检查上次 LL 指令执行后的 RMW 操作是否是原子操作(即不存在其它对这个地址的操作),如果是原子操作,则 t 的值将会被更新至内存中,同时 t 的值也会变为1,表示操作成功;反之,如果 RMW 的操作不是原子操作(即存在其它对这个地址的访问冲突),则 t 的值不会被更新至内存中,且 t 的值也会变为0,表示操作失败。
那么SC是如何实现RMW是否有冲突的操作的,在一般实现中,处理器有两个专门的域给LL和SC指令,即上文中的“不可见的bit位”以及保存ll操作地址的“寄存器”。在See mips run这本书中说到是利用mips 协处理器检测地址来实现,但具体怎么实现没有说明。也可能在LL之后,处理器核心会监测各种事件,当发生异常或者有别的处理器核心对该地址发了invalid请求时,会将不可见的bit位重置,那么当前操作的处理器核心发现bit被重置了,从而导致了SC的失败。
这是See mips run中的描述:
以上都是mips处理器来实现对内存中的数据进行原子的读写操作,spin_lock就是利用这个来实现。
根据以上理论我们开始分析mips汇编代码:
假如这里连续调用了两次的spin_lock()函数,那么第二次调用的时内存中的数据应该是0x30002,
有[ticket_ptr] "+" GCC_OFF_SMALL_ASM() (lock->lock),看出ticket_ptr中存放的就是lock数据。%表示寄存器操作,
" .set push # arch_spin_trylock \n"
" .set noreorder \n"
" \n"
"1: ll %[ticket], %[ticket_ptr] \n" //mips ll特殊加载到ticket中,ticket=0x30002
" srl %[my_ticket], %[ticket], 16 \n" // ticket=0x30002,my_ticket=0x3
" andi %[now_serving], %[ticket], 0xffff \n" // ticket=0x30002,my_ticket=0x3, now_serving = 0x2
" bne %[my_ticket], %[now_serving], 3f \n" // ticket=0x30002,my_ticket=0x3, now_serving = 0x2,别忘了延迟槽,此时结果是不相等跳到了标签3
" addu %[ticket], %[ticket], %[inc] \n" // ticket=0x40002,my_ticket=0x3, now_serving = 0x2
" sc %[ticket], %[ticket_ptr] \n"
" beqz %[ticket], 1b \n"
" li %[ticket], 1 \n"
"2: \n"
" .subsection 2 \n"
"3: b 2b \n"
" li %[ticket], 0 \n" //ticket 赋值为0,该函数会返回ticket,从父函数可以看出会一直循环
" .previous \n"
" .set pop \n"
有以上分析可以看出,spin_lock流程是通过mips处理器提供的对内存的原子操作来实现对一段代码的防冲突访问,根本上是利用的astomic_add等操作,通过判断内存中的数据来确定该段代码是否被允许进入。
以下是测试代码,测试spin_lock和spin_unlock操作后内存中的值是多少:
函数:
结果:
可以看出,加解锁是对不同的字节操作。