关抢占 自旋锁_也说自旋锁

本文详细介绍了自旋锁的发展历程,从早期简单的自旋锁到更公平的Ticket Spinlock,再到减少Cache颠簸的MCS Spinlock。Ticket Spinlock通过排队机制提高了公平性,但仍然存在性能问题,MCS Spinlock则通过让每个CPU在本地自旋避免了大部分Cache冲突。文章深入剖析了各种自旋锁的工作原理和优缺点。
摘要由CSDN通过智能技术生成

前言

随着SMP处理器核越来越多

自旋锁是一种简单的轻量级的锁机制,理论上来说自旋锁可以小到一位,该位表示锁的状态,获取锁的线程尝试使用原子指令比较并交换该位,当锁不可用时反复自旋。自旋锁忙等待的机制会浪费处理器资源,但与让线程睡眠的锁相比节省了两次上下文切换的开销,自旋锁特别适用临界区短且不能睡眠的情况。网上关于自旋锁的资料非常多,本文是一篇对自旋锁进行考古的文章。

正文:自旋锁考古

早期的自旋锁

数据结构spinlock_t

早期的内核中自旋锁的设计很简单,下面是Kernel 2.6.0中的自旋锁结构spinlock_t,可以看到仅仅是一个unsigned int类型的lock。

1

2

3

4

5

6typedef struct {

volatile unsigned int lock;

#ifdef CONFIG_DEBUG_SPINLOCK

unsigned magic;

#endif

} spinlock_t;

spin_lock_init

自旋锁的初始化是将lock置1。

1

2

3#define SPIN_LOCK_UNLOCKED (spinlock_t) { 1 SPINLOCK_MAGIC_INIT }

#define spin_lock_init(x)do { *(x) = SPIN_LOCK_UNLOCKED; } while(0)

spin_lock

在CONFIG_SMP并且CONFIG_PREEMPT的时候,spin_lock如下。

单CPU或者没有打开内核抢占的情况比较简单暂不分析

1

2

3

4

5

6

7#if defined(CONFIG_SMP) && defined(CONFIG_PREEMPT)

#define spin_lock(lock) \

do { \

preempt_disable(); \

if (unlikely(!_raw_spin_trylock(lock))) \

__preempt_spin_lock(lock); \

} while (0)

上面的代码首先调用preempt_disable()关抢占,然后调用_raw_spin_trylock(lock)尝试加锁,函数中内嵌汇编的代码把lock->lock的值交换给oldval,把值0交换给lock->lock。如果oldval>0说明lock是未加锁状态,函数返回真,否则加锁失败返回假。

内嵌汇编语法:http://www.ethernut.de/en/documents/arm-inline-asm.html

1

2

3

4

5

6

7

8

9static inline int _raw_spin_trylock(spinlock_t *lock)

{

char oldval;

__asm__ __volatile__(

"xchgb %b0,%1"

:"=q" (oldval), "=m" (lock->lock)

:"0" (0) : "memory");

return oldval > 0;

}

我们回到spin_lock函数来看,如果_raw_spin_trylock成功那么if语句为假,反之trylock失败if为真才会调用__preempt_spin_lock(lock),其中的do-while循环就是自旋锁的自旋操作。

1

2

3

4

5

6

7

8

9

10

11

12

13

14void __preempt_spin_lock(spinlock_t *lock)

{

if (preempt_count() > 1) {

_raw_spin_lock(lock);

return;

}

do {

preempt_enable(); //开抢占,自旋过程允许其他线程抢占

while (spin_is_locked(lock)) //循环判断自旋锁状态

cpu_relax();

//自旋锁被释放处于未加锁状态,退出while循环,关抢占再次_raw_spin_trylock(lock)抢锁

preempt_disable();

} while (!_raw_spin_trylock(lock));

}

spin_unlock

spin_unlock逻辑很简单其实就是将lock值设置为1,然后开启抢占。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20#define spin_unlock(lock) \

do { \

_raw_spin_unlock(lock); \

preempt_enable(); \

} while (0)

static inline void _raw_spin_unlock(spinlock_t *lock)

{

char oldval = 1;

#ifdef CONFIG_DEBUG_SPINLOCK

if (lock->magic != SPINLOCK_MAGIC)

BUG();

if (!spin_is_locked(lock))

BUG();

#endif

__asm__ __volatile__(

spin_unlo

ck_string

);

}

不足

b111293d1f0ca4cb8481df5a57e19cde.png

