Linux内核自旋锁spinlock_t机制

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;

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

加锁流程

加锁的相关源码如下:


#define raw_spin_lock(lock) _raw_spin_lock(lock)

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

_raw_spin_lock完成实际的加锁动作。

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

void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)        __acquires(lock);

再看_raw_spin_lock的实现,SMP版本中,看_raw_spin_lock最终调用了__raw_spin_lock,__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);
}

LOCK_CONTENDED是一个通用的加锁流程。do_raw_spin_trylock和do_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);
}

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

// 定义在arch/x86/include/asm/spinlock_types.h
typedef 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;
}

从上述代码中可知,__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);
}

arch_spin_lock的源代码:

static __always_inline void arch_spin_lock(arch_spinlock_t *lock)
{
    __ticket_spin_lock(lock);
}

__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 */
}

ticket分成两个部分,一部分叫tail,相当于一个队列的队尾,一个部分叫head,相当于一个队列的队头。初始化的时候,tail和head都是0,表示无人占用锁。
__ticket_spin_lock就是原子性地把tail+1,并且把+1之前的值记录下来,然后不断地和head进行比较。由于是原子性的操作,所以不同的锁竞争者拿到的tail值是不一样的。如果tail值和head一样了,说明这时候没人占用锁了,下一个拿到锁的就是自己了。

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

  1. 初始化tail=0, head=0,线程A将tail+1, 并返回tail的旧值0,将0和head值比较,相等,于是这时候线程A就拿到了锁。
  2. 线程A这时候也来拿锁,将tail值+1,变成2,返回tail的旧值1,将其和head值0比较,不相等,继续循环。
  3. 线程A用完锁了,将head值+1。
  4. 线程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);
}

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

考虑中断处理函数

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

所以要使用spin_lock_irqsave() / spin_unlock_irqrestore()这个版本的加锁、解锁函数。
函数spin_lock_irqsave():保存中断的当前状态,禁止本地中断,然后获取指定的锁。
函数spin_unlock_reqrestore():对指定的锁解锁,让中断恢复到加锁前的状态。所以即使中断最初是被禁止的,代码也不会错误地激活它们。

spinlock的几种变种

  1. rwlock_t 读写锁
  2. seqlock_t 顺序锁

参考资料

Ticket spinklocks
Linux内核源代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容