Linux 内核是典型的多核并发系统:同一个数据结构可能同时被硬中断、软中断、系统调用路径与工作队列线程访问。为保护共享状态,内核提供了一整套从最底层原子操作到高层 RCU 的并发原语。选错原语不会立刻崩溃,而是在高负载、特定 CPU 架构或极端时序下以数据损坏、死锁、优先级反转的形式爆发。
本文按「由底层到高层」的顺序展开:先讲原子操作与内存屏障的语义差异,再分析自旋锁、睡眠锁、读写锁与 seqlock 的适用边界,随后深入 per-CPU 变量与 RCU 的宽限期模型,最后给出 lockdep、KCSAN 等工具驱动的排查清单与完整示例。
一、并发原语全景与选型准则
1.1 并发的三种来源
内核必须同时应对三类并发:SMP 并行(多个核心真正同时执行内核代码)、抢占并发(CONFIG_PREEMPT 下高优先级任务可抢占同核任务)、中断并发(硬中断在任意指令边界打断,软中断在 irq_exit 或 ksoftirqd 中执行)。三者的组合决定了原语的设计空间:有些锁必须能在中断上下文使用(自旋锁),有些允许睡眠(mutex),有些干脆不阻塞读者(RCU)。
1.2 原语选型速查表
| 原语 | 是否睡眠 | 中断上下文可用 | 读者扩展性 | 典型场景 |
|---|---|---|---|---|
atomic_t | 否 | 是 | 极高 | 计数器、标志位 |
spinlock_t | 否 | 是 | 低 | 短临界区、中断共享数据 |
mutex | 是 | 否 | 低 | 长临界区、进程上下文 |
rt_mutex | 是 | 否 | 低 | 需要优先级继承的场景 |
rw_semaphore | 是 | 否 | 中 | 读多写少且读侧耗时长 |
seqlock_t | 否 | 写侧受限 | 极高 | 时间戳、统计快照 |
| per-CPU | 否 | 是 | 极高 | 无跨核共享的计数 |
| RCU | 否 | 是 | 极高 | 读多写极少、指针替换 |
选型核心是三连问:临界区会不会睡眠?会不会被中断打断?读写比例是多少?
1.3 上下文判定
内核用 in_atomic()、in_interrupt()、preempt_count() 判断当前上下文。只要临界区里出现 kmalloc(GFP_KERNEL)、copy_to_user、mutex_lock 等可能睡眠的调用,就必须改用睡眠锁并确保自己处于进程上下文:
if (in_atomic() || irqs_disabled()) {
/* 只能使用自旋锁,不能调用可能睡眠的函数 */
}
二、原子操作与内存屏障
2.1 atomic_t 与 atomic64_t
原子操作保证单个整数的读改写不可分割,无需任何锁:
#include <linux/atomic.h>
static atomic_t refcount = ATOMIC_INIT(0);
atomic_inc(&refcount);
atomic_dec_and_test(&refcount); /* 为 0 说明对象可释放 */
atomic_cmpxchg(&refcount, 0, 1); /* CAS */
atomic_t 只保证单个变量的原子性,不提供跨变量顺序,也不含内存屏障语义。atomic64_t 在 64 位平台是单条指令,在 32 位平台(如 i386)依赖 cmpxchg8b 或锁总线,开销明显更高。
2.2 READ_ONCE 与 WRITE_ONCE
这两个宏解决两件事:编译器优化导致的重读或合并写,以及并发访问的未定义行为。
static struct config cfg = { .mode = 0, .timeout = 100 };
int mode = READ_ONCE(cfg.mode); /* 防止读取被拆分或合并 */
WRITE_ONCE(cfg.mode, 1); /* 防止写入被撕裂 */
若不用 READ_ONCE,编译器可能把循环中的 if (flag) 提升到寄存器造成死循环,也可能把一次 64 位写拆成两条 32 位写,让读者看到半个新值。注意:它们只保证单次访问不被撕裂,不提供跨 CPU 的顺序保证。
2.3 内存屏障语义
| 屏障 | 语义 |
|---|---|
smp_mb() | 全屏障,之前的读写不得重排到之后 |
smp_rmb() | 只约束读操作的顺序 |
smp_wmb() | 只约束写操作的顺序 |
smp_load_acquire() | 获取语义,后续访问不能上移 |
smp_store_release() | 释放语义,之前的访问不能下移 |
barrier() | 纯编译屏障,不生成 CPU 指令 |
data = 42; /* 生产者 */
smp_wmb(); /* data 先于 flag 可见 */
WRITE_ONCE(flag, 1);
if (READ_ONCE(flag)) { /* 消费者 */
smp_rmb(); /* 读到 flag 后再读 data */
printk("data=%d\n", data);
}
等价写法是 smp_store_release(&flag, 1) 与 smp_load_acquire(&flag),它们分别隐含释放与获取屏障,在热路径上比全屏障更轻。另一个常被忽略的 barrier() 是纯编译屏障,不生成任何 CPU 指令,只阻止编译器重排,cpu_relax() 内部即包含它。
2.4 x86 与 ARM64 的语义差异
这是最容易踩坑的地方:x86 是强内存模型(TSO),ARM64 是弱内存模型。
| 重排类型 | x86 | ARM64 |
|---|---|---|
| Load-Load | 不允许 | 允许 |
| Store-Store | 不允许 | 允许 |
| Load-Store | 允许 | 允许 |
| Store-Load | 不允许 | 允许 |
因此 smp_wmb() 在 x86 上可能编译为空操作,同样的代码在 ARM64 上必须生成 dmb ishst。只在 x86 上验证过的并发代码,在 ARM 服务器上极大概率出问题。 热路径应优先使用更弱的 smp_load_acquire/smp_store_release,它们分别映射为 ARM64 的 ldar/stlr,而 x86 天然满足其语义。
三、自旋锁家族
3.1 spin_lock 变体与选择
| 接口 | 关抢占 | 关中断 | 关软中断 | 适用场景 |
|---|---|---|---|---|
spin_lock() | 是 | 否 | 否 | 确定无中断上下文竞争 |
spin_lock_bh() | 是 | 否 | 是 | 进程上下文与软中断共享数据 |
spin_lock_irq() | 是 | 是 | 是 | 与硬中断共享,且已知中断开启 |
spin_lock_irqsave() | 是 | 是 | 是 | 上下文不确定时的通用选择 |
static DEFINE_SPINLOCK(stat_lock);
static unsigned long packets;
unsigned long flags;
spin_lock_irqsave(&stat_lock, flags); /* 通用选择:保存并恢复中断状态 */
packets++;
spin_unlock_irqrestore(&stat_lock, flags);
关键点:spin_lock_irqsave 保存并恢复中断状态,因此可嵌套;而 spin_lock_irq 无条件开中断,在已关中断的上下文中使用会破坏状态。
3.2 queued spinlock
传统 ticket spinlock 在高竞争时会让所有等待者在一个缓存行上自旋(cache line bouncing)。Linux 4.2 引入 queued spinlock(即 queued_spinlock / qspinlock):无竞争时退化为一次原子 cmpxchg,性能接近无锁;有竞争时形成 MCS 队列,等待者各自在本地缓存行上自旋,显著降低 NUMA 系统的一致性流量。CONFIG_QUEUED_SPINLOCKS 默认开启。
3.3 raw_spinlock 与 RT 内核
raw_spinlock_t 是真正的自旋锁,即使在 PREEMPT_RT 内核中也不会被转换成 mutex。
| 类型 | 普通内核 | PREEMPT_RT 内核 |
|---|---|---|
spinlock_t | 自旋 | 转为 rt_mutex,可睡眠 |
raw_spinlock_t | 自旋 | 仍为自旋 |
因此凡必须在原子上下文工作的代码(中断控制器、调度器核心、定时器底层)都必须使用 raw_spinlock_t。
3.4 CONFIG_DEBUG_SPINLOCK
打开后内核会检测:对未初始化的锁加锁、重复加锁、释放不属于自己的锁。
用 grep -E "CONFIG_DEBUG_SPINLOCK|CONFIG_DEBUG_LOCK_ALLOC" /boot/config-$(uname -r) 可确认开关状态。配合 CONFIG_DEBUG_LOCK_ALLOC(lockdep 的基础)可获得更完整的报告。这些选项会增大 spinlock_t 结构体并降低性能,仅用于调试内核。
四、睡眠锁
4.1 mutex
mutex 是最常用的睡眠锁,遵循严格的「谁加锁谁解锁」语义:
static DEFINE_MUTEX(dev_lock);
if (mutex_lock_interruptible(&dev_lock))
return -ERESTARTSYS; /* 被信号打断 */
do_ioctl(f, cmd, arg);
mutex_unlock(&dev_lock);
约束:持有者与释放者必须是同一 task;不支持递归加锁;不能在中断上下文使用;提供 mutex_trylock() 与 mutex_lock_interruptible() 变体。无竞争时走一条快速的 cmpxchg 路径,有竞争时先 optimistic spinning 再挂入等待队列睡眠。
4.2 rtmutex 与优先级继承
普通 mutex 存在优先级反转:低优先级任务持锁,高优先级任务阻塞等待,中等优先级任务抢占低优先级任务,导致高优先级任务被无限拖延。rt_mutex 通过**优先级继承(Priority Inheritance)**解决:高优先级任务阻塞在 rt_mutex 上时,持有者临时继承其优先级。
#include <linux/rtmutex.h>
static DEFINE_RT_MUTEX(rt_lock);
rt_mutex_lock(&rt_lock); /* 期间持有者被提升到当前任务的优先级 */
rt_mutex_unlock(&rt_lock);
在 PREEMPT_RT 内核中,所有 mutex、spinlock、rw_semaphore 底层都由 rtmutex 支撑。优先级继承只能防反转、不能防死锁。
4.3 rwsem
rw_semaphore 允许多读者并发、单写者独占:
static DECLARE_RWSEM(cfg_sem);
down_read(&cfg_sem); /* 读侧,可并发 */
val = cfg.value;
up_read(&cfg_sem);
down_write(&cfg_sem); /* 写侧,独占 */
cfg.value = new_val;
up_write(&cfg_sem);
内核实现维护 count(读者计数与写者标志)与 owner(写者任务指针)。特性:写者优先(新读者在写者等待时被阻塞,避免写者饥饿);提供 down_read_trylock()/down_write_trylock();读侧同样不允许在中断上下文使用。读锁只保证没有写者,不保证读者之间互斥。
4.4 ww_mutex
ww_mutex(wound/wait mutex)解决多把锁的获取顺序问题,典型场景是 GPU 驱动同时锁定多个缓冲区对象。它为每次加锁分配一个 ww_acquire_ctx,当检测到环形等待(ABBA)时,让较新的事务主动放弃已持有的锁并重试,从而打破死锁。
static DEFINE_WW_CLASS(buf_ww_class);
static DEFINE_WW_MUTEX(buf_a, &buf_ww_class);
static DEFINE_WW_MUTEX(buf_b, &buf_ww_class);
ww_acquire_init(ctx, &buf_ww_class);
retry:
if (ww_mutex_lock(&buf_a, ctx) || ww_mutex_lock(&buf_b, ctx)) {
ww_mutex_unlock(&buf_a); /* 被 wound:放弃已持锁并重试 */
goto retry;
}
ww_acquire_done(ctx);
ww_acquire_fini(ctx);
协议要求:整个事务必须在持有同一个 ww_acquire_ctx 期间完成全部加锁。
五、读写锁与 seqlock
5.1 rwlock_t 的缺陷
rwlock_t 是早期的自旋读写锁,已被广泛认知的缺陷包括:
- 读者饥饿写者:读者源源不断时写者可能长期得不到锁
- 缓存行抖动:读者也要写内部计数器,缓存行在核间来回传递
- 公平性差:无排队机制,无法保证 FIFO
因此新代码不应使用 rwlock_t。替代方案:读侧短且不睡眠 → seqlock 或 RCU;读侧可能睡眠 → rw_semaphore;只有单一读者 → 直接用 spinlock。
5.2 seqlock_t
seqlock 的核心思想是让读者无锁读取,读后校验序号是否变化:
static seqlock_t stats_seqlock = __SEQLOCK_UNLOCKED(stats_seqlock);
static struct stats snapshot;
write_seqlock(&stats_seqlock); /* 写侧:必须串行化 */
snapshot.a = a;
snapshot.b = b;
write_sequnlock(&stats_seqlock);
void read_stats(struct stats *out) /* 读侧:无锁重试 */
{
unsigned int seq;
do {
seq = read_seqbegin(&stats_seqlock);
out->a = snapshot.a;
out->b = snapshot.b;
} while (read_seqretry(&stats_seqlock, seq));
}
write_seqlock 内部是 spin_lock,写侧不能在中断上下文睡眠;read_seqbegin 不关中断,读侧可在中断上下文使用。
5.3 seqcount 的使用场景
seqcount_t 是 seqlock 的底层原语,不带锁,写侧需调用者自己保证串行化:
static seqcount_t time_seq = SEQCNT_ZERO(time_seq);
static u64 last_time;
write_seqcount_begin(&time_seq); /* 写侧由外部锁保护 */
last_time = t;
write_seqcount_end(&time_seq);
u64 get_time(void)
{
unsigned int seq;
u64 t;
do {
seq = read_seqcount_begin(&time_seq);
t = last_time;
} while (read_seqcount_retry(&time_seq, seq));
return t;
}
典型应用包括 jiffies_64 读取(32 位平台)、gettimeofday 快速路径、网络栈统计快照。重要限制:读者在重试循环中不能有副作用,否则重试会造成重复副作用。
六、per-CPU 变量
6.1 DEFINE_PER_CPU 与访问
per-CPU 变量为每个 CPU 分配独立副本,从根本上消除跨核竞争:
#include <linux/percpu.h>
static DEFINE_PER_CPU(unsigned long, pkt_count);
this_cpu_inc(pkt_count); /* 访问当前 CPU 副本,无需加锁 */
for_each_possible_cpu(cpu) /* 汇总时遍历所有 CPU 副本 */
sum += per_cpu(pkt_count, cpu);
| 接口 | 说明 | 抢占影响 |
|---|---|---|
this_cpu_inc() | 原子操作当前 CPU 副本 | 隐含关抢占 |
per_cpu(var, cpu) | 访问指定 CPU 副本 | 需自行保证安全 |
get_cpu_var() / put_cpu_var() | 关抢占后取指针 | 手动控制 |
raw_cpu_inc() | 不隐含关抢占,最快 | 可被抢占 |
this_cpu_* 在 x86 上编译为带 %gs: 前缀的单条指令,是内核中最快的计数方式。
6.2 get_cpu_var 与 put_cpu_var
需要在多次访问之间保持在同一 CPU 上时,必须使用 get_cpu_var:
struct local_ctx *ctx = get_cpu_var(my_ctx); /* 关抢占,取当前 CPU 副本 */
ctx->count++;
do_something(ctx);
put_cpu_var(my_ctx); /* 开抢占 */
必须成对使用:漏掉 put_cpu_var 会让抢占被永久关闭,系统表现为单核运行甚至 RCU stall。
6.3 percpu_ref
percpu_ref 是引用计数的混合实现:快速路径用 per-CPU 计数(无原子操作),慢速路径切换到原子计数,从而支持「等待所有引用释放」的语义。
static struct percpu_ref ref;
percpu_ref_init(&ref, release_fn, 0, GFP_KERNEL);
percpu_ref_get(&ref);
percpu_ref_put(&ref);
percpu_ref_kill(&ref); /* 切换为原子模式并等待引用消失 */
典型使用者是块设备层的 request_queue 与 blk-mq,它允许设备热插拔时安全排空在途 I/O。
七、RCU 机制
7.1 宽限期模型
RCU(Read-Copy Update)的核心洞察是:读者不阻塞写者,写者也不阻塞读者。写者分两步:
- 发布新版本:修改数据副本并用指针替换,使新读者看到新数据
- 等待宽限期(grace period):等待所有已存在的读者退出临界区,然后释放旧数据
CPU0(写者): [发布新指针]------[宽限期]------[释放旧数据]
CPU1(读者): [rcu_read_lock ... 使用旧数据 ... unlock]
CPU2(读者): [rcu_read_lock ... unlock]
↑ 发布前进入的读者全部退出后,宽限期结束
只要读者不在临界区内睡眠、不切换到用户态,宽限期就一定能结束。
7.2 读侧与写侧 API
rcu_read_lock(); /* 读侧:极轻量,非 RT 内核仅关闭/恢复抢占 */
p = rcu_dereference(gp);
use(p);
rcu_read_unlock();
synchronize_rcu(); /* 写侧:同步等待,会阻塞 */
call_rcu(&old->rcu_head, free_callback); /* 写侧:异步回调,不阻塞 */
| 接口 | 是否阻塞 | 使用场景 |
|---|---|---|
synchronize_rcu() | 是 | 可睡眠的进程上下文 |
call_rcu() | 否 | 中断上下文、不能睡眠 |
rcu_barrier() | 是 | 等待已排队的 callback 执行完 |
synchronize_rcu_expedited() | 是 | 发 IPI 加速宽限期,有性能代价 |
7.3 发布-订阅模式
RCU 的指针访问必须使用专用宏,它们同时承担内存屏障职责:
struct config *global_cfg;
void update_config(struct config *new_cfg) /* 写者 */
{
struct config *old = rcu_dereference_protected(global_cfg, 1);
rcu_assign_pointer(global_cfg, new_cfg); /* 隐含释放屏障 */
synchronize_rcu();
kfree(old);
}
int read_mode(void) /* 读者 */
{
struct config *cfg;
rcu_read_lock();
cfg = rcu_dereference(global_cfg); /* 隐含获取屏障 */
rcu_read_unlock();
return cfg->mode;
}
关键纪律:rcu_dereference() 必须在 rcu_read_lock() 内使用;rcu_assign_pointer() 保证「数据初始化」先于「指针发布」对读者可见;读者持指针期间对象保证不被释放,但可能已被更新。
7.4 链表 RCU 操作
#include <linux/rculist.h>
static LIST_HEAD(dev_list);
static DEFINE_SPINLOCK(dev_list_lock);
void walk_devices(void) /* 读者:无锁遍历 */
{
struct device *d;
rcu_read_lock();
list_for_each_entry_rcu(d, &dev_list, list)
printk("%s\n", d->name);
rcu_read_unlock();
}
void del_device(struct device *d) /* 写者:加锁保护 */
{
spin_lock(&dev_list_lock);
list_del_rcu(&d->list);
spin_unlock(&dev_list_lock);
synchronize_rcu(); /* 等读者退出后再释放 */
kfree(d);
}
插入用 list_add_rcu(),删除用 list_del_rcu():后者只把前驱的 next 指向后继,不修改被删节点自身的 next,因此正在遍历该节点的读者不会崩溃。
7.5 SRCU 与 rcu_barrier
**SRCU(Sleepable RCU)**允许读者在临界区内睡眠,代价是每次使用需要独立的 srcu_struct 且读侧开销更高:
static struct srcu_struct my_srcu;
int idx = srcu_read_lock(&my_srcu);
use(rcu_dereference(gp)); /* 可睡眠 */
srcu_read_unlock(&my_srcu, idx);
synchronize_srcu(&my_srcu); /* 写侧 */
SRCU 常用于文件系统(如 notify_change 路径)、KVM 等需要在保护区内执行阻塞操作的地方。rcu_barrier() 用于模块卸载:等待所有已排队的 call_rcu 回调执行完毕,防止模块代码在卸载后被回调引用。
7.6 RCU 观测与调优
观测入口是 /sys/kernel/debug/rcu/rcu_pending(状态)、/sys/kernel/debug/rcu/rcugp(宽限期统计)与 /proc/meminfo 中的 callback 计数。
| 现象 | 可能原因 | 对策 |
|---|---|---|
| RCU stall 告警 | 读者在临界区睡眠或死循环 | 检查 rcu_read_lock 配对与循环退出条件 |
synchronize_rcu 延迟高 | 有 CPU 长时间 idle 或关中断 | 使用 synchronize_rcu_expedited() |
| callback 积压 | 写者频率过高 | 改用 kfree_rcu() 或批量延迟释放 |
| 软中断延迟抖动 | callback 在软中断批量执行 | 调整 rcu_nocbs 内核参数 |
八、lockdep 与并发 bug 排查
8.1 lockdep 原理
lockdep 是内核的运行时死锁检测器,通过 CONFIG_PROVE_LOCKING 开启。它不检测单次执行,而是建立锁类依赖图:每次加锁时记录「锁 A 在锁 B 持有期间被获取」,若图中出现环则报告潜在死锁。
static DEFINE_SPINLOCK(lock_a);
static DEFINE_SPINLOCK(lock_b);
void path1(void) /* 只要出现过 A→B 与 B→A 两种顺序就会报警 */
{
spin_lock(&lock_a);
spin_lock(&lock_b);
spin_unlock(&lock_b);
spin_unlock(&lock_a);
}
其价值在于:能在死锁真正发生之前,通过一次偶然的加锁路径组合就发现问题。
8.2 /proc/lockdep_stats 解读
直接读取 /proc/lockdep_stats 即可,关键字段如下。
| 字段 | 含义 |
|---|---|
lock-classes | 已注册的锁类数量 |
direct dependencies | 已观测到的锁依赖边数量 |
dependency chains | 依赖链数量,异常增长说明锁层次变复杂 |
chain lookup misses | 依赖图查找未命中次数,过高说明锁种类过多 |
hardirq-safe locks | 可在硬中断中获取的锁数量 |
/proc/lockdep 会列出所有锁类及其使用计数,适合定位「哪把锁从未被使用」。
8.3 ABBA 死锁报告
典型 lockdep 报告会依次给出「谁在申请什么锁」「已持有什么锁」「反向依赖链」,例如 kworker/0:2 想拿 lock_b 且已持有 lock_a,而依赖链中已存在 lock_b → lock_a,说明两条路径加锁顺序相反。解读步骤:
- 谁在申请什么锁:
kworker/0:2想拿lock_b - 它已经持有什么:
lock_a - 反向依赖链:
lock_b → lock_a已被其他地方建立 - 结论:两条路径加锁顺序相反,构成潜在死锁
修复方式通常是统一加锁顺序,或在无法统一时使用 ww_mutex/mutex_trylock 打破环。
8.4 KCSAN 与 DEBUG_ATOMIC_SLEEP
**KCSAN(Kernel Concurrency Sanitizer)**通过 CONFIG_KCSAN 开启,使用编译器插桩检测数据竞争:
grep CONFIG_KCSAN /boot/config-$(uname -r)
echo on > /sys/kernel/debug/kcsan # 运行时开启
CONFIG_DEBUG_ATOMIC_SLEEP 检测原子上下文中的睡眠行为,打开后内核会在 might_sleep() 检查点直接打印调用栈。CONFIG_DEBUG_OBJECTS 则追踪定时器、工作队列、RCU 头等内核对象的生命周期,能捕获重复初始化与释放后使用:
spin_lock(&my_lock);
buf = kmalloc(1024, GFP_KERNEL); /* BUG: sleeping function called from invalid context */
spin_unlock(&my_lock);
8.5 并发 bug 排查清单
| 症状 | 首选工具 | 检查方向 |
|---|---|---|
| 系统随机卡死无输出 | lockdep / hung_task | 加锁顺序、长时间持锁 |
sleeping function called from invalid context | DEBUG_ATOMIC_SLEEP | 原子上下文中的 GFP_KERNEL 分配 |
scheduling while atomic | DEBUG_ATOMIC_SLEEP | preempt_count 不平衡 |
| 数据偶发损坏 | KCSAN | 无锁共享访问、缺少 READ_ONCE |
| 高优先级任务延迟大 | rt_mutex + PI | 优先级反转 |
RCU stall detected | RCU trace | 读者临界区睡眠或死循环 |
| 性能随核数下降 | perf + qspinlock 统计 | 缓存行抖动、锁竞争 |
排查流程建议:
- 打开
CONFIG_PROVE_LOCKING、CONFIG_DEBUG_ATOMIC_SLEEP、CONFIG_DEBUG_SPINLOCK重新编译调试内核 - 复现问题时优先保存完整 dmesg,lockdep 报告的依赖链是定位关键
- 对疑似数据竞争使用 KCSAN 配合
stress-ng加压 - 修复后回归测试必须覆盖 ARM64 与 x86 两种内存模型
相关阅读
- https://plumephp.com/os-synchronization/ —— 内核同步机制全景,原子操作与锁的入门与对比
- https://plumephp.com/os-futex-locks-internals/ —— 用户态 futex 与内核 mutex 的协作路径解析
- https://plumephp.com/os-multiprocessor-cache/ —— 多处理器缓存一致性,理解内存屏障的硬件前提
延伸阅读
- Linux Kernel Documentation:
Documentation/locking/spinlocks.rst - Linux Kernel Documentation:
Documentation/locking/lockdep-design.rst - Linux Kernel Documentation:
Documentation/RCU/(含whatisRCU.rst、rcu_dereference.rst、listRCU.rst) - Paul E. McKenney, “Is Parallel Programming Hard, And, If So, What Can You Do About It?”
- Paul E. McKenney & John D. Slingwine, “Read-Copy Update: Using Execution History to Solve Concurrency Problems”(PDPTA 1998)
- LWN.net: “What is RCU?” 系列(Part 1-3)
- LWN.net: “The RCU API, 2019 edition”
Documentation/memory-barriers.txt与 Linux 源码kernel/locking/、kernel/rcu/
/* 完整可运行示例:RCU 发布-订阅 + spinlock 保护写侧
* 保存为 rcu_demo.c,同目录放一个 Makefile 即可编译:
* obj-m := rcu_demo.o
* all:
* $(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
* 运行:make && sudo insmod rcu_demo.ko && sleep 3 && dmesg | grep rcu_demo
*/
#include <linux/module.h>
#include <linux/rcupdate.h>
#include <linux/spinlock.h>
#include <linux/slab.h>
#include <linux/timer.h>
MODULE_LICENSE("GPL");
struct demo_cfg { int mode; struct rcu_head rcu; };
static struct demo_cfg __rcu *global_cfg;
static DEFINE_SPINLOCK(cfg_lock);
static unsigned long updates;
static struct timer_list tick_timer;
static void cfg_free(struct rcu_head *head)
{
struct demo_cfg *old = container_of(head, struct demo_cfg, rcu);
pr_info("rcu_demo: freed mode=%d\n", old->mode);
kfree(old);
}
static void publish(int mode)
{
struct demo_cfg *new_cfg, *old;
unsigned long flags;
new_cfg = kzalloc(sizeof(*new_cfg), GFP_KERNEL);
if (!new_cfg)
return;
new_cfg->mode = mode;
spin_lock_irqsave(&cfg_lock, flags);
old = rcu_dereference_protected(global_cfg, lockdep_is_held(&cfg_lock));
rcu_assign_pointer(global_cfg, new_cfg);
updates++;
spin_unlock_irqrestore(&cfg_lock, flags);
if (old)
call_rcu(&old->rcu, cfg_free); /* 宽限期结束后异步释放旧数据 */
}
static void tick(struct timer_list *t)
{
struct demo_cfg *cfg;
publish((int)(jiffies / HZ) % 10);
rcu_read_lock(); /* 读侧与写侧完全无锁并发 */
cfg = rcu_dereference(global_cfg);
pr_info("rcu_demo: read mode=%d updates=%lu\n",
cfg ? cfg->mode : -1, updates);
rcu_read_unlock();
mod_timer(&tick_timer, jiffies + HZ);
}
static int __init demo_init(void)
{
publish(1);
timer_setup(&tick_timer, tick, 0);
mod_timer(&tick_timer, jiffies + HZ);
return 0;
}
static void __exit demo_exit(void)
{
struct demo_cfg *old;
del_timer_sync(&tick_timer);
spin_lock_irq(&cfg_lock);
old = rcu_dereference_protected(global_cfg, lockdep_is_held(&cfg_lock));
rcu_assign_pointer(global_cfg, NULL);
spin_unlock_irq(&cfg_lock);
rcu_barrier(); /* 等所有 call_rcu 回调执行完再释放模块资源 */
kfree(old);
pr_info("rcu_demo: unloaded after %lu updates\n", updates);
}
module_init(demo_init);
module_exit(demo_exit);
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。