Skip to content

第一百八十四章:自旋锁的进化

大乘期

涉及内核源码:

离开无锁队列的河流,林小源来到一座古老的竞技场。竞技场中央矗立着四座擂台,每座擂台上都站着一位守擂者,风格截然不同。

第一座擂台上,一位粗犷的汉子不断挥拳,拳拳带风。"这是原始自旋锁。"一个声音响起。林小源转头,看到一位白发老者坐在观众席上。

"test_and_set——纯粹的忙等待。"老者摇头,"获取不到锁就不停地转,浪费CPU。而且不公平——有的线程可能永远抢不到锁,饿死。"

林小源看到第二座擂台上,一位儒雅的书生手持号牌,排队的人依次上前。"这是Ticket自旋锁。"老者继续说道,"像银行取号一样,每个等待者拿一个号,锁释放时轮到下一个号。公平,FIFO,不会饥饿。"

"自旋锁先要问上下文。"老者用拐杖点地,竞技场外浮现三条路:sleeping locks、CPU local locks、spinning locks。"mutex、semaphore、rw_semaphore 会睡眠,只能在可抢占的任务上下文里谨慎使用;raw spinlock、bit spinlock 是真正忙等; 在普通内核上像 raw spinlock,但在 PREEMPT_RT 上语义会变。"

林小源皱眉:"同一个名字,不同内核配置下不同?"

"对。修行不能只背 API,要看配置和上下文。"

破关试炼

锁类初试

内核锁类型文档把锁大体分成 sleeping、CPU local 和哪一类锁?

答对后才能继续滑动和进入下一章。

第三座擂台上,每位修士都坐在自己的蒲团上,闭目等待。"这是MCS锁。"老者走到林小源身边,"每个CPU有自己的节点,在本地变量上自旋,而不是在全局变量上。这样减少了缓存行的颠簸——多个CPU不会同时争抢同一块缓存行。"

第四座擂台上,一位身穿金色铠甲的将军负手而立,气度非凡。"这是qspinlock——队列自旋锁。"老者眼中闪过一丝敬意,"Linux 4.2引入,结合了MCS锁和Ticket锁的优点。在本地变量上自旋,同时保证公平性。是目前最高效的自旋锁实现。"

林小源看着四座擂台,感受到自旋锁进化的脉络——从粗暴的忙等待,到公平的取号,到本地化的自旋,再到两者的融合。每一步进化都是为了解决上一代的问题。

"但 raw_spinlock_t 不是同一柄剑。"老者指向金甲将军旁边一位沉默的黑甲武士,"raw spinlock 在所有内核里都严格自旋,常用于真正底层、硬中断或必须关抢占/关中断的极短路径。 在非 RT 内核上会关抢占;到了 PREEMPT_RT,它会映射到基于 rt_mutex 的实现,抢占不再被关闭,只保证持锁任务不迁移。"

"那 _irqsave 呢?"

"在普通内核里,它保存并关中断;在 PREEMPT_RT 的 上,硬中断相关后缀不再改变 CPU 的中断关闭状态。要访问硬件状态、真正依赖关中断时,别拿错锁。"

破关试炼

真假自旋

在 PREEMPT_RT 内核中仍保持严格自旋语义的锁类型是什么?

答对后才能继续滑动和进入下一章。

"前辈,自旋锁有什么约束?"林小源问道。

老者竖起三根手指。"第一,不能睡眠。持锁时禁止抢占,你若睡着了,别的CPU自旋等你,死锁。第二,持锁时间要短——自旋就是在烧CPU,持锁越久浪费越大。第三,适合中断上下文——进程上下文有更好的选择,比如mutex。"

"所以自旋锁适合短时间、不能睡眠的场景?"

"正是。"老者点头,"中断处理程序、临界区很短的代码路径——这些地方用自旋锁最合适。如果临界区可能很长,就该用mutex,让等待者睡眠,而不是烧CPU。"

"另外,持锁者必须亲自解锁。"老者补充,"除 semaphore 这类特殊机制外,多数锁都有严格 owner 语义。锁的后缀也不是装饰:_bh() 管 bottom halves,_irq() 管中断,_irqsave/restore() 保存并恢复状态。选错后缀,就像在错的战场布阵。"

"所以锁不是越底层越强。"

"锁是契约。"老者说,"它约束谁能睡、谁能抢占、谁能中断、谁能迁移。看不清契约,死锁、优先级反转和延迟尖刺都会找上门。"

