Linux 内核 spin_lock 原理(几个问题)

对于non-preemptive的linux kernel,只要在内核态,就不会发生进程调度,因此,这种场景下,共享数据根本不需要保护。

当打开premptive选项后,事情变得复杂了,我们考虑下面的场景:
(1)进程A在某个系统调用过程中访问了共享资源R
(2)进程B在某个系统调用过程中也访问了共享资源R
假设在A访问共享资源R的过程中发生了中断,中断唤醒了沉睡中的,优先级更高的B,在中断返回现场的时候,发生进程切换,B启动执行,并通过系统调用访问了R,如果没有锁保护,则会出现两个thread进入临界区,导致程序执行不正确。(没有锁会出现同时进入临界区的情况)

加spin_lock: A在进入临界区之前获取了spin lock,同样的,在A访问共享资源R的过程中发生了中断,中断唤醒了沉睡中的,优先级更高的B,B在访问临界区之前仍然会试图获取spin lock,这时候由于A进程持有spin lock而导致B进程进入了永久的spin……怎么破?linux的kernel很简单,在A进程获取spin lock的时候,禁止本CPU上的抢占(上面的永久spin的场合仅仅在本CPU的进程抢占本CPU的当前进程这样的场景中发生)。如果A和B运行在不同的CPU上,那么情况会简单一些:A进程虽然持有spin lock而导致B进程进入spin状态,不过由于运行在不同的CPU上,A进程会持续执行并会很快释放spin lock,解除B进程的spin状态。(加锁后却不禁用本地抢占,会出现优先级更高的进程持续自旋,导致死锁)

1.为何要引入spin_lock来实现互斥?
如果在进程上下文被访问,完全可以使用信号量semaphore机制来实现互斥。如果在中断上下文被访问呢? 就不能使用semaphore来实现互斥,因为semaphore会引起睡眠的。这时候就引入了spin_lock

2.spin lock为何需要和禁止local中断联合使用?何时和禁止中断下半部联合使用?
假设CPU0上的进程A持有spin lock进入临界区,这时候,外设P发生了中断事件,如果外设P的中断事件被调度到了CPU0,CPU0上的进程A在持有spin lock的状态下被中断上下文抢占,而抢占它的CPU0上的handler在进入临界区之前仍然会试图获取spin lock,悲剧发生了,CPU0上的P外设的中断handler永远的进入spin状态,这时候,CPU1上的进程B也不可避免在试图持有spin lock的时候失败而导致进入spin状态。
(加锁后却不禁用本地抢占,会出现优先级更高的进程持续自旋,导致死锁)
把上面的场景简单修改一下:外设P不是中断handler中访问共享资源R,而是在的bottom half中访问。使用spin lock+禁止本地中断(spin_lock_irq防止和中断并发访问临界区)当然是可以达到保护共享资源的效果,但是使用牛刀来杀鸡似乎有点小题大做,这时候disable bottom half(spin_lock_bh)就OK了。
(加锁后却不禁用中断下半部,会出现优先级更高的进程持续自旋,导致死锁)

3.spin_lock 如何实现?
首先定义一个spinlock_t的数据类型,其本质上是一个整数值(对该数值的操作需要保证原子性),该数值表示spin lock是否可用。初始化的时候被设定为1。当thread想要持有锁的时候调用spin_lock函数,该函数将spin lock那个整数值减去1,然后进行判断,如果等于0,表示可以获取spin lock,如果是负数,则说明其他thread的持有该锁,本thread需要spin。
spinlock_t 结构体:

typedef struct spinlock {
	union {
		struct raw_spinlock rlock;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
		struct {
			u8 __padding[LOCK_PADSIZE];
			struct lockdep_map dep_map;
		};
#endif
	};
} spinlock_t;

typedef struct raw_spinlock {
	arch_spinlock_t raw_lock;
#ifdef CONFIG_GENERIC_LOCKBREAK
	unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
	unsigned int magic, owner_cpu;
	void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
	struct lockdep_map dep_map;
#endif
} raw_spinlock_t;

对于UP系统,

typedef struct { } arch_spinlock_t;  // 一切皆是空相

即便是打开的preempt选项,spin lock不过就是disable preempt而已,不需定义什么spin lock的变量。

对于SMP系统,

typedef struct {
			union {
				u32 slock;
				struct __raw_tickets {
#ifdef __ARMEB__
					u16 next;
					u16 owner;
#else
					u16 owner;
					u16 next;
#endif
				} tickets;
			};
} arch_spinlock_t;

spinlock 的代码:

static __always_inline void spin_lock(spinlock_t *lock)
{
	raw_spin_lock(&lock->rlock);
}

对于up系统:

#define _raw_spin_lock(lock)			__LOCK(lock)
#define __LOCK(lock) \
  do { preempt_disable(); ___LOCK(lock); } while (0)   // 本质上就是一个preempt_disable而已

对于SMP系统:

static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
	preempt_disable();
	spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); // 可以略过,这是和运行时检查锁的有效性有关的,如果没有定义CONFIG_LOCKDEP其实就是空函数
	LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

4.早期spin_lock为何会有不公平?
在这里插入图片描述
lock本质是保存在main memory中的,由于cache的存在,当然不需要每次都有访问main memory。在多核架构下,每个CPU都有自己的L1 cache,保存了lock的数据。假设CPU0获取了spin lock,那么执行完临界区,在释放锁的时候会调用smp_mb invalid其他忙等待的CPU的L1 cache,这样后果就是释放spin lock的那个cpu可以更快的访问L1 cache,操作lock数据,从而大大增加的下一次获取该spin lock的机会。

