Linux内核spinlock实现

Linux内核spinlock实现


linux内核自旋锁spinlock实现详解(基于ARM处理器)

内核当发生访问资源冲突的时候,可以有两种锁的解决方案选择:

  • 一个是原地等待
  • 一个是挂起当前进程,调度其他进程执行(睡眠)

spinlock 是内核中提供的一种比较常见的锁机制,自旋锁是“原地等待”的方式 解决资源冲突的,即,一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能够原地“打转”(忙等待)。由于自旋锁的这个忙等待的特性,注定了它使用场景上的限制: 自旋锁不应该被长时间的持有(消耗 CPU 资源)

本文基于linux-4.9.262版本

1. 自旋锁结构

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 {
	union {
		u32 slock;
		struct __raw_tickets {
#ifdef __ARMEB__
			u16 next;
			u16 owner;
#else
			u16 owner;
			u16 next;
#endif
		} tickets;
	};
} arch_spinlock_t;

说明:

ARMEB = ARM EABI Big-endian #大端字节序
ARMEL = ARM EABI Little-endian #小端字节序
EABI = Embedded Application Binary Interface

u16 next表示下一个可以获取自旋锁的处理器,处理器请求自旋锁的时候会保存该值并对该值加1,然后与owner比较,检查是否可以获取到自旋锁,每请求一次next都加1
u16 owner表示当前获取到/可以获取自旋锁的处理器,每释放一次都加1,这样next与owner就保存一致

2. 获取自旋锁

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

#define raw_spin_lock(lock)	_raw_spin_lock(lock)

// include/linux/spinlock_api_up.h
#define _raw_spin_lock(lock)			__LOCK(lock)
#define __LOCK(lock) \
  do { preempt_disable(); ___LOCK(lock); } while (0)
// preempt_disable禁止内核抢占
#define ___LOCK(lock) \
  do { __acquire(lock); (void)(lock); } while (0)
# define __acquire(x) (void)0

// include/linux/spinlock_api_smp.h
void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)		__acquires(lock);

// kernek/locking/spinlock.c
#ifndef CONFIG_INLINE_SPIN_LOCK
void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
	__raw_spin_lock(lock);
}

// include/linux/spinlock_api_smp.h
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
	preempt_disable();
	spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
	LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}
// include/linux/lockdep.h
#define LOCK_CONTENDED(_lock, try, lock) \
	lock(_lock)

LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);可以展开为
    do_raw_spin_lock(lock);

// linux-4.9.262/kernel/locking/spinlock_debug.c
/*
 * We are now relying on the NMI watchdog to detect lockup instead of doing
 * the detection here with an unfair lock which can cause problem of its own.
 */
void do_raw_spin_lock(raw_spinlock_t *lock)
{
	debug_spin_lock_before(lock);
	arch_spin_lock(&lock->raw_lock);
	debug_spin_lock_after(lock);
}

//linux-4.9.262/arch/arm/include/asm/spinlock.h
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"             //[1]
"	add	%1, %0, %4\n"               //[2]
"	strex	%2, %1, [%3]\n"         //[3]
"	teq	%2, #0\n"                   //[4]
"	bne	1b"                         //[5]
	: "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
	: "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
	: "cc");

	while (lockval.tickets.next != lockval.tickets.owner) {
		wfe();
		lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);
	}

	smp_mb();  // 适用于MP的内存屏障
}

我们先来看看LDREX和STREX两条指令的语义。其实LDREX和STREX指令,是将单纯的更新内存的原子操作分成了两个独立的步骤。

(1) LDREX用来读取内存中的值,并标记对该段内存的独占访问:

LDREX Rx, [Ry]

上面的指令意味着,读取寄存器Ry指向的4字节内存值,将其保存到Rx寄存器中,同时标记对Ry指向内存区域的独占访问。

如果执行LDREX指令的时候发现已经被标记为独占访问了,并不会对指令的执行产生影响。

(2) 而STREX在更新内存数值时,会检查该段内存是否已经被标记为独占访问,并以此来决定是否更新内存中的值:

STREX Rx, Ry, [Rz]

如果执行这条指令的时候发现Rz已经被标记为独占访问了,则将寄存器Ry中的值更新到寄存器Rz指向的内存,并将寄存器Rx设置成0。指令执行成功后,会将独占访问标记位清除。

而如果执行这条指令的时候发现没有设置独占标记,则不会更新内存,且将寄存器Rx的值设置成1。

一旦某条STREX指令执行成功后,以后再对同一段内存尝试使用STREX指令更新的时候,会发现独占标记已经被清空了,就不能再更新了,从而实现独占访问的机制。

[1]将%3指向的内存数值 保存给%0的内存位置。lockval = lock->slock (如果lock->slock没有被其他处理器独占,则标记当前执行处理器对lock->slock地址的独占访问;否则不影响)

[2] newval = lockval + (1 << TICKET_SHIFT)

[3] strex tmp, newval, [&lock->slock] 。如果当前执行处理器没有独占lock->slock地址的访问,不进行存储,将tmp设置为1;如果当前处理器已经独占lock->slock内存访问,则对内存进行写,返回0,清除独占标记。lock->tickets.next = lock->tickets.next + 1

[4] teq %2, #0\n" 检查是否写入成功lockval.tickets.next

[5]bne 1b 如果[4]中tmp不等于0,返回标签1处继续执行

	while (lockval.tickets.next != lockval.tickets.owner) { // 初始化时lock->tickets.owner、lock->tickets.next都为0,假设第一次执行arch_spin_lock,lockval = *lock,lock->tickets.next++,lockval.tickets.next等于lockval.tickets.owner,获取到自旋锁;自旋锁未释放,第二次执行的时候,lock->tickets.owner = 0, lock->tickets.next = 1,拷贝到lockval后,lockval.tickets.next != lockval.tickets.owner,会执行wfe等待被自旋锁释放被唤醒,自旋锁释放时会执行lock->tickets.owner++,lockval.tickets.owner重新赋值
		wfe(); // 暂时中断挂起执行
		lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner); // 重新读取lock->tickets.owner
	}

__asm__ __volatile__(__asm__用于指示编译器在此插入汇编语句, __volatile__用于告诉编译器,严禁将此处的汇编语句与其它的语句重组合优化。即:原原本本按原来的样子处理这这里的汇编

3. 释放自旋锁

static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
	smp_mb();
	lock->tickets.owner++; 
	dsb_sev(); // 执行sev指令,唤醒wfe等待的处理器
}

lock->tickets.owner++作用:
lock->tickets.owner增加1,下一个被唤醒的处理器会检查该值是否与自己的lockval.tickets.next相等,lock->tickets.owner代表可以获取的自旋锁的处理器,lock->tickets.next你一个可以获取的自旋锁的owner;处理器获取自旋锁时,会先读取lock->tickets.next用于与lock->tickets.owner比较并且对lock->tickets.next加1,下一个处理器获取到的lock->tickets.next就与当前处理器不一致了,两个处理器都与lock->tickets.owner比较,肯定只有一个处理器会相等,自旋锁释放时时对lock->tickets.owner加1计算,因此,先申请自旋锁多处理器lock->tickets.next值更新,自然先获取到自旋锁

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Erice_s

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值