1.每 CPU 变量 (Per-CPU Variables)
核心思想:为每个 CPU 分配一份独立的数据副本,避免缓存行 bouncing(缓存行在多个 CPU 间来回迁移)。
c
// 定义
DEFINE_PER_CPU(int, my_counter);
// 或动态分配
void __percpu *ptr = alloc_percpu(int);
// 安全访问(关抢占 + 获取当前 CPU 指针)
int *p = get_cpu_var(my_counter);
(*p)++;
put_cpu_var(my_counter);
// 远程访问(其他 CPU)
per_cpu(my_counter, cpu_id);
关键点:
- 通过
get_cpu_var() / put_cpu_var()配对使用,内部会关闭内核抢占,防止在访问期间被调度到其他CPU - 数据按
CPU缓存行对齐(通常64/128字节),避免false sharing - 适用于计数器、统计信息等高频写、低频聚合读的场景
2.内核抢占 (Kernel Preemption)
概念:允许高优先级任务在内核态执行时抢占当前任务。
c
// 抢占控制 API
preempt_disable(); // 增加抢占计数
preempt_enable(); // 减少抢占计数,必要时检查抢占
preempt_count(); // 当前抢占计数
// 自愿调度
schedule();
cond_resched(); // 如果 need_resched 被设置,则主动让出
抢占点:
- 中断返回时(irq_exit)
- 开启抢占时(preempt_enable)
- 显式调用 schedule()
- 从系统调用返回用户态时
与自旋锁的关系:持有自旋锁时会自动关闭抢占(preempt_disable 嵌入在 spin_lock 中),防止持有锁时被抢占比导致死锁。
3.内存屏障 (Memory Barriers)
目的:防止编译器和 CPU 对内存操作进行乱序重排,保证多核间的内存可见性顺序。
c
// 编译器屏障
barrier();
// 通用内存屏障
smp_mb(); // 全屏障:之前的读写都完成后,才能执行之后的读写
smp_rmb(); // 读屏障
smp_wmb(); // 写屏障
// 带依赖的屏障(ARM/弱序架构常用)
smp_read_barrier_depends();
// 与锁配对的隐式屏障
spin_lock/unlock() // 隐含全屏障
mutex_lock/unlock() // 隐含全屏障
atomic_*_return() // 隐含全屏障
使用原则:通常不需要显式使用,应优先使用原子操作、锁等高层抽象,它们已内含正确的屏障语义。
4.原子变量 (Atomic Variables)
实现:依赖底层 CPU 的原子指令(如 x86 的 LOCK 前缀、XCHG、CMPXCHG;ARM 的 LDREX/STREX)。
c
atomic_t v = ATOMIC_INIT(0);
atomic_set(&v, 5);
int val = atomic_read(&v);
atomic_inc(&v);
atomic_dec_and_test(&v); // 递减并测试是否为 0
atomic_add_return(3, &v); // 加法并返回新值(含内存屏障)
atomic_cmpxchg(&v, old, new); // CAS 操作
位原子操作:
c
set_bit(0, &bitmap); // 原子设置第 0 位
test_and_set_bit(0, &bitmap); // 测试并设置(CAS 语义)
适用场景:计数器、标志位、引用计数等极短临界区。
5.自旋锁 (Spinlock)
机制:忙等待锁,持有锁期间关闭内核抢占(但不关中断)。
c
spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock); // 获取锁,关抢占
// 临界区
spin_unlock(&lock); // 释放锁,开抢占
// 中断安全版本(防止中断处理程序也尝试获取同一把锁导致死锁)
spin_lock_irqsave(&lock, flags);
spin_unlock_irqrestore(&lock, flags);
// 下半部安全版本
spin_lock_bh(&lock);
spin_unlock_bh(&lock);
核心规则:
- 临界区必须极短(不能睡眠、不能调度、不能访问用户空间)
- 单核非抢占内核中,自旋锁退化为空操作(仅关抢占)
- 锁粒度要细,减少竞争
5.1.spin_lock --- 基础版
c
spin_lock(&lock);
// 临界区
spin_unlock(&lock);
实际做了什么:
c
preempt_disable(); // 关闭内核抢占
while (!trylock(&lock)) // 忙等待
;
保护范围:仅屏蔽同 CPU 上的其他进程上下文(通过关闭抢占),以及其他 CPU 上的所有上下文(通过锁变量本身)。
不保护:本地 CPU 上的中断、软中断、NMI。
5.2.spin_lock_irqsave --- 中断安全版
c
unsigned long flags;
spin_lock_irqsave(&lock, flags);
// 临界区
spin_unlock_irqrestore(&lock, flags);
实际做了什么:
c
local_irq_save(flags); // 保存当前中断状态,关闭本地硬中断
preempt_disable(); // 关闭内核抢占
while (!trylock(&lock))
;
保护范围:屏蔽了本地 CPU 上的一切------进程、中断、软中断、NMI。以及其他 CPU 上的所有上下文(通过锁变量本身)。
5.3.spin_lock_bh --- 下半部安全版
c
spin_lock_bh(&lock);
// 临界区
spin_unlock_bh(&lock);
实际做了什么:
c
local_bh_disable(); // 关闭本地软中断(包括 tasklet、timer、NET_RX 等)
preempt_disable(); // 关闭内核抢占
while (!trylock(&lock))
;
保护范围:屏蔽本地 CPU 上的进程上下文和软中断上下文,但不关闭硬中断。
5.4.对比总结
| 变体 | 关闭抢占 | 关闭硬中断 | 关闭软中断 | 保护范围 | 开销 |
|---|---|---|---|---|---|
spin_lock |
✅ | ❌ | ❌ | 进程 ↔ 进程 | 最小 |
spin_lock_bh |
✅ | ❌ | ✅ | 进程 ↔ 软中断 | 中等 |
spin_lock_irqsave |
✅ | ✅ | ✅ | 进程 ↔ 中断/一切 | 最大 |
选择决策树
c
你在进程上下文持锁?
│
├── 中断处理程序会抢这把锁? ──→ spin_lock_irqsave
│
├── 软中断/tasklet 会抢这把锁? ──→ spin_lock_bh
│
└── 只有进程上下文竞争? ──→ spin_lock
内核源码中的实际模式:
c
// Linux 内核中典型的"分层"锁使用(以网络子系统为例)
// 1. 硬中断(最内层,已关中断)
irqreturn_t rx_irq(int irq, void *dev_id) {
spin_lock(&queue->lock); // ✅ 中断里只用基础 spin_lock
// 收包入队
spin_unlock(&queue->lock);
raise_softirq(NET_RX_SOFTIRQ); // 触发软中断
}
// 2. 软中断(关软中断,但硬中断可发生)
void net_rx_action(struct softirq_action *h) {
spin_lock(&queue->lock); // ✅ 软中断里也用基础 spin_lock
// 协议栈处理
spin_unlock(&queue->lock);
}
// 3. 进程上下文(可能和中断/软中断竞争)
void dev_queue_xmit(struct sk_buff *skb) {
spin_lock_bh(&queue->lock); // ✅ 防软中断抢锁
// 发包处理
spin_unlock_bh(&queue->lock);
}
// 4. 通用路径(不确定上下文)
void some_generic_fn(void) {
unsigned long flags;
spin_lock_irqsave(&queue->lock, flags); // ✅ 最安全,但开销最大
// ...
spin_unlock_irqrestore(&queue->lock, flags);
}
6.互斥锁 (Mutex)
机制:睡眠锁,无法获取时当前任务进入睡眠状态,让出 CPU。
c
struct mutex mtx;
mutex_init(&mtx);
mutex_lock(&mtx); // 不可中断睡眠
mutex_lock_interruptible(&mtx); // 可中断(返回 -EINTR)
mutex_trylock(&mtx); // 非阻塞尝试
// 临界区
mutex_unlock(&mtx);
与自旋锁对比:
| 特性 | 自旋锁 (Spinlock) | 互斥锁 (Mutex) |
|---|---|---|
| 等待方式 | 忙等待(消耗 CPU) | 睡眠(让出 CPU) |
| 临界区长度 | 极短(几行代码) | 较长(可睡眠) |
| 可重入 | 不可重入 | 不可重入 |
| 上下文 | 中断上下文可用 | 仅进程上下文 |
| 开销 | 获取快(无上下文切换) | 获取慢(可能切换上下文) |
7.信号量 (Semaphore)
机制:计数锁,允许指定数量的持有者同时进入临界区。
c
struct semaphore sem;
sema_init(&sem, 1); // 二值信号量(类似互斥锁)
sema_init(&sem, 5); // 计数信号量(最多 5 个并发)
down(&sem); // P 操作:计数减 1,若为负则睡眠
down_interruptible(&sem);
up(&sem); // V 操作:计数加 1,唤醒等待者
现代内核中的定位:
- 二值信号量基本被 Mutex 取代(Mutex 有 owner 追踪,调试更友好)
- 计数信号量用于资源池限制(如同时打开的 socket 数量限制)
8.读拷贝更新 (RCU - Read-Copy-Update)
核心思想:读端零开销(无需锁、无需原子操作),写端通过拷贝+替换+延迟回收实现。
c
// ========== 读端 ==========
rcu_read_lock(); // 轻量级,仅关抢占(或记录 QS 状态)
struct my_struct *p = rcu_dereference(global_ptr); // 解引用,含内存屏障
// 读取 p 的数据(禁止写操作!)
rcu_read_unlock();
// ========== 写端 ==========
struct my_struct *new_p = kmalloc(...);
*new_p = *old_p; // 复制旧数据
new_p->field = new_value; // 修改副本
rcu_assign_pointer(global_ptr, new_p); // 原子替换指针,含写屏障
// 延迟释放旧数据(等待所有读端完成)
synchronize_rcu(); // 阻塞,直到所有已存在的读端结束
kfree(old_p);
// 或使用回调异步释放
call_rcu(&old_p->rcu_head, my_free_func);
宽限期 (Grace Period):
- 所有 CPU 都至少经历一次上下文切换(或静止状态 quiescent state)
- 在此之后,旧的读端引用已不存在,可以安全释放
适用场景:
- 读多写极少的链表、哈希表、路由表
- 内核中 dentry_cache、路由表、进程链表 大量使用 RCU
9.性能优化策略
9.1.锁的选择层次
cpp
读多写极少 → RCU(读端零开销)
读多写少 → 读写锁 / Seqlock(顺序锁,写优先)
读写均衡 → 自旋锁(短临界区)/ 互斥锁(长临界区)
计数限制 → 信号量 / 每 CPU 计数器聚合
9.2.关键优化原则
| 策略 | 说明 |
|---|---|
| 缩小临界区 | 只保护必要的数据访问,I/O、复杂计算移到锁外 |
| 降低锁粒度 | 全局锁 → 分片锁(per-bucket、per-cpu、per-node) |
| 避免锁 | 用每 CPU 变量、RCU、无锁算法(如 llist)替代 |
| 锁顺序 | 多锁时必须全局统一获取顺序,防止死锁 |
| 缓存对齐 | 锁本身和数据结构按缓存行对齐,避免 false sharing |
| 自适应自旋 | mutex 在短等待时先自旋(CONFIG_MUTEX_SPIN_ON_OWNER),避免上下文切换开销 |
9.3.内核中的无锁数据结构
- llist:基于 CAS 的单链表(用于工作队列)
- percpu_ref:引用计数 + RCU 延迟释放
- seqlock:写端无阻塞,读端通过序列号检测冲突并重试(用于 jiffies、xtime)
10.总结速查
| 机制 | 等待行为 | 能否睡眠 | 中断上下文 | 典型用途 |
|---|---|---|---|---|
| 每 CPU 变量 | 无 | 否 | 可 | 统计计数器 |
| 原子变量 | 无 | 否 | 可 | 引用计数、标志位 |
| 自旋锁 | 忙等 | 否 | 可(irqsave 版本) | 极短临界区 |
| 互斥锁 | 睡眠 | 可 | 否 | 一般同步 |
| 信号量 | 睡眠 | 可 | 否 | 资源池限制 |
| RCU | 无(读端) | 否(读端) | 可(读端) | 读多写少的数据结构 |