内核并发原语与 RCU 机制

内核并发原语是驱动、网络栈与文件系统正确运行的底层保障。本文从原子操作与内存屏障的语义差异讲起,梳理 spinlock、mutex、rtmutex、rwsem、seqlock 的适用边界与死锁风险,深入剖析 per-CPU 变量与 RCU 宽限期模型,并给出 lockdep、KCSAN 等工具的实战排查清单。

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 是弱内存模型。

重排类型x86ARM64
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 是早期的自旋读写锁,已被广泛认知的缺陷包括:

  1. 读者饥饿写者:读者源源不断时写者可能长期得不到锁
  2. 缓存行抖动:读者也要写内部计数器,缓存行在核间来回传递
  3. 公平性差:无排队机制,无法保证 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)的核心洞察是:读者不阻塞写者,写者也不阻塞读者。写者分两步:

  1. 发布新版本:修改数据副本并用指针替换,使新读者看到新数据
  2. 等待宽限期(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,说明两条路径加锁顺序相反。解读步骤:

  1. 谁在申请什么锁:kworker/0:2 想拿 lock_b
  2. 它已经持有什么:lock_a
  3. 反向依赖链:lock_b → lock_a 已被其他地方建立
  4. 结论:两条路径加锁顺序相反,构成潜在死锁

修复方式通常是统一加锁顺序,或在无法统一时使用 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 contextDEBUG_ATOMIC_SLEEP原子上下文中的 GFP_KERNEL 分配
scheduling while atomicDEBUG_ATOMIC_SLEEPpreempt_count 不平衡
数据偶发损坏KCSAN无锁共享访问、缺少 READ_ONCE
高优先级任务延迟大rt_mutex + PI优先级反转
RCU stall detectedRCU trace读者临界区睡眠或死循环
性能随核数下降perf + qspinlock 统计缓存行抖动、锁竞争

排查流程建议:

  1. 打开 CONFIG_PROVE_LOCKING、CONFIG_DEBUG_ATOMIC_SLEEP、CONFIG_DEBUG_SPINLOCK 重新编译调试内核
  2. 复现问题时优先保存完整 dmesg,lockdep 报告的依赖链是定位关键
  3. 对疑似数据竞争使用 KCSAN 配合 stress-ng 加压
  4. 修复后回归测试必须覆盖 ARM64 与 x86 两种内存模型

相关阅读

  • https://plumephp.com/os-synchronization/ —— 内核同步机制全景,原子操作与锁的入门与对比
  • https://plumephp.com/os-futex-locks-internals/ —— 用户态 futex 与内核 mutex 的协作路径解析
  • https://plumephp.com/os-multiprocessor-cache/ —— 多处理器缓存一致性,理解内存屏障的硬件前提

延伸阅读

  1. Linux Kernel Documentation: Documentation/locking/spinlocks.rst
  2. Linux Kernel Documentation: Documentation/locking/lockdep-design.rst
  3. Linux Kernel Documentation: Documentation/RCU/(含 whatisRCU.rst、rcu_dereference.rst、listRCU.rst)
  4. Paul E. McKenney, “Is Parallel Programming Hard, And, If So, What Can You Do About It?”
  5. Paul E. McKenney & John D. Slingwine, “Read-Copy Update: Using Execution History to Solve Concurrency Problems”(PDPTA 1998)
  6. LWN.net: “What is RCU?” 系列(Part 1-3)
  7. LWN.net: “The RCU API, 2019 edition”
  8. 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);

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「os」更多文章

  1. ARM64 体系结构与内核实现
  2. 内核网络栈:sk_buff、NAPI 与 XDP
  3. eBPF 开发实战:CO-RE 与 libbpf