5.ticket-based spin_lock的描述
owner和clock相等的时候,表示没有线程持有锁;不相等时,表示有线程持有锁。
最开始的时候,slock被赋值为0,也就是说owner和next都是0,owner和next相等,表示unlocked。当第一个个thread调用spin_lock来申请lock的时候,这时候该thread持有该spin lock,并且执行next++,也就是将next设定为1。如果该thread执行很快,没有其他thread来竞争就调用spin_unlock了,这时候执行owner++,也就是将owner设定为1,此时又回到没有线程持有锁的状态。
然后第二个thread调用spin_lock来申请lock,next++之后等于2;此thread持锁太久,持续而来的thread会使next不断增加,而owner保存不变。当第二个thread执行spin_unlock,owner++等于2,表示持有2的那个线程可以进入临界区了。

6.老版spin_lock的源码分析(更新锁的状态成功,代表进入临界区)
主要是以SMP系统分析,

void __lockfunc _spin_lock(spinlock_t *lock)
{
    preempt_disable();    // 关闭掉抢占
    spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); // 调试使用,系统没有开启CONFIG_DEBUG_LOCK_ALLOC配置的话,这样代码啥都没干
    _raw_spin_lock(lock);
}
define _raw_spin_lock(lock)     __raw_spin_lock(&(lock)->raw_lock)
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
    unsigned long tmp;
    __asm__ __volatile__(
"1: ldrex   %0, [%1]\n"            // c语言 tmp=lock->lock ,读取lock的状态赋值给tmp
"   teq %0, #0\n"                  // c语言 if(tmp == 0) , 判断lock的状态是否为0。如果不为0,说明自旋锁处于上锁状态,执行bne 1b指令,跳到标号1处不停执行。
"   strexeq %0, %2, [%1]\n"       // lock->lock=1 , 使用常量1来更新锁的状态,并将执行结果放入到tmp中,执行成功, tmp会清零
"   teqeq   %0, #0\n"            // if(tmp == 0)  用来判断tmp是否为0,如果为0,表明更新锁的状态成功;如果不为0表明锁的状态没哟更新成功,执行”bne 1b”,跳转到标号1继续执行。
"   bne 1b"
    : "=&r" (tmp)
    : "r" (&lock->lock), "r" (1)
    : "cc");
    smp_mb();
}

7.新版spin_lock的源码分析

ARM32 上spin_lock代码实现

static inline void arch_spin_lock(arch_spinlock_t *lock)
{
    unsigned long tmp;
    u32 newval;
    arch_spinlock_t lockval;
 
    prefetchw(&lock->slock);
    __asm__ __volatile__(
"1: ldrex   %0, [%3]\n"       // lockval = lock->slock  读取锁的值赋值给lockval
"   add %1, %0, %4\n"        // newval = lockval +(1 << TICKET_SHIFT)  将next++之后的值存在newval中
"   strex   %2, %1, [%3]\n"  // lock->slock = newval, tmp = 0  将新的值存在lock中,将是否成功结果存入在tmp中
"   teq %2, #0\n"     
"   bne 1b"                 // if tmp != 0 , goto 1
    : "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
    : "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
    : "cc");
 
    while (lockval.tickets.next != lockval.tickets.owner) {
        wfe();// 当tickets中的next和owner不相等的时候,说明临界区在忙, 需要等待。然后cpu会执行wfe指令。
        lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner); // 当其他cpu忙完之后,会更新owner的值,如果owner的值如果与next值相同,那到next号的cpu执行。
    }      
    smp_mb();
    }

ARM64 上spin_lock代码实现

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. */
"   prfm    pstl1strm, %3\n"            // 将lock变量读到cache,增加访问速度
"1: ldaxr   %w0, %3\n"                 // lockval = lock
"   add %w1, %w0, %w5\n"              // newval = lockval +  TICKET_SHIFT
"   stxr    %w2, %w1, %3\n"           // lock = newval , tmp = 0
"   cbnz    %w2, 1b\n"                // if (tmp != 0) goto 1
    /* Did we get the lock? */
"   eor %w1, %w0, %w0, ror #16\n"     // lockval ^ lockval循环右移16位,将结果赋予 newval , 判断next是否等于owner
"   cbz %w1, 3f\n"                    // if ( newval == 0 )  跳转到3 ,进入临界区
    /*
     * No: spin on the owner. Send a local event to avoid missing an
     * unlock before the exclusive load.
     */
"   sevl\n"
"2: wfe\n"                              // 自旋等待
"   ldaxrh  %w2, %4\n"                // tmp = lock -> owner , 获取当前的Owner值存放在tmp中
"   eor %w1, %w2, %w0, lsr #16\n"     //  if(next == owner)  判断next是否等于owner
"   cbnz    %w1, 2b\n"                //  如果不等跳到标号2自旋
    /* 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");
}
CBZ & CBNZ
比较,Rn 为零则跳转;比较,为非零则跳转。
语法
CBZ Rn, label

CBNZ Rn, label

8.新版spin_unlock的源码分析

static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
    asm volatile(
"   stlrh   %w1, %0\n"         //   lock->owner = lock->owner + 1
    : "=Q" (lock->owner)
    : "r" (lock->owner + 1)
    : "memory");
}

解锁的操作相对简单,就是给owner执行加1的操作。
当 next == owner 时 , 可以进入临界区。

9.为何spin_lock不能睡眠?

#define spin_lock(lock)
do { preempt_disable(); __acquire(lock); (void)(lock);} while(0)

进程1:关抢占,获得锁,睡眠调度(关抢占扔可以主动放弃cpu,只是不会被高优先级的进程中断此进程)

进程2:关抢占(空操作),获得锁失败,反复尝试获得锁(已关抢占,无进程能阻止该进程做傻事了)

参考博客: http://www.wowotech.net/kernel_synchronization/spinlock.html

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值