【linux 内核】 锁机制的实现 spin_lock()函数

这篇记录下内存锁是如何实现的。

以下是spin_lock()函数的代码调用流程片段:

 

【linux 内核】 锁机制的实现 spin_lock()函数_第1张图片

【linux 内核】 锁机制的实现 spin_lock()函数_第2张图片

    注意__raw_spin_lock这里调用了preept_disable()函数关闭了cpu抢占。这个函数不用关心LOCK_CONTENDED宏的作用,知道这里调用do_raw_spin_lock()函数就可以,参数是lock。

【linux 内核】 锁机制的实现 spin_lock()函数_第3张图片

【linux 内核】 锁机制的实现 spin_lock()函数_第4张图片

锁的结构介绍:

【linux 内核】 锁机制的实现 spin_lock()函数_第5张图片

【linux 内核】 锁机制的实现 spin_lock()函数_第6张图片

从__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操作后内存中的值是多少:

函数:

【linux 内核】 锁机制的实现 spin_lock()函数_第7张图片

结果:

【linux 内核】 锁机制的实现 spin_lock()函数_第8张图片

 

可以看出,加解锁是对不同的字节操作。

你可能感兴趣的:(linux内核)