破关试炼

后缀之试

自旋锁后缀中,用于保存并关闭中断、解锁时恢复状态的是哪一类后缀?

答对后才能继续滑动和进入下一章。
c
/*
 * 自旋锁的进化:
 *
 * 1. 原始自旋锁
 *    while (test_and_set(lock))
 *        ; // 忙等待
 *    问题: 不公平,可能饥饿
 *
 * 2. Ticket 自旋锁
 *    类似银行取号
 *    next — 下一个号
 *    owner — 当前服务号
 *    获取: my = next++
 *    等待: while (owner != my)
 *    优点: 公平,FIFO
 *
 * 3. MCS 锁
 *    每个 CPU 有自己的节点
 *    在本地变量上自旋
 *    减少缓存颠簸
 *
 * 4. qspinlock (队列自旋锁)
 *    结合 MCS 和 ticket
 *    Linux 4.2 引入
 *    更高效
 *
 * 自旋锁的约束:
 *   不能睡眠
 *   持锁时间要短
 *   中断上下文使用
 */

/* 原始自旋锁 */
typedef struct {
    volatile int locked;
} spinlock_basic_t;

void spin_lock_basic(spinlock_basic_t *lock) {
    while (__sync_lock_test_and_set(&lock->locked, 1))
        ; /* 忙等待 */
}

void spin_unlock_basic(spinlock_basic_t *lock) {
    __sync_lock_release(&lock->locked);
}

/* Ticket 自旋锁 */
typedef struct {
    volatile unsigned short next;
    volatile unsigned short owner;
} spinlock_ticket_t;

void spin_lock_ticket(spinlock_ticket_t *lock) {
    unsigned short my = __sync_fetch_and_add(&lock->next, 1);
    while (lock->owner != my)
        ; /* 等待我的号 */
}

void spin_unlock_ticket(spinlock_ticket_t *lock) {
    lock->owner++;
}

printf("=== 自旋锁的进化 — 从忙等待到 ticket ===\n\n");

/* 原始自旋锁 */
printf("--- 原始自旋锁 ---\n");
spinlock_basic_t basic_lock = { .locked = 0 };
printf("获取锁...\n");
spin_lock_basic(&basic_lock);
printf("已获取锁\n");
spin_unlock_basic(&basic_lock);
printf("已释放锁\n\n");

/* Ticket 自旋锁 */
printf("--- Ticket 自旋锁 ---\n");
spinlock_ticket_t ticket_lock = { .next = 0, .owner = 0 };
printf("获取锁 (号: 0)...\n");
spin_lock_ticket(&ticket_lock);
printf("已获取锁\n");
spin_unlock_ticket(&ticket_lock);
printf("已释放锁\n\n");

printf("--- 进化历程 ---\n");
printf("1. 原始自旋锁:\n");
printf("   test_and_set 忙等待\n");
printf("   不公平,可能饥饿\n\n");
printf("2. Ticket 自旋锁:\n");
printf("   取号排队\n");
printf("   公平,FIFO\n\n");
printf("3. MCS 锁:\n");
printf("   本地变量自旋\n");
printf("   减少缓存颠簸\n\n");
printf("4. qspinlock:\n");
printf("   MCS + ticket\n");
printf("   最高效\n\n");

printf("--- 自旋锁约束 ---\n");
printf("1. 不能睡眠\n");
printf("   持锁时禁止抢占\n");
printf("   睡眠会导致死锁\n\n");
printf("2. 持锁时间要短\n");
printf("   长时间持锁浪费 CPU\n\n");
printf("3. 中断上下文使用\n");
printf("   进程上下文可用 mutex\n");

道藏笔记

内核启示

自旋锁从忙等待进化到 qspinlock。

进化历程:

  • 原始 — test_and_set 忙等待
  • Ticket — 取号排队,FIFO
  • MCS — 本地变量自旋
  • qspinlock — MCS + ticket

约束:

  • 不能睡眠
  • 持锁时间要短
  • 中断上下文使用
  • PREEMPT_RT 下 语义会改变
  • 真正底层关中断路径使用 raw_spinlock_t

公平性:

  • 原始锁可能饥饿
  • Ticket 保证 FIFO

自旋锁是守护者——保护并发,但需谨慎。


破关试炼

自旋锁进化之试

Linux 4.2 引入、结合 MCS 和 Ticket 优点的队列自旋锁实现叫什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道