随着SMP处理器核越来越多,早期的自旋锁争用中不公平现象是很明显的。CPU0上的任务a持锁,其他CPU上的任务c等自旋等待,此时锁l所在各CPU的L1 Cache的Cache line状态是共享Shared (S),当任务a释放锁,将锁l的值写为1,这时候CPU0的Cache状态是已修改Modified (M),而其他CPU Cache状态是无效Invalid (I),如果此时CPU0上的任务b决定快速获取锁l,由于b与刚刚释放锁的a在同一处理器,b可以凭借拥有该高速缓存行而具有优势,对自旋等待更久的锁是不公平的。

b58193ad207625c06e953b9100dcf899.png

Ticket spinlockOn an 8 core (2 socket) Opteron, spinlock unfairness is extremely noticable, with a userspace test having a difference of up to 2x runtime per thread, and some threads are starved or “unfairly” granted the lock up to 1 000 000 (!) times.

根据Nick Piggin在 8 core环境中的测试,某些线程被饥饿旋转1 000 000 次,如果core数更多这种不公平现象恐怕会更严重,从公平的角度来讲,拿锁的顺序也应该讲一个先来后到的原则,Nick在2.6.25 内核中引入了Ticket Spinlock。

[^1]: 详见 https://lwn.net/Articles/267968/

Ticket spinlock的设计思想是排队,把早期自旋锁中的lock拆分成owner和next两部分,owner表示当前持锁的排队号,next表示下一个来拿锁的发的号码,类似于我们去银行窗口办事,我们每个人办事之前都要取号(next),窗口也会显示当前正在服务的号码(owner)。

a74d7d82270773577b4798bbae463507.png

数据结构spinlock_t

下面基于3.10版本的内核分析Ticket spinlock,在不考虑config debug等等的情况下,spinlock_t其实是对raw_spinlock又套了一层(为了兼容rt分支),raw_spinlock中是arch_spinlock_t,arch_spinlock_t与体系结构相关,以x86为例。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16typedef struct spinlock {

struct raw_spinlock rlock;

} spinlock_t;

typedef struct raw_spinlock {

arch_spinlock_t raw_lock;

} raw_spinlock_t;

typedef struct arch_spinlock {

union {

__ticketpair_t head_tail;

struct __raw_tickets {

__ticket_t head, tail;

} tickets;

};

} arch_spinlock_t;

spin_lock_init

初始化将raw_lock置0。

1

2

3

4

5

6

7

8

9

10

11

12

13

14#define spin_lock_init(_lock)\

do {\

spinlock_check(_lock);\

raw_spin_lock_init(&(_lock)->rlock);\

} while (0)

# define raw_spin_lock_init(lock)\

do { *(lock) = __RAW_SPIN_LOCK_UNLOCKED(lock); } while (0)

#define __RAW_SPIN_LOCK_INITIALIZER(lockname)\

{\

.raw_lock = __ARCH_SPIN_LOCK_UNLOCKED,\

SPIN_DEBUG_INIT(lockname)\

SPIN_DEP_MAP_INIT(lockname) }

#define __ARCH_SPIN_LOCK_UNLOCKED{ { 0 } }

spin_lock

可以看到spin_lock会调用__raw_spin_lock。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18static inline void spin_lock(spinlock_t *lock)

{

raw_spin_lock(&lock->rlock);

}

#define raw_spin_lock(lock)_raw_spin_lock(lock)

void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)

{

__raw_spin_lock(lock);

}

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);

}

而在没有define自旋锁debug的时候__raw_spin_lock其实简化成下面的代码,首先关抢占,最终会调用arch_spin_lock。

1

2

3

4

5

6

7

8

9

10

11

12static inline void __raw_spin_lock(raw_spinlock_t *lock)

{

preempt_disable();

do { } while (0) //spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);

do_raw_spin_lock(lock) //LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);

}

static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)

{

__acquire(lock);

arch_spin_lock(&lock->raw_lock);

}

x86的arch_spin_lock如下,首先inc.tail = 2(递增步长为2),然后xadd将tickets的值赋给inc,并将tickets.tail增加2,头尾相等说明unlock状态,可以直接拿锁,否则开始自旋。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25#define __TICKET_LOCK_INC2

static __always_inline void arch_spin_lock(arch_spinlock_t *lock)

