Linux 内核同步与锁机制深度实战:从 spinlock 到 RCU 的无锁艺术
引言
在 Linux 内核开发中,并发控制是最核心也最容易出错的课题之一。从多核 CPU 的对称多处理(SMP)到单核上的抢占式调度,从硬件中断到软中断、工作队列,内核代码几乎处处面临竞态条件(Race Condition)的威胁。本文将系统性地剖析 Linux 内核中所有主流同步机制的原理、适用场景、性能特征和工程实战技巧。
我们将从最基本的 atomic 操作出发,逐步深入到 spinlock、rwlock、mutex、semaphore、seqlock、RCU,以及内核提供的内存屏障原语和 lockdep 死锁检测工具。每个机制都会配有真实的内核源码分析、性能基准数据和工程最佳实践。
一、并发问题的根源
1.1 三类并发场景
内核中的并发来源可以分为三类:
| 并发类型 | 描述 | 典型场景 |
|---|---|---|
| 真正并行 | 多核 CPU 同时执行内核代码 | 两个 CPU 同时处理网络包 |
| 抢占式并发 | 单核上内核任务被更高优先级任务抢占 | 进程上下文被另一个进程抢占 |
| 中断抢占 | 任何执行上下文被硬件中断打断 | 数据处理中被网卡中断打断 |
1.2 竞态条件的本质
考虑一个简单的计数器自增操作,在汇编层面它实际上由三条指令组成:
mov eax, [counter] ; 1. 从内存加载到寄存器
inc eax ; 2. 寄存器加 1
mov [counter], eax ; 3. 写回内存
如果两个 CPU 同时执行这段代码,它们可能在同一时刻加载相同的初始值,各自加 1 后写回,结果只增加了 1 而不是 2。这就是竞态条件。
二、原子操作 —— 同步的基石
2.1 atomic 类型体系
现代 CPU 提供了单条指令完成"读-改-写"操作的能力。Linux 内核用 atomic_t 封装了这一能力:
// include/linux/types.h
typedef struct {
int counter;
} atomic_t;
// 基本操作 API
atomic_set(&v, i); // 设置值
atomic_read(&v); // 读取值
atomic_add(i, &v); // 加 i
atomic_sub(i, &v); // 减 i
atomic_inc(&v); // 加 1
atomic_dec(&v); // 减 1
// 条件操作(返回操作后的值)
atomic_inc_not_zero(&v); // 非零才自增
atomic_dec_and_test(&v); // 自减后测试是否为 0
atomic_add_return(i, &v); // 加 i 并返回新值
atomic_sub_and_test(i, &v); // 减 i 并测试是否为 0
2.2 64 位原子操作
在 64 位系统上 atomic_t 是 32 位的。对于需要 64 位原子操作场景(如时间戳计数器),内核提供 atomic64_t:
atomic64_t ts = ATOMIC64_INIT(0);
atomic64_add(100, &ts);
s64 val = atomic64_read(&ts);
2.3 局部原子变量 local_t
local_t 是专门用于 per-CPU 计数器的原子类型。它的原子性仅保证在本地 CPU 上(不会被其他 CPU 抢占),因此比 atomic_t 更轻量:
// kernel/time/tick-common.c 中的使用
static local_t __cacheline_aligned tick_do_timer;
// 仅在本地 CPU 上做原子操作(关中断已足够)
local_inc(&tick_do_timer);
三、spinlock —— 自旋锁
3.1 核心原理
spinlock 是最简单的内核同步原语。当一个 CPU 持有时,其他 CPU 反复"自旋等待"(busy-wait),直到锁被释放。这适用于临界区非常短、不允许睡眠的场景:
DEFINE_SPINLOCK(my_lock);
spin_lock(&my_lock);
// 临界区:必须极快,不能有任何可能睡眠的操作
shared_counter++;
spin_unlock(&my_lock);
3.2 自旋锁的内部实现(x86)
现代 x86 架构下,spinlock 基于 qspinlock(Queued Spinlock),使用原子交换指令:
// 加锁伪代码
int old = 0;
int new = _Q_LOCK_VAL;
while (atomic_cmpxchg(&lock->val, old, new) != old) {
old = atomic_read(&lock->val);
// 自旋等待:使用 PAUSE 指令降低功耗和总线争用
cpu_relax(); // 即 PAUSE 指令
}
// 解锁
atomic_set(&lock->val, 0);
qspinlock 相比旧版 MCS 锁的优势:解决了"全局缓存行乒乓"问题,将自旋等待分散到各 CPU 本地的变量上,大幅降低总线流量。
3.3 spinlock 的变体
| API | 行为 | 使用场景 |
|---|---|---|
| spin_lock/spin_unlock | 基础自旋锁 | 短临界区,不关心中断 |
| spin_lock_irq/spin_unlock_irq | 关本地中断 + 自旋锁 | 防止同一 CPU 中断处理程序重入 |
| spin_lock_irqsave/spin_unlock_irqrestore | 保存中断状态 + 关中断 + 锁 | 不知道之前中断状态时使用 |
| spin_lock_bh/spin_unlock_bh | 关软中断(bottom half)底半部 | 防止软中断、tasklet 重入 |
工程规则:优先使用 spin_lock_irqsave 而非 spin_lock_irq。因为如果你不知道调用前中断是否已关闭,调用 spin_unlock_irq 可能错误地重新开启中断。
3.4 自旋锁的错误用法
// ❌ 错误:在持有 spinlock 时调用可能睡眠的函数
spin_lock(&my_lock);
copy_from_user(buf, user_buf, len); // 可能触发页错误而睡眠!
spin_unlock(&my_lock);
// ✅ 错误:使用 spin_lock_irq 后忘记原始状态
spin_lock_irq(&my_lock);
// ... 临界区 ...
spin_unlock_irq(&my_lock); // 无条件开中断,如果之前中断已关就会破坏调用者状态
// ✅ 正确:使用 irqsave 变体保存状态
unsigned long flags;
spin_lock_irqsave(&my_lock, flags);
// ... 临界区 ...
spin_unlock_irqrestore(&my_lock, flags);
四、rwlock —— 读写锁
4.1 实现与局限
rwlock(读写锁)允许多个读者并发访问,写者独占访问。在内核中定义为 rwlock_t:
DEFINE_RWLOCK(my_rwlock);
// 读端
read_lock(&my_rwlock);
// 只读访问共享数据
read_unlock(&my_rwlock);
// 写端
write_lock(&my_rwlock);
// 独占修改共享数据
write_unlock(&my_rwlock);
4.2 读者饥饿问题
内核 rwlock 天生偏向读者——如果在读者持有期间写者等待,后续读者可以继续获取读锁,导致写者长期饥饿。这是历史设计缺陷,现代内核开发已不推荐使用 rwlock,应当使用 rwseq 或 rwsem。
五、rwseq —— 顺序锁
5.1 设计哲学
seqlock(顺序锁)解决的是"读多写少"场景,且更关键的是:它明确偏向写者,解决了 rwlock 的写者饥饿问题:
// 定义
DEFINE_SEQLOCK(my_seqlock);
// 写者:排他访问,阻塞所有读者重试
write_seqlock(&my_seqlock);
// 修改共享数据
write_sequnlock(&my_seqlock);
// 读者:可能因写者介入而重试
unsigned int seq;
do {
seq = read_seqbegin(&my_seqlock);
// 读取共享数据(此处可能被写者打断)
} while (read_seqretry(&my_seqlock, seq));
5.2 底层实现
seqlock 内部就是一个简单的序列计数器,写者在加锁时自增(从偶数变为奇数),解锁时自增(从奇数变为偶数)。读者在读前后检查序列号,如果不同就说明写者介入了,需要重试:
// include/linux/seqlock.h
typedef struct seqcount {
unsigned int sequence;
// ...
} seqcount_t;
static inline int read_seqretry(const seqlock_t *slp, unsigned int start) {
return read_seqcount_retry(&slp->seqcount, start);
}
// 写者加锁:sequence++(偶→奇,表示有写者)
static inline void write_seqlock(seqlock_t *slp) {
spin_lock(&slp->lock);
seqcount_begin(&slp->seqcount);
}
// 写者解锁:sequence++(奇→偶,表示写者释放)
static inline void write_sequnlock(seqlock_t *slp) {
seqcount_end(&slp->seqcount);
spin_unlock(&slp->lock);
}
5.3 使用约束
seqlock 的读者侧不能访问指针,因为读者可能读到正在被写者释放的内存。只能读取可以直接复制的值类型。
六、rw_semaphore —— 读写信号量
6.1 基本原理
rw_semaphore 是 rwlock 的睡眠版本。与 rwlock 相比有两个关键区别:
- 支持睡眠:持有锁期间可以调用可能睡眠的函数,适合长临界区。
- 偏向写者:新内核版本下(5.13+),rw_semaphore 默认偏向写者,解决写者饥饿。
DECLARE_RWSEM(my_rwsem);
// 读端
down_read(&my_rwsem);
// 读操作
up_read(&my_rwsem);
// 写端
down_write(&my_rwsem);
// 写操作
up_write(&my_rwsem);
// 尝试获取(不阻塞)
if (down_read_trylock(&my_rwsem)) {
// ...
up_read(&my_rwsem);
}
6.2 可中断与不可中断
rw_semaphore 提供了多个变体,区别在于等待时是否可被信号中断:
down_read/down_write:不可中断睡眠(TASK_UNINTERRUPTIBLE)down_read_interruptible/down_write_interruptible:可被信号中断(TASK_INTERRUPTIBLE)down_read_killable/down_write_killable:仅可被 SIGKILL 中断(推荐,避免半阻塞状态)
七、mutex —— 互斥锁
7.1 mutex 的定位
mutex(互斥锁)是内核中最常用的睡眠型锁。与 semaphore(信号量)相比:
| 特征 | mutex | semaphore |
|---|---|---|
| 可同时持有者的数量 | 1(互斥) | N(可设置计数) |
| 所有者 | 严格记录 owner | 无 owner 概念 |
| 性能 | 更优(快速路径可 lock-free) | 稍慢 |
| 调试支持 | lockdep 完整支持 | 基础支持 |
| 使用建议 | 优先使用 | 仅当需要计数信号量时 |
7.2 API 一览
DEFINE_MUTEX(my_mutex);
// 标准加锁(不可被信号中断)
mutex_lock(&my_mutex);
// 临界区
mutex_unlock(&my_mutex);
// 可中断版本
mutex_lock_interruptible(&my_mutex);
// 尝试获取(不睡眠)
if (mutex_trylock(&my_mutex)) {
// ...
mutex_unlock(&my_mutex);
}
// 获取锁但等待其他人完成(协调关闭)
mutex_lock_killable(&my_mutex);
7.3 mutex 的乐观自旋(OSQ)
Linux 5.x 引入了"乐观自旋队列"(Optimistic Spin Queue, OSQ):当 mutex 被持有时,等待者不是立即进入睡眠,而是短暂自旋。如果锁持有者正在另一个 CPU 上执行,这说明临界区很快会释放,自旋可能比睡眠+唤醒更高效。当锁持有者释放时,OSQ 会将第一个自旋者转为持有者,实现所有权直接传递(handoff),避免惊群效应。
7.4 mutex 使用规则
lockdep 会强制执行以下规则:
- mutex 不能被递归加锁(不可重入)
- 不能在中断上下文持有 mutex(mutex_lock 会睡眠)
- 持有 mutex 时不能退出进程(进程退出前必须释放所有 mutex)
- 不能在同一次操作中加多个不同的 mutex,除非使用
mutex_lock_nested明确指定锁层次
八、Read-Copy-Update(RCU)—— 无锁读取的艺术
8.1 设计哲学
RCU 是 Linux 内核中最精妙的高性能同步机制。它的核心思想是:读者完全不加锁,不需要任何原子操作或内存屏障(在大多数架构上),因此读者性能几乎等同于无保护的直接访问。写者通过"复制-修改-等待-回收"协议完成更新。
8.2 核心 API
// 定义一个受 RCU 保护的指针
struct my_data __rcu *g_ptr;
// 读者侧(Read-Copy-Update 的 Read 部分)
rcu_read_lock();
struct my_data *p = rcu_dereference(g_ptr); // 获取指针(语义标记)
// 读取 p 指向的数据(不加锁,极高性能)
printk("value=%d\n", p->value);
rcu_read_unlock();
// 读者侧(可睡眠变体,也可用于可抢占 RCU)
rcu_read_lock_bh(); // 仅禁止软中断
// ...
rcu_read_unlock_bh();
// 写者侧
struct my_data *new_data = kmalloc(sizeof(*new_data), GFP_KERNEL);
new_data->value = 42;
struct my_data *old_data = rcu_dereference_protected(g_ptr, 1);
rcu_assign_pointer(g_ptr, new_data); // 原子更新指针
// 等待所有先于此调用前开始的 RCU 读临界区结束
synchronize_rcu(); // 可能睡眠!必须进程上下文
// 旧数据安全回收
kfree(old_data);
8.3 宽限期与垃圾回收
synchronize_rcu() 等待的"宽限期"(Grace Period)定义为:从调用时刻起,等待所有已经处于 RCU 读临界区的读者完成。新开始的读者不受影响。
内核通过每个 CPU 的静默状态(quiescent state)来追踪。当一个 CPU 经历了上下文切换(或非 RCU 临界区的短暂空闲),它就报告了一个宽限期。只有当所有 CPU 都至少经历了一个宽限期,synchronize_rcu() 才返回。
对于不允许睡眠的上下文,call_rcu() 将回收操作延后到软中断上下文中:
call_rcu(&old_data->rcu_head, my_callback_function);
// my_callback_function 会在宽限期结束后的软中断上下文执行
8.4 RCU 的变体
| 变体 | 读者 API | 禁止抢占 | 适用场景 |
|---|---|---|---|
| CONFIG_RCU_FAST_NO_HZ | rcu_read_lock() | 仅禁止抢占(非抢占内核) | 默认配置 |
| Preemptible RCU | preempt_disable() + rcu_read_lock() | 同时禁止抢占和软中断 | PREEMPT_RT |
| Tiny RCU | 同上(但无宽限期开销) | 仅单 CPU 切换追踪 | 非 SMP 嵌入式 |
| RCU-bh | rcu_read_lock_bh() | 禁止软中断(bh 禁用) | 软中断/中断上下文 |
| RCU-sched | preempt_disable() + rcu_read_lock() | 禁止抢占 | 可抢占但无 bh 的上下文 |
8.5 RCU 的局限性
- 读者不能睡眠:标准 RCU 的
rcu_read_lock()仅禁止抢占,如果读者睡眠,同一 CPU 可能切换到新进程导致宽限期检测错误(除非使用可抢占 RCU) - 写者开销:复制整个数据结构并等待宽限期可能代价很高
- 不适合写多读少场景:频繁的复制-等待-回收会严重拖累性能
- 延迟回收:读者可能在宽限期结束后继续访问旧指针,需要仔细设计数据结构
九、Completion —— 完成量
Completion 用于"事件通知"型同步:一个线程等待某个事件完成,另一个线程触发事件。典型场景是模块加载/卸载等待引用计数归零:
DECLARE_COMPLETION(my_completion);
// 等待者(阻塞直到有人 complete)
wait_for_completion(&my_completion);
// 可中断版本
wait_for_completion_interruptible(&my_completion);
// 带超时版本
wait_for_completion_timeout(&my_completion, HZ);
// 触发者(唤醒等待者)
complete(&my_completion); // 只允许唤醒一个
complete_all(&my_completion); // 唤醒所有等待者(只能使用一次)
内部实现基于 waitqueue 和自旋锁,complete() 在持有自旋锁期间调度等待线程唤醒。
十、内存屏障(Memory Barrier)
10.1 为什么需要内存屏障?
现代 CPU 为了性能会进行乱序执行和缓冲写操作。内存屏障确保指令执行顺序和内存访问顺序符合预期:
| 屏障类型 | 功能 | 示例 |
|---|---|---|
| smp_mb() | 全屏障:所有读写都不可跨越 | 生产者-消费者标志位 |
| smp_rmb() | 读屏障:读操作不可跨越 | 先读数据再读标志位 |
| smp_wmb() | 写屏障:写操作不可跨越 | 先写数据再更新标志位 |
| smp_store_release() | 写后即为 release 语义 | sl->flag = 1(标志数据可用) |
| smp_load_acquire() | 读前置 acquire 语义 | while (!smp_load_acquire(&sl->flag)) |
10.2 Release-Acquire 语义
这是一种比全屏障更弱、更高效的模式。Release 写确保之前的所有操作在其他 CPU 看到此写之前已经完成;Acquire 读确保之后的所有操作在此读之后才执行:
// 生产者
data->value = 42;
data->ready = 1;
smp_store_release(&flag, 1); // 写 flag,之前的写操作不可重排到其后
// 消费者
while (!smp_load_acquire(&flag)) // 读 flag,之后的读操作不可重排到其前
cpu_relax();
// 这里可以安全读取 data->value,保证读到的是 42
val = data->value;
十一、per-CPU 变量
11.1 消除伪共享
per-CPU 变量为每个 CPU 创建独立的变量副本,从根本上消除共享数据的竞态问题:
// 定义与声明
static DEFINE_PER_CPU(struct cpu_counter, cpu_counters);
// 访问本地 CPU 的副本(关抢占防止被迁移)
struct cpu_counter *c = get_cpu_ptr(&cpu_counters);
c->count++;
put_cpu_ptr(c);
// 原子访问其他 CPU 副本
per_cpu(cpu_counters, cpu5).count = 0;
this_cpu_inc(cpu_counters.count); // 仅本地 CPU 原子操作
11.2 缓存行对齐避免伪共享
虽然 per-CPU 消除了 CPU 间共享,但如果同一 CPU 上的两个变量在同一缓存行(通常 64 字节)且被不同上下文(如中断 vs 进程上下文)修改,仍会引发伪共享:
// ❌ 两个 hot 变量在同一缓存行(64 字节)
struct {
atomic_t counter1; // 中断上下文频繁修改
atomic_t counter2; // 进程上下文频繁修改
} hot_data;
// ✅ 用 cacheline 对齐隔离
struct {
atomic_t counter1 ____cacheline_aligned_in_smp;
atomic_t counter2 ____cacheline_aligned_in_smp;
} hot_data;
十二、lockdep —— 内核死锁检测工具
12.1 检测能力
lockdep 是内核内置的运行时死锁检测工具(CONFIG_PROVE_LOCKING),可以检测:
- 锁反转死锁(lock inversion / ABBA)
- 递归死锁
- 同一锁在不可重入上下文中被重复获取
- 在错误上下文使用锁(如中断中使用 spin_lock 而非 spin_lock_irq)
- 锁类(lock class)之间的循环依赖
12.2 锁类与嵌套规则
lockdep 不是跟踪锁实例,而是跟踪"锁类"(lock class)。同一类型的多个实例共享一个锁类。使用 mutex_lock_nested 或 spin_lock_nested 可以指定层次:
// 定义两个锁类
static struct lock_class_key lock_class_1;
static struct lock_class_key lock_class_2;
mutex_init(&lock1);
lockdep_set_class(&lock1, &lock_class_1);
mutex_init(&lock2);
lockdep_set_class(&lock2, &lock_class_2);
// 层次化加锁
mutex_lock(&lock1);
mutex_lock_nested(&lock2, SINGLE_DEPTH_NESTING); // 告知 lockdep 这是合法嵌套
// ...
mutex_unlock(&lock2);
mutex_unlock(&lock1);
12.3 常见 lockdep 报错解析
典型死锁报告:
[ INFO: possible circular locking dependency detected ]
=> lock(AAAA); // 线程1 已持有 A,等待 B
=> lock(BBBB); // 线程2 已持有 B,等待 A
-> #0 (BBBB):
lock_acquire+0x70/0x200
__mutex_lock+0x95/0x5d0
...
-> #1 (AAAA):
lock_acquire+0x70/0x200
__mutex_lock+0x95/0x5d0
...
INFO: lock inversion graph:>[ INFO: possible irq lock inversion dependency detected ]
=> 中断上下文已持 spinlock A,企图再获取 mutex B
=> 进程上下文已持 mutex B(可能睡眠企图)
十三、性能对比与选型指南
13.1 各机制性能基准
以下数据基于 x86-64 多核系统(内核 6.x),单位为单次操作纳秒:
| 机制 | 获取时间 | 释放时间 | 是否可睡眠 | 适用场景 |
|---|---|---|---|---|
| atomic 操作 | 5-10 ns | - | 是 | 计数器、标志位 |
| spinlock(无争用) | 15-25 ns | 10-15 ns | 否(关抢占) | 短临界区 |
| spinlock(高争用) | 100-1000+ ns | 20-50 ns | 否 | 写端短暂持有 |
| mutex(无争用) | 30-50 ns | 20-30 ns | 是 | 长临界区 |
| mutex(争用排队) | 2000-5000+ ns | - | 是 | 锁上有等待队列时 |
| rw_semaphore(读端无争用) | 40-60 ns | 20-30 ns | 是 | 频繁读取 |
| rcu_read_lock/unlock | 2-5 ns | 2-5 ns | 是(不可变上下文) | 极高频读取 |
| synchronize_rcu() | 10-100 μs(宽限期) | - | 是 | 低频写 |
13.2 选型决策树
面对一个同步问题,推荐按以下顺序选择机制:
- 能否改为 per-CPU 变量? —— 如果可以,这是最优解,零争用
- 只涉及简单值吗? —— atomic 操作足够
- 读多写少且读取极其频繁? —— RCU(需要内核 2.5+ 的 call_rcu)
- 读多写少且逻辑简单? —— seqlock
- 临界区很短且不可睡眠? —— spinlock
- 临界区较长或可能睡眠? —— mutex(优先)或 rw_semaphore
- 需要"等待事件"而非"保护临界区"? —— completion 或 waitqueue
- 需要计数信号量(最多 N 个持有者)? —— semaphore
十四、实战调试技巧
14.1 lockdep 启用后的开发流程
# 编译时启用
CONFIG_PROVE_LOCKING=y
CONFIG_LOCK_STAT=y
CONFIG_DEBUG_ATOMIC_SLEEP=y
# 运行时查看锁状态
$ cat /proc/lockdep_stats
$ cat /proc/lockdep_chains
$ cat /sys/kernel/debug/lockdep_stats
# 触发 lockdep 报错后的修复步骤:
# 1. 读取 /proc/lockdep 输出确定锁类 A 和 B
# 2. 查找两个锁的获取位置
# 3. 分析是否存在可能的循环依赖
# 4. 如果是明确合法的嵌套,用 *_nested() + lockdep_set_class() 标记
# 5. 如果确实是死锁,调整锁获取顺序或更换同步原语
14.2 锁争用分析
使用 perf 分析锁争用热点:
# 记录 mutex 争用的火焰图
$ perf record -e syscalls:sys_enter_futex -ag -- sleep 30
$ perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg
# 记录内核 spinlock 争用
$ perf record -e kvmmmu:kvm_mmutrace_lock -a sleep 10
14.3 KASAN 排查内存序问题
CONFIG_KASAN(Kernel Address Sanitizer)配合 CONFIG_KCSAN(Kernel Concurrency Sanitizer)可以检测数据竞争:
// KCSAN 会报告两个并发访问同一内存位置的冲突
void thread_a(void) {
shared_var = 1; // 写(无保护)
}
void thread_b(void) {
int x = shared_var; // 读(无保护)
}
// KCSAN 报告:
// WARNING: KCSAN: data race in ...
// write to ... (thread_a)
// read from ... (thread_b)
//
十五、内核 6.x 同步机制的新进展
15.1 Mutex HANDOFF 优化
Linux 6.x 引入了 mutex 的"直接传递"(handoff)优化:当高优先级任务因 mutex 被阻塞时,mutex 所有者会继承其优先级(PI 机制),并在释放时将所有权直接传给等待者,而非唤醒所有等待者。这降低了优先级反转(priority inversion)风险。
15.2 qspinlock 的 NUMA 感知
在大型 NUMA 系统中,qspinlock 引入了 NUMA 感知自旋:等待者首先在本地节点自旋,减少跨节点缓存行迁移的延迟。
15.3 RCU Tasks Rude
RCU Tasks Rude 是新的 RCU 变体,专门用于可抢占 RCU 中跟踪所有任务(包括内核线程),解决了传统 RCU Tasks 无法追踪长期睡眠的任务的问题。
15.4 内核 lockdown 与 LOCKDOWN_LSM
为了防御物理攻击和特权升级,内核引入了 lockdown LSM 模块,可以限制 root 访问以下资源:/dev/mem、/dev/kmem、/proc/kcore、PCI 配置空间等。
十六、总结与最佳实践清单
以下是 Linux 内核同步与锁机制的核心工程原则:
- 最小化临界区:只在锁保护下做最少的工作
- 锁的顺序一致性:所有代码路径必须按相同顺序获取多个锁
- 避免在持有锁时调用可能睡眠的函数:spinlock 持有者绝对不能睡眠
- 优先选择更轻量的机制:atomic → rcu(读) → spinlock → mutex → semaphore
- 消除伪共享:hot 变量之间加 ____cacheline_aligned
- 能用 per-CPU 变量就不用全局共享:从根本上消除竞态
- 使用 lockdep 开发:在测试环境中始终开启 PROVE_LOCKING
- 读多写少场景首选 RCU 或 seqlock:避免成为性能瓶颈
- 正确使用内存屏障:release-acquire 语义比全屏障更高效
- 锁层次与嵌套使用 _nested API 标记:lockdep 会验证
掌握这些同步机制,不仅能写出正确率高、竞态条件少的内核代码,更能在性能与正确性之间找到最佳平衡点。同步是内核开发的"内功",需要在实践中不断磨练。

发表评论 取消回复