第一百八十四章:自旋锁的进化
大乘期涉及内核源码:
一
离开无锁队列的河流,林小源来到一座古老的竞技场。竞技场中央矗立着四座擂台,每座擂台上都站着一位守擂者,风格截然不同。
第一座擂台上,一位粗犷的汉子不断挥拳,拳拳带风。"这是原始自旋锁。"一个声音响起。林小源转头,看到一位白发老者坐在观众席上。
"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() 保存并恢复状态。选错后缀,就像在错的战场布阵。"
"所以锁不是越底层越强。"
"锁是契约。"老者说,"它约束谁能睡、谁能抢占、谁能中断、谁能迁移。看不清契约,死锁、优先级反转和延迟尖刺都会找上门。"
后缀之试
自旋锁后缀中,用于保存并关闭中断、解锁时恢复状态的是哪一类后缀?
/*
* 自旋锁的进化:
*
* 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");#include <stdio.h>
/*
* 自旋锁的进化:
*
* 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++;
}
int main() {
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");
return 0;
}道藏笔记
内核启示
自旋锁从忙等待进化到 qspinlock。
进化历程:
- 原始 — test_and_set 忙等待
- Ticket — 取号排队,FIFO
- MCS — 本地变量自旋
- qspinlock — MCS + ticket
约束:
- 不能睡眠
- 持锁时间要短
- 中断上下文使用
- PREEMPT_RT 下 语义会改变
- 真正底层关中断路径使用
raw_spinlock_t
公平性:
- 原始锁可能饥饿
- Ticket 保证 FIFO
自旋锁是守护者——保护并发,但需谨慎。
自旋锁进化之试
Linux 4.2 引入、结合 MCS 和 Ticket 优点的队列自旋锁实现叫什么?