Linux 内核同步机制与锁优化深度实战
从原语实现到生产级优化,覆盖 spinlock、mutex、rwlock、seqlock、semaphore 内部原理,lockdep 死锁检测,内存屏障,percpu 原子操作,缓存伪共享,NUMA 感知策略。
一、为什么锁是内核最复杂的子系统
Linux 内核运行在真正的并发环境中:多核 CPU、中断、软中断、tasklet、内核抢占、SMP、NUMA 拓扑、异步 I/O 完成事件——所有这些可能同时访问同一块共享数据。一个看似无害的写操作,如果在错误的时机被打断、被另一个核并发修改、或者被编译器乱序,都会在凌晨三点引发"幽灵 bug"。
内核同步的难度不仅来自并发源的多样性,更来自性能与正确性的永恒博弈:锁太细,开销剧增;锁太粗,系统串行化;锁错了,死锁或活锁可能数周才触发一次。
二、自旋锁 Spinlock:短临界区的王者
2.1 内部实现
Linux 内核的 spinlock 从最早期的简单 xchg/test-and-set,演化到 MCS 锁优化版本,再到 PREEMPT_RT 下的 rtmutex 替代实现。
// 简化版 MCS spinlock 核心逻辑
struct mcs_spinlock {
struct mcs_spinlock *next;
int locked; // 1 = 获得锁
int count; // 用于嵌套
};
// 入锁:原子交换将自己加入队尾
void mcs_spin_lock(struct mcs_spinlock **lock, struct mcs_spinlock *node)
{
struct mcs_spinlock *prev;
node->locked = 0;
node->next = NULL;
prev = xchg(lock, node); // 原子交换队尾指针
if (likely(prev == NULL)) {
// 无等待者,直接获得锁
return;
}
// 有前驱,设置前驱的 next 指针并等待
prev->next = node;
smp_wmb(); // 保证 next 写入对后继可见
while (!READ_ONCE(node->locked))
cpu_relax(); // PAUSE 指令降低功耗
}
// 出锁:唤醒后继
void mcs_spin_unlock(struct mcs_spinlock **lock, struct mcs_spinlock *node)
{
struct mcs_spinlock *next = READ_ONCE(node->next);
if (likely(next == NULL)) {
// 可能无后继,尝试原子解锁
if (likely(cmpxchg_relaxed(lock, node, NULL) == node))
return;
// 后继正在入锁中,等它完成链接
next = xchg(&node->next, NULL);
while (!next)
cpu_relax();
}
smp_store_release(&next->locked, 1);
}
核心思想:每个等待者自旋在自己的局部变量上(node->locked),而非全局变量。这样只有一个缓存行(队尾指针)被反复读,避免缓存行 bounce。
2.2 Ticket Spinlock 与 MCS 的区别
| 特性 | Ticket Spinlock | MCS Spinlock | Queued Spinlock (qspinlock) |
|---|---|---|---|
| 等待方式 | 自旋在 owner counter |
自旋在本地 locked |
三级队列 + 本地 MCS node |
| 缓存行争用 | 高 | 低 | 低 |
| 公平性 | FIFO | FIFO | 队头 FIFO,队尾可扩展 |
| ARM 支持 | 旧版 | 需要额外实现 | 原生支持 |
ARM64 上的 qspinlock 采用三级队列:tail_idx → tail_cpu → tail_qnode,减少缓存行传播开销。
2.3 PREEMPT_RT 下的 rtmutex
标准内核中,spinlock 在持有期间会关抢占。PREEMPT_RT 项目中,spinlock 被替换为基于 rtmutex 的可睡眠锁:
PREEMPT_RT: spin_lock() → rt_mutex_acquire() (可睡眠)
Standard: spin_lock() → preempt_disable() + qspinlock (忙等)
关键区别:rtmutex 支持优先级继承(Priority Inheritance)。当高优先级任务持有锁时,临时提升其优先级,避免中优先级任务造成的优先级反转。
2.4 使用场景和陷阱
// ✅ 正确:保护极短临界区(< 100ns)
spin_lock(&irp->lock);
list_add_tail(&new_ctx, &irp->queue);
spin_unlock(&irp->lock);
// ❌ 错误:spinlock 内调用睡眠函数
spin_lock(&lock);
kmalloc(size, GFP_KERNEL); // 分配失败会触发内存回收,可能睡眠!
spin_unlock(&lock);
核心守则:spinlock 保护的临界区内绝不能调用可能睡眠的函数(kmalloc GFP_KERNEL、copy_from_user、mutex_lock、down 等)。
三、互斥锁 Mutex:通用睡眠锁的标准实现
3.1 数据结构
struct mutex {
atomic_long_t owner; // 持有者的 task_struct 指针 + 标志位
raw_spinlock_t wait_lock; // 保护等待队列
struct list_head wait_list; // 等待任务的队列
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
struct optimistic_spin_queue osq; // 乐观自旋优化
#endif
};
owner 字段编码了两个信息:低两位是标志位(MUTEX_FLAG_WAITERS = 1,MUTEX_FLAG_PICKUP = 2),其余位是持有者的指针。通过原子操作快速判断锁是否被持有。
3.2 获取流程
mutex_lock()
↓
trylock_fast() —— atomic_try_cmpxchg_acquire() 快速路径
↓ 失败
mutex_lock_slowpath()
↓
osq_lock() —— 乐观自旋:检查持有者是否在运行,若是则自旋等待
↓ 持有者正在运行,快速释放
__mutex_lock_or_spin()
↓
wait_list加入 → schedule() 让出CPU
↓ 被唤醒
从 wait_list 摘除,标记 PICKUP 标志
乐观自旋优化:当检测锁持有者正在某个 CPU 上运行时(通过 task_on_cpu()),新请求者不会立即切换到睡眠,而是自旋等待。因为持有者通常会在几微秒内释放锁。实测在高竞争场景下,乐观自旋可以减少 30-50% 的上下文切换。
3.3 释放流程
mutex_unlock()
↓
atomic_set(&lock->owner, 0) 或 xchg 清除标志
↓
如果没有 WAITERS 标志 → 快速返回
↓
有等待者 → 从 wait_list 取出第一个任务 → wake_up_q()
↓ 如果 PICKUP 标志已设(优化了唤醒延迟)
3.4 避免死锁的策略
内核 mutex 通过三种机制避免死锁:
-
锁定顺序约定:代码审查 + lockdep 检查。全局锁的获取顺序被严格约定。
-
不可重入:mutex 不允许同一线程重复获取(与 recursive mutex 不同),避免简单的自死锁。
-
lockdep 验证器:编译期静态分析,运行时追踪所有锁的获取顺序,一旦发现潜在的死锁循环立即报告。
四、读写锁 RWLock 与读写信号量
4.1 RWLock 的问题:写者饥饿
早期内核中 rwlock 被广泛批评:当读者高并发持续持有锁时,写者可能永远无法获得锁。在 64 位系统上,rwlock 使用 32 位计数器(24 位读者计数 + 1 位写者锁),当所有 CPU 上都有读者在忙等时,写者的原子操作需要等待 2^24 个读者全部释放后才能生效。
4.2 rw_semaphore 的队列化解决方案
现代内核推荐 rw_semaphore 代替 rwlock,它在内核配置了 CONFIG_RWSEM_SPIN_ON_OWNER 后具备乐观自旋:
// rw_semaphore 获取写锁
void down_write(struct rw_semaphore *sem)
{
// 尝试快速获取
if (likely(atomic_long_try_cmpxchg_acquire(&sem->count,
0, RWSEM_WRITER_LOCKED)))
return;
// 慢路径:进入等待队列
rwsem_down_write_failed(sem);
}
公平模式(公平读写):Linux 5.x 后引入了 rwsem 的公平模式(类似 qspinlock),写者在注册等待后,后续读者会被"阻塞在门前",避免写者被饿死。
4.3 Per-CPU 读写锁:rwlock 的最佳替代
当"读多写少"模式明确时,percpu_rwlock 或 RCU 是更好的选择:
struct percpu_rwlock {
atomic_t __percpu *read_lock; // 每 CPU 的读计数器
atomic_t write_lock;
};
// 获取读锁:只操作本 CPU 缓存行
void percpu_read_lock(struct percpu_rwlock *rw)
{
this_cpu_inc(*rw->read_lock);
smp_mb(); // 内存屏障确保写者见
}
// 获取写锁:等待所有 CPU 读计数归零
void percpu_write_lock(struct percpu_rwlock *rw)
{
atomic_set(&rw->write_lock, 1);
smp_mb();
for_each_online_cpu(cpu) {
while (per_cpu(*rw->read_lock, cpu))
cpu_relax();
}
}
优势:读者间完全无竞争(各自 CPU 独立),写者代价 O(n_cpu)。实测读性能比普通 rwlock 高 5-10 倍。
五、SeqLock:读者无锁的黑科技
5.1 设计哲学
seqlock 的核心思想:让时间戳充当锁,让读者检测写入冲突。
struct seqlock_t {
seqcount_t seq; // 序列号计数器
spinlock_t lock; // 仅保护写者
};
// 读者路径
unsigned seqbegin;
do {
seqbegin = read_seqcount_begin(&lock->seq);
/* 读取共享数据 */
} while (read_seqcount_retry(&lock->seq, seqbegin));
// 写者路径
write_seqlock(&lock->lock);
/* 修改共享数据 */
write_sequnlock(&lock->lock);
5.2 内部原语
序列号在写者持有期间为奇数,正常为偶数。读者循环检测序列号变化:
// 读者开始:读取序列号
static inline unsigned read_seqcount_begin(seqcount_t *s)
{
unsigned ret;
do {
ret = smp_load_acquire(&s->sequence);
if (unlikely(ret & 1)) // 写者在操作中
cpu_relax();
} while (unlikely(ret & 1));
return ret;
}
// 读者重试:检查序列号变化
static inline int read_seqcount_retry(seqcount_t *s, unsigned start)
{
smp_rmb();
return unlikely(s->sequence != start); // 被写者中断过
}
5.3 典型应用场景
seqlock 特别适用于读极多、写极少、数据一致性可以"重来一次"的场景:
// 内核时间子系统:jiffies_64 的更新
u64 get_jiffies_64(void)
{
u64 seq, ret;
do {
seq = read_seqbegin(&jiffies_lock);
ret = jiffies_64;
} while (read_seqretry(&jiffies_lock, seq));
return ret;
}
// 网络包统计:TCP 字节计数
static inline u64 tcp_clock_ns(void)
{
struct tk_base *base = ...
u64 val;
unsigned seq;
do {
seq = raw_read_seqcount_begin(&base->seq);
val = base->clock;
} while (raw_read_seqcount_retry(&base->seq, seq));
return val;
}
限制:如果读者处理时间过长,写者会饥饿(读者永远重试),因此读者临界区必须极短(通常 < 1000 cycles)。
六、信号量 Semaphore 与完成量 Completion
6.1 计数信号量:资源池管理
当需要允许N 个并发访问同一资源池时,使用 semaphore:
struct semaphore {
raw_spinlock_t lock;
unsigned int count; // 可用资源数
struct list_head wait_list;
};
void down(struct semaphore *sem) // P 操作
{
unsigned long flags;
spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0)) {
sem->count--;
spin_unlock_irqrestore(&sem->lock, flags);
} else {
// 加入等待队列,睡眠
...
spin_unlock_irqrestore(&sem->lock, flags);
schedule();
}
}
void up(struct semaphore *sem) // V 操作
{
...
// 唤醒 wait_list 中的第一个等待者
}
与 mutex 的对比:
| 特性 | mutex | semaphore |
|---|---|---|
| 所有者 | 严格所有者追踪 | 无 ("释放者)" |
| 递归获取 | 禁止 | 允许 |
| 资源计数 | 1(独占) | N(共享池) |
| 乐观自旋 | 有 | 无 |
| 声明宏 | DEFINE_MUTEX |
DEFINE_SEMAPHORE |
6.2 Completion:事件通知的信号量
当线程需要"等待某个事件发生"时使用 completion,而不仅仅是获取资源:
struct completion {
unsigned int done;
wait_queue_head_t wait;
};
// 等待方(消费者)
wait_for_completion(struct completion *c);
// 发送方(生产者)
complete(struct completion *c);
completion 在内核中的典型用法:模块卸载等待、I/O 完成通知、kthread_park 唤醒。
与 semaphore 的语义差异:completion 的 complete() 只会唤醒一个等待者(或全部,如果调用 complete_all()),而 semaphore 的 up() 只唤醒一个等待者。completion 是"信号已发出",semaphore 是"资源可用"——前者更适合一次性事件。
6.3 SRCU:可睡眠的 RCU
SRCU(Sleepable RCU)是一种特殊的同步原语,允许读者在持有期间睡眠(但代价更高):
调用链:
srcu_read_lock() // 递增本 CPU 读者计数(preempt 保护)
sleepable_code() // 可睡眠
srcu_read_unlock() // 递减读者计数
synchronize_srcu() // 等待现有读者完成,然后回调
SRCU 在内核中使用场景较少(如 vcpu 阻塞),但比 RCU 更灵活。其内部使用 percpu 计数器 + 全局 epoch 追踪来实现所有权验证。
七、原子操作与内存屏障
7.1 原子操作原语
// 基本原子操作
atomic_t v = ATOMIC_INIT(0);
atomic_inc(&v); // 原子的 v += 1
atomic_add(2, &v); // 原子的 v += 2
atomic_sub_return(1, &v); // 原子的 v -= 1,返回新值
atomic_cmpxchg(&v, old, new); // CAS:如果 v == old,则 v = new
// 64 位 atomic64
typedef struct { s64 counter; } atomic64_t;
atomic64_inc(&v);
// 位级别操作
set_bit(3, &flags);
clear_bit(3, &flags);
test_and_set_bit(3, &flags); // 返回旧值并设置
test_and_clear_bit(3, &flags);
// 无 LOCK 前缀的版本(单向屏障)
__set_bit(3, &flags); // 无序保证
7.2 内存屏障的四个层次
// Linux 内核的四层屏障:
// 1. 编译器屏障:阻止编译器重排
barrier();
// 2. SMP 内存屏障:CPU 级别的全屏障(读+写)
smp_mb(); // 保证 mb 前后的读写不乱序
smp_rmb(); // 读屏障:保证之前的读在之后的读之前完成
smp_wmb(); // 写屏障:保证之前的写在之后的写之前完成
// 3. 获取/释放语义(C11 memory_order)
smp_load_acquire(&ptr); // 获取语义:之后的读写不被重排到此之前
smp_store_release(&ptr, v); // 释放语义:之前的读写不被重排至此之后
// 4. I/O 屏障(设备层面)
mmiowb(); // MMIO 写屏障
wmb(); // 包含 DMA 顺序
为什么需要内存屏障:
现代 CPU(尤其是 ARM64/RISC-V)采用弱内存模型(Weak Memory Model)。CPU 会为了性能乱序执行指令。例如:
// CPU 线程 A
a = 1;
flag = 1;
// CPU 线程 B
while (!flag) {} // 等待 flag
print(a); // 可能打印 0!
B 可能先看到 flag=1,然后才看到 a=1。插入屏障即可解决:
// CPU 线程 A
a = 1;
smp_wmb(); // 保证写入顺序
flag = 1;
7.3 原子操作的内存序选择
// 宽松序——无同步,仅保证原子性
atomic_fetch_add_explicit(&v, 1, memory_order_relaxed);
// 获取序——和 Release 配对使用
int old = atomic_fetch_add_explicit(&v, 1, memory_order_acquire);
// 释放序——和 Acquire 配对使用
atomic_store_explicit(&ptr, new_val, memory_order_release);
// 全序——默认组合,最高开销
atomic_fetch_add(&v, 1); // = memory_order_seq_cst
最佳实践:追求性能的自定义无锁结构体使用 relaxed/acquire/release;默认场景使用 seq_cst。在 Linux 内核中,smp_load_acquire 和 smp_store_release 被大量用于链表、引用计数等场景。
八、Per-CPU 变量与本地原子操作
8.1 Per-CPU 计数器:统计的终极武器
// 动态分配 per-cpu 变量
static DEFINE_PER_CPU(long, event_count);
// 等价于:
// static __section(".data..percpu") long event_count;
// 操作(关抢占保护,无需额外锁)
this_cpu_inc(event_count); // 本 CPU += 1
this_cpu_add(event_count, n); // 本 CPU += n
raw_cpu_add(event_count, n); // 无 preempt 保护(慎用)
// 读取所有 CPU 的总和
long sum = 0;
int cpu;
for_each_online_cpu(cpu)
sum += per_cpu(event_count, cpu);
应用场景:网络包计数器(netstat 的 RX/TX bytes)、调度器运行队列长度统计、软中断分发计数、NUMA 节点间统计。
8.2 本地原子操作:Local Atomic vs Per-CPU
// local atomic:作用于当前 CPU,不需要 preempt 保护
local_inc(&my_counter);
local_dec(&my_counter);
local_cmpxchg(&my_counter, old, new);
// 与 atomic 的区别
local_inc(&v); // 仅保证本 CPU 原子性,无 LOCK 前缀
atomic_inc(&v); // 全局原子,多核间同步,有 LOCK 或 LSE 指令
// 例:ARM64 上 local_inc 是普通 str+adds,atomic_inc 是 stadd 指令
local_inc 的优势:无 LOCK 前缀,无数 MESI 协议开销,性能高 10-100 倍。
8.3 读取所有 CPU 总和的优化
逐 CPU 遍历统计时,32 位计数器可能溢出。内核使用序列号技巧保证一致性:
// /proc/net/snmp 中的典型模式
int cpu;
unsigned int tmp;
long sum = 0;
for_each_possible_cpu(cpu) {
tmp = *per_cpu_ptr(seq->private->ext, cpu);
sum += tmp;
tmp ^= 0xffffffffu;
tmp += 1;
if ((int)tmp < 0)
sum += 0x100000000LL;
/* 处理 32-bit 环绕与总和 */
}
九、Reference Counting 与 RCU 回调
9.1 kref 与 refcount_t
struct kref {
refcount_t refcount;
};
void kref_init(struct kref *kref)
{
refcount_set(&kref->refcount, 1);
}
void kref_get(struct kref *kref)
{
refcount_inc(&kref->refcount);
}
int kref_put(struct kref *kref, void (*release)(struct kref *kref))
{
if (refcount_dec_and_test(&kref->refcount)) {
release(kref);
return 1; // 已释放
}
return 0;
}
refcount_t vs atomic_t:refcount_t 是改进版本,增加了溢出检测(不会回绕到 0 或负数)——CVE 研究中 kernel UAF 漏洞的常见根源就是 refcount 错误。
9.2 kref 的饱和递增
// 当 refcount 从 0 增加时(引用零对象的尝试)
bool refcount_inc_not_zero(refcount_t *r)
{
unsigned int new, val = atomic_read(&r->refs.counter);
do {
if (unlikely(!val))
return false;
// 检查溢出
if (unlikely(val == UINT_MAX))
return false;
new = val + 1;
} while (!atomic_try_cmpxchg_relaxed(&r->refs.counter, &val, new));
return true;
}
9.3 RCU 的现代用法与局限
// 经典 RCU 读取
rcu_read_lock();
ptask = rcu_dereference(g_task_ptr); /* 必须是 rcu_dereference */
if (ptask)
do_something(ptask);
rcu_read_unlock();
// 更新:原子替换 + 等待回调
new_task = kmalloc(...);
spin_lock(&g_lock);
old_task = rcu_dereference_protected(g_task_ptr, lockdep_is_held(&g_lock));
rcu_assign_pointer(g_task_ptr, new_task);
spin_unlock(&g_lock);
synchronize_rcu(); /* 等待现有读者完成 */
kfree(old_task);
十、Lockdep:死锁检测之神
3.1 Lockdep 的核心能力
Lockdep 在编译期创建一个 锁依赖图,运行时验证:
- 获取顺序:A→B 和 B→A 不能在同一执行路径中出现
- 中断上下文:锁在进程上下文获取后,不能在 IRQ 上下文再次获取(除非使用 _irqsave)
- 递归获取:非递归锁不被允许
- 锁类别验证:同一锁类的不同实例必须遵循固定顺序
关键输出:当检测到潜在死锁、不安全的使用场景(如 mutex 在中断上下文持有)时,lockdep 会打印详细的信息,包括锁的获取路径和调用栈。
3.2 Lockdep 的类分级管理
内核使用 lock_class_key 为每个锁分配类别键:
// 静态初始化键
static struct lock_class_key my_lock_key;
// 初始化锁时分配类别
spinlock_t my_lock;
spin_lock_init(&my_lock);
// 或者使用 DEFINE 宏
DEFINE_SPINLOCK(my_lock); // 自动分配类
// 显式声明类别
lockdep_set_class(&my_lock, &my_lock_key);
3.3 Lockdep 的典型案例分析
======================================================
[ INFO: possible circular dependency detected ]
------------------------------------------------------
kworker/u16:0/45 is trying to acquire lock:
00000000decd8e6a (&dev->lock){+.+.}, at: driver_lock+0x3c/0x80
but task is already holding lock:
00000000a38e2e3f (&common->mutex){+.+.}, at: common_op+0x42/0xa0
backtrace of second lock:
mutex_lock+0x6e/0xa0
common_op+0x42/0xa0
subsystem_handler+0x25/0x60
根因分析:A 路径持有 mutex 后尝试获取 dev->lock;B 路径持有 dev->lock 后尝试获取 mutex。当两个路径在两个 CPU 上并行执行时,导致死锁。
解决方案:统一获取顺序(总是先 mutex 后 dev->lock),或使用 lockdep 的 lock_acquire_exclusive 声明互斥排序规则。
3.4 Lockdep 的生产部署
// /proc/lockdep_stats 中查看统计
// 关键指标:
// - held lock count: 当前持有的锁数
// - lock depth: 最大锁嵌套深度
// - lock class count: 注册的总锁类
// 运行时开关(不要在生产环境禁用!)
static int __init disable_lockdep(char *str)
{
disable_lockdep_flag = 1;
return 0;
}
__setup("nolockdep", disable_lockdep);
性能开销:lockdep 增加约 10-20% 内核开销。建议开发/测试环境总是启用;生产环境可以关闭,但 CI 中必须运行全量 lockdep 测试。
十一、缓存伪共享与缓存行优化
11.1 什么是 False Sharing
当两个 CPU 修改同一缓存行(cache line,通常 64 字节)内的不同变量时,会发生缓存伪共享:CPU A 的写导致整个缓存行无效,CPU B 被迫从内存重新加载,即使它们实际不共享任何数据。
缓存行(64 字节):
| counter_A | counter_B | flag | padding |
CPU 0 写 CPU 1 写
→ 反复 bounce
11.2 检测 False Sharing
# 方法1:perf c2c(cache-to-cache)
perf c2c record -a -- sleep 10
perf c2c report -c pid,tid,iaddr
# 关注指标:
# - Hitm (Hit Modified): 其他 CPU 持有该缓存行的修改副本
# - LLC Miss: 末级缓存未命中
# 方法2:LBR + 采样
perf record -e LLC-load-misses -a -- sleep 10
perf report --sort=dso,symbol
perf c2c 输出样例:
Cacheline Data Shared Samples Load Hitm Store Hitm
0x7f3b800a2c40 120 85 0
50.0% udp_recvmsg+0x3f0 (ksrc/linux/net/ipv4/udp.c)
30.0% udp_sendmsg+0x280 (ksrc/linux/net/ipv4/udp.c)
20.0% tcp_recvmsg+0x1c0 (ksrc/linux/net/ipv4/tcp.c)
如果 Hitm > 5% of LLC misses,通常说明存在伪共享问题。
11.3 消除 False Sharing 的技术
方案1:对齐填充(padding)
struct my_stats {
atomic_t counter_a; // 占 4 字节
char pad[60]; // 填充到 64 字节边界
atomic_t counter_b; // 占 4 字节
char pad2[60]; // 填充到 64 字节边界
};
方案2:缓存行对齐分配(编译期)
// 定义缓存行对齐的结构体
struct aligned_counter ____cacheline_aligned_in_smp {
atomic_t value;
};
struct aligned_counter counters[NR_CPUS];
方案3:L1 缓存行对齐(防御 CPU 预取器)
struct padded_var {
atomic_t value;
} ____cacheline_aligned; // 对齐到 L2/L3 边界
// 或显式填充到 128 字节(覆盖相邻预取)
struct padded_var {
atomic_t value;
char pad[124]; // 64(本行) + 64(预取邻居)
} ____cacheline_aligned;
11.4 内核中的实际案例
案例1:网络包统计
// 旧版(存在伪共享)
struct netns_mib {
long tcp_in_segs;
long tcp_out_segs;
long udp_in_datagrams;
long udp_out_datagrams;
// ... 120+ 计数器全在少数几个缓存行
};
// 新版(每缓存行独立)
struct pcpu_dstats {
atomic64_t rx_packets;
atomic64_t tx_packets;
// 独占一个缓存行
} ____cacheline_aligned_in_smp;
案例2:调度实体
struct cfs_rq {
struct load_weight load;
unsigned long runnable_weight;
// ...
struct rb_root_cached tasks_timeline;
struct sched_entity *curr;
// 拆分到同一cfs_rq在不同CPU上操作的热点字段
} ____cacheline_aligned;
十二、NUMA 感知同步策略
12.1 NUMA 对锁性能的影响
在 NUMA 系统中,远程内存访问延迟是本地访问的 2-5 倍。传统 ticket spinlock 的队尾指针可能位于另一个 NUMA 节点,频繁的跨节点缓存一致性协议(cache coherency)导致性能灾难。
12.2 内核 NUMA 感知锁的实现
MCS Spinlock 的队列化已经天然缓解 NUMA 问题——每个等待者自旋在本地内存。但 rwlock 中仍存在 NUMA 不友好路径:
// 危险的 NUMA 场景
rwlock_t global_lock; // 锁变量在 Node 0
// Node 1 上的 CPU 持续尝试获取锁
// 每次获取失败 → 锁变量从 Node 0 跨 QPI/UPI 传输
// 性能:吞吐量下降 5-10 倍
12.3 优化策略:分片锁(Sharding)
// 将一个大锁拆分为 N 个小锁,每个 CPU 组使用不同锁
#define LOCK_NS 64
struct per_lock_ns {
spinlock_t lock;
struct list_head list;
} ____cacheline_aligned;
static struct per_lock_ns sharded_locks[LOCK_NS];
void sharded_add(struct elem *e, int key)
{
int ns = key & (LOCK_NS - 1);
spin_lock(&sharded_locks[ns].lock);
list_add(&e->list, &sharded_locks[ns].list);
spin_unlock(&sharded_locks[ns].lock);
}
核心思路:冲突的锁获取被隔离在不同锁实例上。如果 key 均匀分布,锁竞争减少 N 倍。
12.4 Per-CPU 锁 + 经典锁的混合设计
struct percpu_rw_semaphore_cpu {
struct rw_semaphore brw;
};
struct percpu_rw_semaphore {
struct percpu_rw_semaphore_cpu *prim;
unsigned int sig;
};
// 读路径:无读者间竞争
// 写路径:原子关闭所有 CPU 的信号量,然后同步等待
典型应用:percpu_rw_semaphore 在 Linux 内核中用于 mmap_sem(内存映射锁),将 O(N_cpu) 的读性能提升到了接近无锁。
十三、无锁编程与 Hazard Pointer
13.1 Hazard Pointer 的实现
struct hp_record {
struct hp_record *next;
atomic_ptr_t hp[MAX_HP]; // 每个线程的危险指针数组
unsigned int active;
};
struct hp_chunk {
int nr_records;
struct hp_record records[0];
};
// 使用:保护指针在 RCU-like 场景中的安全释放
void protect(struct hp_record *rec, int i, void *ptr)
{
atomic_store_explicit(&rec->hp[i], ptr, memory_order_release);
smp_wmb(); // 保护指针可见性
if (atomic_load(&global_ptr) != ptr) {
atomic_store(&rec->hp[i], NULL);
throw;
}
}
void retire(void *ptr)
{
// 推迟释放,直到没有线程引用它
add_to_retire_list(ptr);
if (retire_list_size > R) {
scan_and_free(thread_priv->chunk);
}
}
13.2 RCU 在更高层面的替代
对于更简单的场景,无锁编程的规则是:
- 单写者-多读者:使用 seqlock + 快照模式
- 多写者-少读者:使用
rcu_assign_pointer+kfree_rcu - CAS 链表:引用霍华德·旺(Howard)的论文 Unreality 技巧
- 树结构:使用 epoch-based reclamation
关键:无锁代码的正确性证明极为困难,应优先使用内核已有原语(RCU、seqlock、percpu)。
十四、锁调试与性能分析
14.1 Lockstat:锁竞争统计
# 开启锁统计
echo 1 > /proc/sys/kernel/lock_stat
# 运行测试后查看
cat /proc/lock_stat
# 关键输出段:
# class name con-bounces waittime-total holdtime-total
# &inode->i_mutex 1.3M 8.4s 1.2s
# &mm->mmap_sem 890K 12.6s 3.4s
14.2 Ftrace 锁追踪
# 追踪锁获取事件
echo 1 > /sys/kernel/debug/tracing/events/lock/lock_acquire/enable
echo 1 > /sys/kernel/debug/tracing/events/lock/lock_release/enable
cat /sys/kernel/debug/tracing/trace_pipe
14.3 性能调优决策树
锁竞争严重?
├── 是 → 性能分析(lock_stat / perf c2c)
│ ├── 缓存线争用(false sharing)→ 对齐优化
│ ├── 同一锁被不同 CPU 频繁争抢 → 分片锁/sharded lock
│ ├── 写者饥饿(rwlock)→ rw_semaphore 公平模式 或 seqlock
│ └── 锁顺序不一致 → lockdep 检测 🔥
└── 否 → 当前锁策略是否安全?
├── lockdep 报告问题?→ 修复 锁顺序/中断上下文 使用
└── 是否可简化为 RCU?→ 极度读多写少 → RCU + call_rcu
十五、生产环境最佳实践与检查清单
15.1 锁选型速查表
| 场景 | 推荐原语 | 替代 | 绝对不要 |
|---|---|---|---|
| 短临界区,中断上下文 | spinlock irqsave | spinlock bh | mutex, semaphore |
| 短临界区,进程上下文 | spinlock | mutex | rwlock(写饥饿) |
| 可能睡眠的临界区 | mutex | rw_semaphore(独享锁) | spinlock |
| 读多写少,热点统计 | seqlock | RCU | rwlock |
| 写多读少,状态机 | rw_semaphore(公平) | spinlock | rwlock |
| 时间读取(无状态) | seqlock | RCU(rcu_dereference) | spinlock |
| 资源池计数 | semaphore | mutex + 计数 | atomic(释放者问题) |
| 事件等待 | completion | semaphore(count=1) | spin锁 busy-wait |
| 每 CPU 计数器 | percpu 变量 | local 原子 | 全局 atomic |
15.2 代码审查要点
- spinlock 边界检查:用
might_sleep()和might_fault()标注可能睡眠的函数,spinlock 内调用时产生警告 - 中断上下文使用规则:
spin_lock_irqsave()vsspin_lock_irq()——如果不知当前 IRQ 状态,总是用 irqsave 变体 - 递归获取:非递归 mutex 不允许递归获取;递归场景用 semaphore 或 lockdep 声明子类别
- 锁粒度:热路径上的锁,临界区越小越好;长临界区应考虑 RCU 或 seqlock
- NUMA 感知:分片策略前用
perf c2c确认 cache-line bounce 位置 - lockdep 零警告:任何 lockdep WARNING 等于潜在漏洞,CI 阶段不允许通过
15.3 关键内核配置选项
CONFIG_DEBUG_SPINLOCK=y # spinlock 调试(未初始化锁检测)
CONFIG_DEBUG_MUTEXES=y # mutex 调试(递归、死锁)
CONFIG_PROVE_LOCKING=y # lockdep 依赖图验证
CONFIG_LOCK_STAT=y # 锁统计(lock_stat)
CONFIG_LOCKDEP=y # 核心基础设施
CONFIG_DEBUG_ATOMIC_SLEEP=y # 原子上下文中睡眠检测
六、总结
Linux 内核同步子系统从最初简单的 cli/sti 中断控制,发展到今天包含 spinlock/mutex/rw_semaphore/seqlock/completion/RCU/lockdep 在内的完整矩阵。理解这些原语的内部实现是内核开发的基本功,而正确选择和部署它们,则是在高性能与正确性之间精细权衡的艺术。
三个原则始终贯穿: - 正确性优先:lockdep 是你的朋友,不是负担 - 测量再优化:perf c2c + lock_stat 不可跳过 - 简化原语选择:能用 percpu 就不用 global atomic → 能用 RCU 就不用 seqlock → 能用 seqlock 就不用 spinlock → 能用 mutex 就不用 semaphore

发表评论 取消回复