写点什么

深入 Linux 内核之自旋锁 spinlock_t 机制

用户头像
赖猫
关注
发布于: 2021 年 02 月 04 日

深度详解Linux内核网络结构及分布

epoll的具体实现与epoll线程安全,互斥锁,自旋锁,CAS,原子操作。


spinlock 用在什么场景?


自旋锁用在临界区代码非常少的情况。


spinlock 在使用时有什么注意事项?


临界区代码应该尽可能精简

不允许睡眠(会出现死锁)

Need to have interrupts disabled when locked by ordinary threads, if

shared by an interrupt handler。(会出现死锁)


spinlock 是怎么实现的?


看一下源代码:


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;
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;123456789101112131415161718192021222324252627
复制代码


如果忽略 CONFIG_DEBUG_LOCK_ALLOC 话,spinlock 主要包含一个arch_spinlock_t的结构,从名字可以看出,这个结构是跟体系结构有关的。


Linux、C/C++技术交流群:【960994558】整理了一些个人觉得比较好的学习书籍、大厂面试题、有趣的项目和热门技术教学视频资料共享在里面(包括 C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK 等等.),有需要的可以自行添加哦!~


加锁流程


加锁的相关源码如下:


#define raw_spin_lock(lock) _raw_spin_lock(lock)
static inline void spin_lock(spinlock_t *lock){ raw_spin_lock(&lock->rlock);}123456
复制代码


_raw_spin_lock完成实际的加锁动作。


根据 CPU 体系结构,spinlock分为 SMP 版本和 UP 版本,这里以 SMP 版本为例来分析。SMP 版本中,_raw_spin_lock为声明为:


static inline void __raw_spin_lock(raw_spinlock_t *lock){    // 禁止抢占    preempt_disable();    // for debug    spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);    // real work done here    LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);}123456789
复制代码


LOCK_CONTENDED 是一个通用的加锁流程。do_raw_spin_trylockdo_raw_spin_lock的实现依赖于具体的体系结构,以 x86 为例,do_raw_spin_trylock最终调用的是:


do_raw_spin_trylock的源代码:


static inline int do_raw_spin_trylock(raw_spinlock_t *lock){    // 体系结构相关    return arch_spin_trylock(&(lock)->raw_lock);}12345
复制代码


以 x86 为例,arch_spin_trylock最终调用__ticket_spin_trylock函数。其源代码如下:


// 定义在arch/x86/include/asm/spinlock_types.htypedef struct arch_spinlock {    union {        __ticketpair_t head_tail;        struct __raw_tickets {            __ticket_t head, tail; // 注意,x86使用的是小端模式,存在高地址空间的是tail        } tickets;    };} arch_spinlock_t;
// 定义在arch/x86/include/asm中static __always_inline int __ticket_spin_trylock(arch_spinlock_t *lock){ arch_spinlock_t old, new; // 获取旧的ticket信息 old.tickets = ACCESS_ONCE(lock->tickets); // head和tail不一致,说明锁正被占用,加锁不成功 if (old.tickets.head != old.tickets.tail) return 0;
new.head_tail = old.head_tail + (1 << TICKET_SHIFT); // 将tail + 1
/* cmpxchg is a full barrier, so nothing can move before it */ return cmpxchg(&lock->head_tail, old.head_tail, new.head_tail) == old.head_tail;}12345678910111213141516171819202122232425
复制代码


从上述代码中可知,__ticket_spin_trylock的核心功能,就是判断自旋锁是否被占用,如果没被占用,尝试原子性地更新lock中的head_tail的值,将 tail+1,返回是否加锁成功。


不考虑 CONFIG_DEBUG_SPINLOCK 宏的话, 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);}12345
复制代码


arch_spin_lock的源代码:


static __always_inline void arch_spin_lock(arch_spinlock_t *lock){    __ticket_spin_lock(lock);}1234
复制代码


__ticket_spin_lock的源代码:


static __always_inline void __ticket_spin_lock(arch_spinlock_t *lock){    register struct __raw_tickets inc = { .tail = 1 };        // 原子性地把ticket中的tail+1,返回的inc是+1之前的原始值    inc = xadd(&lock->tickets, inc);
for (;;) { // 循环直到head和tail相等 if (inc.head == inc.tail) break; cpu_relax(); // 读取新的head值 inc.head = ACCESS_ONCE(lock->tickets.head); } barrier(); /* make sure nothing creeps before the lock is taken */}1234567891011121314151617
复制代码


ticket 分成两个部分,一部分叫 tail,相当于一个队列的队尾,一个部分叫 head,相当于一个队列的队头。初始化的时候,tailhead都是 0,表示无人占用锁。

__ticket_spin_lock 就是原子性地把 tail+1,并且把+1 之前的值记录下来,然后不断地和 head 进行比较。由于是原子性的操作,所以不同的锁竞争者拿到的 tail 值是不一样的。如果 tail 值和 head 一样了,说明这时候没人占用锁了,下一个拿到锁的就是自己了。


举例来说,假设线程 A 和线程 B 竞争同一个自旋锁:


初始化 tail=0, head=0,线程 A 将 tail+1,

并返回 tail 的旧值 0,将 0 和 head 值比较,相等,于是这时候线程 A 就拿到了锁。

线程 A 这时候也来拿锁,将 tail 值+1,变成 2,返回 tail 的旧值 1,将其和 head 值 0 比较,不相等,继续循环。

线程 A 用完锁了,将 head 值+1。

线程 B 读取 head 值,并将其和 tail 值比较,发现相等,获得锁。


解锁流程


对于 SMP 架构来说,spin_unlock最终调用的是__raw_spin_unlock,其源代码如下:


static 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);}1234567891011121314
复制代码


arch_spin_unlock在 x86 体系结构下的实现代码如下:


static __always_inline void arch_spin_unlock(arch_spinlock_t *lock){    __ticket_spin_unlock(lock);}
static __always_inline void __ticket_spin_unlock(arch_spinlock_t *lock){ // 将tickers的head值加1 __add(&lock->tickets.head, 1, UNLOCK_LOCK_PREFIX);}12345678910
复制代码


考虑中断处理函数


如果自旋锁可能在中断处理处理中使用,那么在获取自旋锁之前,必须禁止本地中断。则,持有锁的内核代码会被中断处理程序打断,接着试图去争用这个已经被持有的自旋锁。这样的结果是,中断处理函数自旋,等待该锁重新可用,但是锁的持有者在该中断处理程序执行完毕之前不可能运行,这就成为了双重请求死锁。注意,需要关闭的只是当前处理器上的中断。因为中断发生在不同的处理器上,即使中断处理程序在同一锁上自旋,也不会妨碍锁的持有者(在不同处理器上)最终释放。


所以要使用spin_lock_irqsave() / spin_unlock_irqrestore()这个版本的加锁、解锁函数。

函数spin_lock_irqsave():保存中断的当前状态,禁止本地中断,然后获取指定的锁。

函数spin_unlock_reqrestore():对指定的锁解锁,让中断恢复到加锁前的状态。所以即使中断最初是被禁止的,代码也不会错误地激活它们。


spinlock 的几种变种


rwlock_t 读写锁

seqlock_t 顺序锁


以上有不足的地方欢迎指出讨论,觉得不错的朋友希望能得到您的转发支持,同时可以持续关注我


用户头像

赖猫

关注

还未添加个人签名 2020.11.28 加入

纸上得来终觉浅,绝知此事要躬行

评论

发布
暂无评论
深入Linux内核之自旋锁spinlock_t机制