{

// inc.tail = 2

register struct __raw_tickets inc = { .tail = TICKET_LOCK_INC };

//tickets的值赋给inc,然后将tickets.tail增加2

inc = xadd(&lock->tickets, inc);

//头尾相等说明unlock状态,可以直接拿锁

if (likely(inc.head == inc.tail))

goto out;

inc.tail &= ~TICKET_SLOWPATH_FLAG;

for (;;) {

unsigned count = SPIN_THRESHOLD;

//自旋,循环判断head、tail是否相等

do {

if (ACCESS_ONCE(lock->tickets.head) == inc.tail)

goto out;

cpu_relax();

} while (--count);

__ticket_lock_spinning(lock, inc.tail);

}

out:barrier();/* make sure nothing creeps before the lock is taken */

}

spin_unlock

与spin_lock类似,spin_unlock最后调用__raw_spin_unlock,do_raw_spin_unlock解锁之后开抢占。arch_spin_unlock函数if中与虚拟化有关,不考虑这种情况arch_spin_unlock只是将tickets.head增加TICKET_LOCK_INC。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29static inline void __raw_spin_unlock(raw_spinlock_t *lock)

{

spin_release(&lock->dep_map, 1, _RET_IP_);

do_raw_spin_unlock(lock);

preempt_enable();

}

static inline void do_raw_spin_unlock(raw_spinlock_t *lock) __releases(lock)

{

arch_spin_unlock(&lock->raw_lock);

__release(lock);

}

static __always_inline void arch_spin_unlock(arch_spinlock_t *lock)

{

if (TICKET_SLOWPATH_FLAG &&

static_key_false(&paravirt_ticketlocks_enabled)) {

arch_spinlock_t prev;

prev = *lock;

add_smp(&lock->tickets.head, TICKET_LOCK_INC);

/* add_smp() is a full mb() */

if (unlikely(lock->tickets.tail & TICKET_SLOWPATH_FLAG))

__ticket_unlock_slowpath(lock, prev);

} else

__add(&lock->tickets.head, TICKET_LOCK_INC, UNLOCK_LOCK_PREFIX);

}

不足

与早期简单的spinlock相比Ticket spinlock更公平,但是还是存在性能问题。Cache问题需要考虑,锁争用会导致大量的Cache颠簸,如果多个CPU之间重复获取自旋锁,则会有多个其他CPU不断查询其值,锁所在Cache line状态为Shared而不是Exclusive,对该Cache line的数据进行修改将其他CPU导致Cache未命中,如果锁不是独占一个Cache line还会影响同一Cache line中的其他数据,因此锁争用会大大降低系统性能。

MCS spinlock

内核开发者Tim Chen在内核中引入了MCS spinlock,通过让每个CPU在自己本地的自旋锁结构体变量上自旋,能够避免绝大部分的cache颠簸。

mcs_spinlock包含一个next指针和一个整形变量用于表示当前锁的状态。

1

2

3

4struct mcs_spinlock {

struct mcs_spinlock *next;

int locked; //0表示unlock

};

MCS锁会初始化一个全局的mcs_spinlock结构体,全局结构体next指针初始为NULL。当CPU 0取锁时,申请一个自己的msc_spinlock结构体,用原子指令将本地结构体地址与全局mcs_spinlock的next指针交换,交换后CPU 0得到的指针为空,说明当前可以持锁。在CPU 0持锁的过程中,CPU 1来取锁,CPU 1申请一个自己的msc_spinlock结构体,使用原子指令将本地msc_spinlock结构体地址与全局mcs_spinlock的next指针交换,此时CPU 1得到的是CPU 0的mcs_spinlock结构体地址,表示锁当前不可用,然后CPU 1会在CPU 0的结构体的next域中写入自己的mcs_spinlock结构体地址,然后在本地的mcs_spinlock上自旋。这样每一个每取锁CPU都需要申请自己的结构体,全局的mcs_spinlock结构体的next指针永远指向锁等待队列的队尾。

解锁的过程是这样的,当一个CPU要解锁时执行一条“有条件交换”的原子指令,如果next中的值依旧是自己的结构体地址,表示当前无其他CPU等锁,那么就将全局锁的next域置空,锁被释放。否则,修改next指向的结构体中locked域的值。,下一个CPU locked的值变化后,便会停止自旋。

如果不理解可以看 https://lwn.net/Articles/590243/

MCS并没有完全替代Ticket spinlock,其中一个原因是MCS的数据结构大于32bit,内核中很多重要的结构体中都使用了spinlock,其中一些(典型的如struct page)的体积是不允许变大的。

Queue spinlock

Queue spinlock是对大体积MSC锁的一种优化,qspinlock只占有32bit,qspinlock比较复杂,在下一篇文章中单独分析。

联系我

您可以直接在下方直接留言并留下您的邮箱,或者E-Mail联系我。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值