并发与同步 (Concurrency and Synchronization)


章节概述

本章是内核编程中最容易出错的领域。Linux 内核运行在高度并发的环境中:多个 CPU 核心同时执行内核代码、中断随时可能发生、内核线程在后台运行。没有正确的同步机制,内核数据会在一瞬间被破坏——而且这些 bug 极难复现和调试。

并发编程是 C 语言最棘手的挑战之一。C 语言本身不提供任何并发安全机制——编译器不知道哪些变量被多线程访问,不会警告数据竞争。内核开发者必须依赖一套精心设计的同步原语来保证正确性。

本章内容:

  • 内核中的并发源:SMP、抢占、中断、软中断
  • 原子操作:atomic_t、bit 原子操作、内存屏障
  • 自旋锁(Spinlock):实现原理、使用场景、局限性
  • 互斥锁(Mutex)与信号量:可睡眠的锁
  • RCU(Read-Copy-Update):Linux 同步的皇冠明珠
  • 内存屏障与顺序:CPU 乱序执行和编译优化
  • 死锁:原因、检测、避免
  • Rust 的并发安全:所有权系统如何解决这些挑战

前置要求


第一节:内核中的并发源


1.1 四种并发源

graph TD
    subgraph "内核并发的四个来源"
        SMP["SMP (多核)<br/>● CPU0 和 CPU1 同时执行<br/>● 可能访问同一数据结构<br/>● 需要锁或 per-CPU 变量"]
        PREEMPT["内核抢占<br/>● 当前进程可能被抢占<br/>● 另一进程在内核态运行<br/>● CONFIG_PREEMPT"]
        IRQ["硬件中断<br/>● ISR 可能与被打断的代码<br/>  共享数据<br/>● 必须禁用中断或使用<br/>  spin_lock_irqsave"]
        SOFTIRQ["软中断 / Tasklet<br/>● bottom half 延迟执行<br/>● 可能与进程上下文<br/>  或同类型 softirq 并发<br/>● 同类型不会并发(同CPU)"]
    end
    
    SMP -->|"锁/原子操作"| PROTECTION["同步保护"]
    PREEMPT -->|"preempt_disable"| PROTECTION
    IRQ -->|"local_irq_save"| PROTECTION
    SOFTIRQ -->|"local_bh_disable"| PROTECTION

1.2 并发问题示例

// ============================================
// 经典竞争条件: 两个 CPU 同时执行此代码
// ============================================
 
// 共享数据结构
struct shared_data {
    unsigned long counter;
    struct list_head pending_list;
    void *buffer;
};
static struct shared_data global_data;
 
// BUG! 两个 CPU 同时调用此函数
static void dangerous_increment(void)
{
    // CPU0: 读取 counter = 5
    // CPU1: 读取 counter = 5
    // CPU0: 计算 5 + 1 = 6, 写入 counter = 6
    // CPU1: 计算 5 + 1 = 6, 写入 counter = 6
    // 结果: counter = 6 (应该是 7!)
    global_data.counter++;
}
 
// BUG! 并发链表操作
static void dangerous_list_add(int data)
{
    struct node *n = kmalloc(sizeof(*n), GFP_ATOMIC);
    n->data = data;
    
    // 如果两个 CPU 同时执行 list_add,
    // 链表的 prev/next 指针可能被破坏!
    list_add(&n->list, &global_data.pending_list);
}
 
// BUG! 被中断打断
static void dangerous_isr_data(void)
{
    // 假设 ISR 会修改 buffer 内容
    // 如果此函数在访问 buffer 时被中断打断,
    // 且 ISR 释放了 buffer → UAF!
    if (global_data.buffer) {
        memcpy(local_buf, global_data.buffer, size);
        // ← 这里发生中断, ISR 中 kfree(global_data.buffer)!
    }
}

1.3 并发类型总结

并发场景何时发生保护手段
进程 vs 进程多核同时调用同一系统调用spin_lock / mutex
进程 vs 中断ISR 与被打断的进程代码spin_lock_irqsave
中断 vs 中断同 IRQ 在多核同时触发spin_lock
中断 vs softirqISR 修改了 softirq 读的数据local_bh_disable
进程 vs softirq进程上下文与软中断处理函数spin_lock_bh
// 练习 1: 识别并发问题
// 阅读以下代码,找出并发问题:
//
// static int device_count = 0;
//
// void register_device(struct device *dev) {
//     dev->id = device_count++;
//     list_add(&dev->list, &device_list);
//     device_count++;
// }
//
// void unregister_device(struct device *dev) {
//     if (device_count > 0) {
//         list_del(&dev->list);
//         device_count--;
//         kfree(dev);
//     }
// }
//
// irqreturn_t data_ready_isr(int irq, void *dev_id) {
//     struct device *dev = dev_id;
//     dev->data_available = 1;
//     if (dev->reader_pid != 0)
//         kill_pid(find_vpid(dev->reader_pid), SIGIO, 1);
//     return IRQ_HANDLED;
// }
//
// 问题:
// 1. device_count++ 在 SMP 上安全吗?
// 2. list_add/list_del 有保护吗?
// 3. 如果 ISR 发生在 register_device 的 list_add 之前, 会怎样?
// 4. 如果 unregister_device 在中断上下文中被调用,kfree 安全吗?

第二节:原子操作


2.1 atomic_t —— 原子整数

// ============================================
// atomic_t: 内核原子变量
// 定义在 include/linux/atomic.h (或 include/asm-generic/atomic.h)
// ============================================
 
#include <linux/atomic.h>
 
// 声明和初始化
atomic_t my_counter = ATOMIC_INIT(0);  // 初始化为 0
 
// 基本操作 (所有这些操作都是原子的!)
atomic_set(&my_counter, 42);           // 设置值
int val = atomic_read(&my_counter);    // 读取值
 
// 原子加法
atomic_inc(&my_counter);               // my_counter += 1
atomic_dec(&my_counter);               // my_counter -= 1
atomic_add(5, &my_counter);            // my_counter += 5
atomic_sub(3, &my_counter);            // my_counter -= 3
 
// 返回旧值/新值的版本
int old = atomic_inc_return(&my_counter);  // 返回递增后的值
int old = atomic_fetch_inc(&my_counter);   // 返回递增前的值
int old = atomic_dec_return(&my_counter);  // 返回递减后的值
int new = atomic_add_return(10, &my_counter); // 返回加后的值
 
// compare-and-swap (CAS) —— 原子比较并交换
// 如果 *v == oldval, 则设置 *v = newval, 返回 true
// 否则不修改, 返回 false
if (atomic_cmpxchg(&my_counter, 42, 100) == 42) {
    // my_counter 原来是 42, 现在改为 100
} else {
    // my_counter 不是 42, 没有修改
    // atomic_cmpxchg 返回修改前的实际值
}
 
// 实现原理 (x86-64 使用 LOCK 前缀):
// atomic_inc 编译为:
//   lock incl (%rdi)   ← LOCK 前缀确保原子性
//
// atomic_add_return 编译为:
//   lock xadd %eax, (%rdi)  ← LOCK + XADD 指令
 
// 对比普通变量的非原子性:
// int counter = 0;
// counter++;  // 编译为: mov (%rdi),%eax; inc %eax; mov %eax,(%rdi)
//             // 三条指令 → CPU 可能在中间被打断!

2.2 原子位操作

// ============================================
// 原子位操作 —— 直接操作 unsigned long 的位
// 用于标志位、状态位等场景
// ============================================
 
#include <linux/bitops.h>
 
unsigned long flags = 0;
 
// 设置/清除/测试位
set_bit(3, &flags);        // flags |= (1 << 3) → 原子地
clear_bit(3, &flags);      // flags &= ~(1 << 3)
change_bit(5, &flags);     // flags ^= (1 << 5) → 翻转位
 
// 测试位
if (test_bit(3, &flags))
    printk("bit 3 is set\n");
 
// 原子地测试并设置 (test-and-set)
// 返回旧的位值, 并设置该位
if (!test_and_set_bit(LOCK_BIT, &flags)) {
    // 位之前是 0, 现在设为 1 —— 我们获得了"锁"
} else {
    // 位已经是 1 —— 别人持有"锁"
}
 
// 原子地测试并清除
if (test_and_clear_bit(DATA_READY_BIT, &flags)) {
    // 位之前是 1, 现在清除 —— 处理数据
    process_data();
}
 
// 查找第一个被设置/清除的位
int first_set = find_first_bit(&flags, BITS_PER_LONG);
// 这在 BITS_PER_LONG 位内查找
// 单寄存器的版本: ffs — 编译为 bsf 指令 (一条指令!)
int first_one = ffs(flags);  // 返回第一个 1 的索引 (1-based)
 
// 使用场景: 标志位数组
#define FLAG_DATA_READY    0
#define FLAG_IRQ_ENABLED   1
#define FLAG_DEVICE_BUSY   2
#define FLAG_BUFFER_FULL   3
 
set_bit(FLAG_IRQ_ENABLED, &flags);
if (test_bit(FLAG_DATA_READY, &flags)) { ... }
clear_bit(FLAG_DEVICE_BUSY, &flags);

2.3 原子操作 vs 锁

// ============================================
// 何时使用原子操作, 何时使用锁
// ============================================
 
// 使用原子操作:
//  单一变量的简单加减
//  设置/清除标志位
//  引用计数 (atomic_inc / atomic_dec_and_test)
//  性能最关键路径
 
// 使用锁 (spinlock / mutex):
//  保护多个相关变量 (临界区)
//  保护复杂数据操作 (如链表操作)
//  需要长时间持有的保护
//  需要与数据结构紧耦合的保护
 
// 示例: 使用原子操作的引用计数
struct my_object {
    atomic_t refcount;
    // ... 数据 ...
};
 
struct my_object *obj_get(struct my_object *obj)
{
    if (obj)
        atomic_inc(&obj->refcount);  // 原子地增加引用
    return obj;
}
 
void obj_put(struct my_object *obj)
{
    if (obj && atomic_dec_and_test(&obj->refcount)) {
        // refcount 降到 0 → 我们是最后一个使用者 → 安全释放
        kfree(obj);
    }
}
// atomic_dec_and_test: 原子地递减, 如果新值为 0 返回 true
// 这是 RCU 和 lock-free 编程的基石

第三节:自旋锁 (Spinlock)


3.1 自旋锁的本质

自旋锁是最基本的互斥机制。当锁不可用时,CPU 在一个紧密循环中反复检查(“自旋”),直到锁可用。

stateDiagram-v2
    [*] --> UNLOCKED: 初始化
    UNLOCKED --> LOCKED: spin_lock() 成功
    LOCKED --> UNLOCKED: spin_unlock()
    UNLOCKED --> SPINNING: spin_lock() 失败
    SPINNING --> LOCKED: 锁被释放 (自旋成功)
    SPINNING --> SPINNING: 继续自旋
    note right of SPINNING: CPU 在忙等待<br/>不能做其他事情<br/>"自旋"的名称来源

3.2 自旋锁的使用

// ============================================
// 自旋锁完整使用指南
// ============================================
 
#include <linux/spinlock.h>
 
// 定义和初始化
DEFINE_SPINLOCK(my_lock);        // 静态定义 + 初始化
// 或动态初始化:
// spinlock_t my_lock;
// spin_lock_init(&my_lock);
 
struct my_device {
    spinlock_t lock;             // 保护此设备数据结构的锁
    int counter;
    struct list_head queue;
    // ... 由此锁保护的所有字段
};
 
// =============================================
// 基础使用: spin_lock / spin_unlock
// =============================================
void process_request(struct my_device *dev, int data)
{
    unsigned long flags;
    
    // spin_lock: 获取自旋锁
    // 如果锁已经被持有, 忙等待直到获得
    spin_lock(&dev->lock);
    
    // === 临界区开始 ===
    // 此时保证独占访问 dev 的所有锁保护字段
    dev->counter++;
    list_add_tail(&dev->queue, ...);
    // === 临界区结束 ===
    
    spin_unlock(&dev->lock);
}
 
// =============================================
// 中断安全版本: spin_lock_irqsave
// =============================================
// 问题: 如果进程上下文持有自旋锁时发生中断?
// → ISR 也在同一 CPU 上执行, 也请求同一把锁 → 死锁!
// 因为 ISR 不会让出 CPU, 锁永远不会被释放
//
// 解决: 获取锁的同时禁用本地中断
 
void process_request_irq_safe(struct my_device *dev, int data)
{
    unsigned long flags;
    
    // spin_lock_irqsave: 获取锁 + 保存并禁用本地中断
    spin_lock_irqsave(&dev->lock, flags);
    
    // 临界区操作...
    dev->counter++;
    
    // spin_unlock_irqrestore: 释放锁 + 恢复之前的本地中断状态
    spin_unlock_irqrestore(&dev->lock, flags);
}
 
// =============================================
// Bottom half 安全版本: spin_lock_bh
// =============================================
// 禁止本地软中断 (softirq 和 tasklet), 但允许硬件中断
 
void process_bottom_half_safe(struct my_device *dev)
{
    spin_lock_bh(&dev->lock);
    // 软中断不会在这期间执行 (在当前 CPU 上)
    dev->counter++;
    spin_unlock_bh(&dev->lock);
}
 
// ============================================
// 选择指南:
//
// spin_lock()              进程 vs 进程 (SMP)
// spin_lock_irq()          进程 vs 中断
// spin_lock_irqsave()      进程 vs 中断 (推荐, 可嵌套)
// spin_lock_bh()           进程 vs bottom half
//
// 规则: 始终使用能保护你免受当前环境中最强并发源影响的变体
//       如需在中断处理中使用锁: 使用 _irqsave

3.3 自旋锁的实现原理

// ============================================
// 自旋锁的实现 (简化, x86-64)
// 实际代码在 include/asm-generic/qspinlock.h 和 kernel/locking/qspinlock.c
// ============================================
 
// 经典自旋锁实现 (ticket spinlock):
typedef struct {
    union {
        u32 val;
        struct {
            u16 owner;   // 当前持有者编号
            u16 next;    // 下一个可用的编号
        };
    };
} arch_spinlock_t;
 
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
    u32 val;
    
    // 1. 原子地获取 ticket (next) 并递增
    //    使用 fetch-and-add (xadd)
    asm volatile("lock xaddl %0, %1"
                 : "+r"(val), "+m"(lock->val)
                 :: "memory");
    // val 包含旧的 next 值 (我们的 ticket 号)
    
    // 2. 自旋等待 owner 变成我们的 ticket 号
    while (lock->owner != (u16)val) {
        // cpu_relax: PAUSE 指令 → 降低功耗 + 避免内存序冲突
        // 还提示 CPU 我们在自旋中, 让它可以用 MWAIT 等省电状态
        cpu_relax();
    }
    
    // 获得锁! 我们是这个 ticket 号的 owner
}
 
static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
    // 释放锁: 增加 owner (允许下一个 ticket 持有锁)
    asm volatile("incw %0" : "+m"(lock->owner) :: "memory");
}
 
// 现代内核使用 MCS lock / qspinlock (队列自旋锁)
// - ticket lock 在超过 8 核时性能下降 (缓存一致性协议开销)
// - qspinlock 使用 MCS 队列减少缓存乒乓效应
// 实现: kernel/locking/qspinlock.c

3.4 自旋锁的规则

// 自旋锁的关键规则:
//
// 1. 持有自旋锁时不能睡眠!
//    原因: 如果睡眠, 等待此锁的其他 CPU 也会自旋等待
//    直到调度器重新调度当前 CPU → 浪费 CPU 时间
//    可能死锁如果调度器也需要此锁
//
// 2. 持有自旋锁的时间应该尽可能短!
//    原因: CPU在自旋等待锁时不能做其他有用工作
//    长时间持有自旋锁会严重影响系统吞吐量
//
// 3. 持有自旋锁时调用可能睡眠的函数 → 内核会警告
//    (如果配置了 CONFIG_DEBUG_ATOMIC_SLEEP)
//    sleep while atomic bug!
//
// 4. 不要递归获取同一个自旋锁 (spinlock 不是递归锁)
//    会立即死锁!
//
// 5. 获取多个锁时, 始终以相同顺序获取
//    防止 AB-BA 死锁 (见第五节)
 
// 错误示例:
spin_lock(&lock_a);
// ... lots of work ...  ← 错误! 临界区太长
// kmalloc(..., GFP_KERNEL);  ← 错误! 可能睡眠
// msleep(100);               ← 错误! 睡眠
// schedule();                ← 错误! 让出 CPU
kfree(ptr);
spin_unlock(&lock_b);  // ← 错误! 锁不匹配!

第四节:互斥锁与信号量 —— 可睡眠的锁


4.1 mutex —— 内核互斥锁

// ============================================
// 互斥锁 (mutex): 允许持有者睡眠
// ============================================
 
#include <linux/mutex.h>
 
// 定义
DEFINE_MUTEX(my_mutex);
// 或动态初始化:
// struct mutex my_mutex;
// mutex_init(&my_mutex);
 
// 获取和释放 (进程上下文, 可以睡眠)
mutex_lock(&my_mutex);                        // 阻塞获取
int ret = mutex_lock_interruptible(&my_mutex); // 可被信号中断
int ret = mutex_trylock(&my_mutex);            // 非阻塞尝试
 
mutex_unlock(&my_mutex);
 
// 使用示例: 保护的临界区可以执行耗时操作
static ssize_t device_read(struct file *filp, char __user *buf,
                            size_t count, loff_t *ppos)
{
    struct my_device *dev = filp->private_data;
    
    // 使用 mutex (可以睡眠)
    if (mutex_lock_interruptible(&dev->mutex))
        return -ERESTARTSYS;  // 被信号中断
    
    // 临界区: 可以调用可能睡眠的函数
    while (buffer_empty(dev)) {
        mutex_unlock(&dev->mutex);
        // 等待数据到来... 睡眠在此
        if (wait_event_interruptible(dev->waitq, !buffer_empty(dev)))
            return -ERESTARTSYS;
        if (mutex_lock_interruptible(&dev->mutex))
            return -ERESTARTSYS;
    }
    
    // 可以调用 kmalloc(..., GFP_KERNEL) — 可能睡眠
    void *tmp = kmalloc(count, GFP_KERNEL);
    
    // 耗时操作...
    
    mutex_unlock(&dev->mutex);
    return ret;
}
 
// ============================================
// mutex vs spinlock 选择指南
// ============================================
// 
// 使用 spinlock 当:
//   - 临界区很短 (几微秒以内)
//   - 持有者在中断上下文 (不能睡眠!)
//   - 不能容忍上下文切换开销
//
// 使用 mutex 当:
//   - 临界区可能很长
//   - 可能需要在临界区内睡眠
//   - 只在进程上下文中使用
//   - 需要可中断的等待

4.2 信号量 (Semaphore)

// ============================================
// 信号量: 允许多个并发持有者
// ============================================
 
#include <linux/semaphore.h>
 
// 定义 (初始计数 = 同时允许的持有者)
DEFINE_SEMAPHORE(my_sem);  // 初始计数 = 1 (二元信号量 = 类似互斥锁)
 
// 动态定义
struct semaphore sem;
sema_init(&sem, 5);  // 初始计数 = 5, 最多 5 个线程同时进入
 
// 获取信号量 (P 操作, "尝试")
// down: 减少计数, 如果计数 <= 0 则睡眠
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_killable(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
int down_timeout(struct semaphore *sem, long jiffies);
 
// 释放信号量 (V 操作, "通知")
// up: 增加计数, 如果计数 <= 0 则唤醒等待者
void up(struct semaphore *sem);
 
// 使用场景:
// 1. 生产者-消费者: sem = 缓冲区空闲槽数
// 2. 限流: sem = 最大并发用户数
// 3. 资源池: sem = 可用资源数
 
// 信号量 vs 互斥锁:
//   - 信号量的计数可以 > 1, 允许多个持有者
//   - mutex 严格要求同一时间只有一个持有者
//   - mutex 有 owner 概念 (只有锁的持有者才能释放)
//     信号量的 up/down 可以不在同一线程

4.3 完成变量 (Completion)

// ============================================
// Completion: 轻量级的"事件通知"机制
// 用于等待某个事件完成
// ============================================
 
#include <linux/completion.h>
 
// 定义
DECLARE_COMPLETION(done);
// 或动态:
// struct completion done;
// init_completion(&done);
 
// 等待方: 睡眠直到事件完成
void wait_for_completion(struct completion *c);
unsigned long wait_for_completion_timeout(struct completion *c,
                                           unsigned long timeout);
int wait_for_completion_interruptible(struct completion *c);
 
// 通知方: 唤醒等待者
void complete(struct completion *c);              // 唤醒一个等待者
void complete_all(struct completion *c);          // 唤醒所有等待者
 
// 典型使用: 设备初始化/清理等待
static struct completion probe_done;
 
static int device_thread_fn(void *data)
{
    // 执行初始化...
    // ...
    // 初始化完成, 通知主线程
    complete(&probe_done);
    return 0;
}
 
static int device_probe(struct platform_device *pdev)
{
    init_completion(&probe_done);
    
    // 创建线程执行初始化
    kthread_run(device_thread_fn, NULL, "device_init");
    
    // 等待初始化完成 (可选超时)
    if (!wait_for_completion_timeout(&probe_done, 5 * HZ)) {
        dev_err(&pdev->dev, "Device init timeout\n");
        return -ETIMEDOUT;
    }
    
    return 0;
}

第五节:RCU —— Read-Copy-Update


5.1 RCU 的核心思想

graph TD
    subgraph "RCU (Read-Copy-Update) 基本流程"
        INIT["初始状态<br/>旧数据 A 被读者引用"]
        COPY["1. Writer 复制旧数据<br/>创建新版本 A'" ]
        MOD["2. Writer 修改副本<br/>旧数据不变, 读者仍可访问旧数据"]
        SWAP["3. Writer 原子地切换指针<br/>新读者看到 A', 旧读者仍持有 A 的引用"]
        WAIT["4. Writer 等待宽限期 (Grace Period)<br/>等待所有旧读者完成"]
        FREE["5. Writer 释放旧数据 A<br/>安全: 没有读者仍在访问 A"]
    end
    
    INIT --> COPY --> MOD --> SWAP --> WAIT --> FREE

RCU 是 Linux 内核中最精妙的同步机制——它允许读者(readers)无需任何锁开销就能安全地访问共享数据。writer 创建新数据副本,修改后原子地切换指针,然后等待所有现存的读者完成后再释放旧数据。

5.2 RCU API 与使用

// ============================================
// RCU 完整使用示例
// ============================================
 
#include <linux/rcupdate.h>
#include <linux/slab.h>
 
// 假设我们有一个配置数据结构
struct config_data {
    int timeout;
    int max_retries;
    char name[64];
    // ...
};
 
// 全局指针 —— 读者通过 RCU 访问
static struct config_data __rcu *global_config;
 
// ============================================
// Reader 端: 读取配置 (无锁!)
// ============================================
 
int get_timeout(void)
{
    struct config_data *cfg;
    int timeout;
    
    // 1. 进入 RCU 读临界区
    rcu_read_lock();
    
    // 2. 安全地读取 RCU 保护的指针
    //    rcu_dereference: 确保编译器不优化掉此读取
    //    产生必要的内存屏障
    cfg = rcu_dereference(global_config);
    
    // 3. 安全地访问数据
    //    在这一刻, 即使 writer 正在修改 global_config,
    //    cfg 仍然指向有效的旧数据
    if (cfg)
        timeout = cfg->timeout;
    else
        timeout = 30;  // default
    
    // 4. 离开 RCU 读临界区
    rcu_read_unlock();
    
    // 注意: 离开读临界区后, 不能再使用 cfg!
    // writer 可能已经释放了旧数据
    return timeout;
}
 
// 读者规则:
// 1. rcu_read_lock / rcu_read_unlock 之间
// 2. 不能睡眠
// 3. 不能阻塞
// 4. 读取操作极快 (无锁开销)
 
// ============================================
// Writer 端: 更新配置
// ============================================
 
int update_config(int new_timeout)
{
    struct config_data *old_cfg, *new_cfg;
    
    // 1. 分配新配置
    new_cfg = kmalloc(sizeof(*new_cfg), GFP_KERNEL);
    if (!new_cfg)
        return -ENOMEM;
    
    // 2. 复制旧配置到新配置
    rcu_read_lock();
    old_cfg = rcu_dereference(global_config);
    if (old_cfg)
        *new_cfg = *old_cfg;  // 复制所有字段
    else
        memset(new_cfg, 0, sizeof(*new_cfg));
    rcu_read_unlock();
    
    // 3. 修改新配置
    new_cfg->timeout = new_timeout;
    
    // 4. 原子地切换指针
    //    rcu_assign_pointer: 包含必要的内存屏障
    //    确保新数据对所有 CPU 可见
    rcu_assign_pointer(global_config, new_cfg);
    
    // 5. 等待宽限期 (所有旧读者完成)
    //    在此期间, old_cfg 仍然有效
    //    synchronize_rcu 会睡眠等待
    synchronize_rcu();
    
    // 6. 安全释放旧配置
    //    现在可以确定没有读者在访问 old_cfg
    kfree(old_cfg);
    
    return 0;
}
 
// Writer 也可以在回调中释放旧数据 (避免阻塞在 synchronize_rcu)
static void free_old_config(struct rcu_head *rcu)
{
    struct config_data *cfg = container_of(rcu, struct config_data, rcu);
    kfree(cfg);
}
 
int update_config_async(int new_timeout)
{
    struct config_data *old_cfg, *new_cfg;
    
    new_cfg = kmalloc(sizeof(*new_cfg), GFP_KERNEL);
    if (!new_cfg)
        return -ENOMEM;
    
    rcu_read_lock();
    old_cfg = rcu_dereference(global_config);
    if (old_cfg)
        *new_cfg = *old_cfg;
    rcu_read_unlock();
    
    new_cfg->timeout = new_timeout;
    rcu_assign_pointer(global_config, new_cfg);
    
    //  异步回调: 不等 synchronize_rcu
    // 当宽限期结束后, 自动调用 free_old_config
    if (old_cfg)
        call_rcu(&old_cfg->rcu, free_old_config);
    
    return 0;
}

5.3 RCU 链表操作

// ============================================
// RCU 结合链表 —— 最常用的模式
// ============================================
 
#include <linux/rculist.h>
 
struct my_entry {
    int key;
    int value;
    struct list_head list;
    struct rcu_head rcu;  // RCU 回调所需的字段
};
 
static LIST_HEAD(my_list);  // 链表头
static DEFINE_SPINLOCK(my_list_lock);  // writer 锁
 
// Reader: 遍历链表 (无锁!)
struct my_entry *lookup_entry(int key)
{
    struct my_entry *entry;
    
    rcu_read_lock();
    
    // list_for_each_entry_rcu: RCU 安全的链表遍历
    list_for_each_entry_rcu(entry, &my_list, list) {
        if (entry->key == key) {
            rcu_read_unlock();
            return entry;  // 调用者必须在使用期间持有 rcu_read_lock
        }
    }
    
    rcu_read_unlock();
    return NULL;  // 未找到
}
 
// Writer: 添加条目
int add_entry(int key, int value)
{
    struct my_entry *entry = kmalloc(sizeof(*entry), GFP_KERNEL);
    if (!entry) return -ENOMEM;
    
    entry->key = key;
    entry->value = value;
    
    spin_lock(&my_list_lock);
    list_add_tail_rcu(&entry->list, &my_list);
    spin_unlock(&my_list_lock);
    
    return 0;
}
 
// Writer: 删除条目 (需要 RCU 宽限期)
void del_entry(struct my_entry *entry)
{
    spin_lock(&my_list_lock);
    list_del_rcu(&entry->list);  // RCU 安全的链表删除
    spin_unlock(&my_list_lock);
    
    // 等待所有可能引用了 entry 的读者完成
    synchronize_rcu();
    
    // 安全释放
    kfree(entry);
}
 
// 异步删除版本:
static void free_entry_rcu(struct rcu_head *rcu)
{
    struct my_entry *entry = container_of(rcu, struct my_entry, rcu);
    kfree(entry);
}
 
void del_entry_async(struct my_entry *entry)
{
    spin_lock(&my_list_lock);
    list_del_rcu(&entry->list);
    spin_unlock(&my_list_lock);
    
    // 宽限期结束后自动回调 free_entry_rcu
    call_rcu(&entry->rcu, free_entry_rcu);
}

5.4 宽限期 (Grace Period) 详解

// ============================================
// RCU 宽限期是什么?
// ============================================
//
// 宽限期开始于 writer 执行 rcu_assign_pointer
// 结束于所有在宽限期开始前已进入 rcu_read_lock 的
// CPU 都离开了 rcu_read_unlock
//
// 换句话说:
//   宽限期 = 确保所有现有的读临界区都已结束
//
// 如何检测宽限期结束?
//   - rcu_read_lock / unlock 不做任何内存操作
//   - 但它们在禁止抢占 (禁用调度器)
//   - 所以不能进行上下文切换
//   - RCU 在每个 CPU 上跟踪 quiescent state
//     (quiescent state = 经历了上下文切换 or 进入 idle)
//   - 所有 CPU 都经历了一次 quiescent state → 宽限期结束
 
// synchronize_rcu() 内部做的事:
//   1. 注册一个等待宽限期结束的回调
//   2. 睡眠等待
//   3. 被 RCU 子系统在宽限期结束时唤醒
//
// 开销: synchronize_rcu() 可能花费数毫秒到数百毫秒
 
// RCU 的威力:
// - 读者开销: 几乎为零 (只在某些架构上有小内存屏障)
// - 读者不需要原子操作
// - 读者不需要锁
// - 读者可以非常高频地执行
// - 适用于读多写少的场景
//   (如路由表、系统调用表、VFS 缓存等)
 
// 练习 2: RCU 实验
// 1. 编写内核模块, 创建一个 RCU 保护的计数器
//    - 100 个 reader 线程, 每个读取 1000000 次
//    - 1 个 writer 线程, 每 100ms 更新一次指针
//    - 使用 ktime 测量 reader 读操作的平均开销
//
// 2. 对比: 用读-写自旋锁 (rwlock) 重复同样实验
//    计算两种方案的 reader throughput 差异
//
// 3. 使用 perf 测量:
//    sudo perf stat -e L1-dcache-loads,L1-dcache-misses
//    观察 RCU 方案是否产生更少的缓存缺失

第六节:内存屏障与顺序


6.1 重排序问题

// ============================================
// CPU 和编译器都可能重排序你的代码!
// ============================================
 
// 假设处理器 0 执行这个:
// x = 1;
// y = 1;
 
// 处理器 1 执行这个:
// while (y != 1) ;  // 自旋等待
// assert(x == 1);
 
// 问题: assert 可能失败吗?
// 答: 可能!
//
// 原因:
//   1. CPU 可能交换 x=1 和 y=1 的写入顺序
//      (因为它们是独立的内存地址, CPU 的存储缓冲区可以
//       先提交 y 再提交 x)
//   2. 编译器也可能在优化中交换它们的顺序
//
// 这就是为什么需要内存屏障 (Memory Barrier) 和
// 正确的锁使用来保证内存顺序

6.2 内存屏障类型

// ============================================
// Linux 内存屏障
// ============================================
 
// 编译器屏障: 只阻止编译器重排序, 不阻止 CPU
barrier();
 
// 全屏障: 阻止所有重排序 (编译器和 CPU)
mb();   // 全内存屏障 (mfence on x86, dmb sy on ARM)
rmb();  // 读屏障 (lfence on x86, dmb ld on ARM)
wmb();  // 写屏障 (sfence on x86, dmb st on ARM)
 
// 更轻量级的屏障:
smp_mb();   // 只在 SMP 系统上有效
smp_rmb();
smp_wmb();
 
// ============================================
// x86 的内存模型 (相对强的模型)
// ============================================
//
// x86 是 Total Store Order (TSO) 模型:
//   - 读-读 不重排序
//   - 读-写 不重排序
//   - 写-写 不重排序 (存储按 FIFO 顺序提交)
//   - 写-读 可能重排序! (存储缓冲区)
//     (后面的读可能提前到前面的写之前完成)
//
// 因此 x86 上:
//   smp_rmb() → 空操作 (不需要)
//   smp_wmb() → 空操作 (不需要)
//   smp_mb()  → mfence 或 lock addl $0, (%rsp)
 
// ============================================
// ARM64 的内存模型 (弱内存模型)
// ============================================
//
// ARM 是 Relaxed Memory Order:
//   几乎所有的重排序都可能发生
//   必须有显式的内存屏障指令
//   dmb ish (Inner SHareable domain barrier)
//   dmb ishld (读屏障)
//   dmb ishst (写屏障)
//
// 这就是为什么"可移植的锁实现"必须包含内存屏障
// — 因为不同架构的内存排序模型差异巨大!

6.3 内存屏障的实际使用

// ============================================
// 锁的实现中隐含内存屏障
// ============================================
 
// spin_lock() 隐含 acquire 语义:
//   获取锁后的所有内存操作不会被重排序到获取锁之前
//   相当于: 获取锁 + smp_mb() (获得所有之前的内存更新)
//
// spin_unlock() 隐含 release 语义:
//   释放锁前的所有内存操作不会被重排序到释放锁之后
//   相当于: smp_mb() (提交所有待处理的内存更新) + 释放锁
 
// 示例: 生产者-消费者 (不依赖锁时需要显式屏障)
volatile int data_ready = 0;
int shared_data;
 
// 生产者 (CPU0):
shared_data = 42;
smp_wmb();  //  确保 shared_data 的写入在 data_ready 之前
data_ready = 1;
 
// 消费者 (CPU1):
while (data_ready != 1)
    cpu_relax();  // 等待...
smp_rmb();  //  确保读取 data_ready 后能看到 shared_data 的写入
assert(shared_data == 42);  // 现在保证成功
 
// 注意: 使用自旋锁后不需要手动屏障!
// spin_lock 和 rcu_assign_pointer 内部自动包含必要的屏障
// 通常只在 lock-free 编程时需要手动内存屏障

第七节:死锁


// ============================================
// 死锁的经典场景
// ============================================
 
// 1. AB-BA 死锁 (最常见的多锁死锁)
//
// 线程A:                  线程B:
//   spin_lock(&lock_a);     spin_lock(&lock_b);
//   spin_lock(&lock_b);     spin_lock(&lock_a);  // 死锁!
//   ...
//   spin_unlock(&lock_b);   spin_unlock(&lock_a);
//   spin_unlock(&lock_a);   spin_unlock(&lock_b);
//
// 解决: 始终以相同顺序获取锁
 
// 2. 自死锁 (递归获取不可重入锁)
//
//   spin_lock(&lock);
//   // ...
//   spin_lock(&lock);  // 死锁! spinlock 不可重入
//
// 如果需要递归, 使用:
//   rwlock (read/write lock, 同一线程可多次获取读锁)
 
// 3. 中断上下文死锁
//
// 进程上下文:              中断上下文:
//   spin_lock(&lock);
//   ...          ← 1. 发生中断!
//                     spin_lock(&lock);  // 永远等待! (同 CPU)
//
// 解决: spin_lock_irqsave 在获取锁时禁用中断
 
// 4. 资源分配死锁
//
// 分配两个对象 A 和 B, 都需要从同一内存池分配
// 如果池只剩下一个对象 → 可能死锁
//
// 解决: 使用 GFP_ATOMIC 或在分配关键资源前预留
 
// ============================================
// 内核死锁检测工具: lockdep
// ============================================
//
// lockdep (Lock Dependency Validator):
//   跟踪所有锁的 获取顺序 和 中断状态
//   构建锁的依赖关系图
//   在运行时检测潜在的循环依赖 (死锁可能性)
//
// 启用: CONFIG_PROVE_LOCKING=y
// 查看报告: dmesg | grep "possible circular locking dependency"
 
// lockdep 检测的 AB-BA 死锁模板:
// 
// ======================================================
// WARNING: possible circular locking dependency detected
// ======================================================
// CPU0                    CPU1
// ----                    ----
// lock(A);                lock(B);
// lock(B);                lock(A);
//
// *** DEADLOCK ***
// 练习 3: 死锁实验
// 1. 编写内核模块, 故意制造 AB-BA 死锁:
//    - 创建 2 个 spinlock
//    - 创建 2 个内核线程: 线程A 先获取 lock1 再 lock2
//                         线程B 先获取 lock2 再 lock1
//    - 观察系统是否死锁
//
// 2. 启用 lockdep (CONFIG_PROVE_LOCKING), 
//    观察 lockdep 是否能在死锁发生前检测到潜在的循环依赖
//
// 3. 使用 lockdep 检查现有内核模块的锁使用:
//    - 加载你的设备驱动模块
//    - dmesg | grep lockdep
//    - 检查是否有锁顺序违规警告

第八节:Rust 并发安全对比


// ============================================
// C 语言的并发陷阱 (编译器不帮你检查)
// ============================================
 
// 陷阱 1: 忘记获取锁
// struct my_dev *dev = ...;
// dev->counter++;  // BUG! 没有获取 dev->lock
// C 编译器: 完全没问题, 顺利编译
// 运行时: 数据竞争 (可能但不必定崩溃)
 
// 陷阱 2: 数据在锁外被访问
struct my_dev *dev;
// (某处获取了锁, 修改了 dev->ptr)
// ...
spin_unlock(&dev->lock);
void *tmp = dev->ptr;  // BUG! 没在锁保护下读取
// C 编译器: 没问题
// 运行时: 可能读一半被修改 (torn read) 或读到已释放的内存
 
// 陷阱 3: 使用已释放的内存 (UAF)
spin_lock(&dev->lock);
void *buf = dev->buf;
spin_unlock(&dev->lock);
// 此时另一个线程可能已经 kfree(dev->buf)!
memcpy(dest, buf, len);  // BUG! 使用已释放内存
// C 编译器: 没问题
// 运行时: 可能正常, 可能崩溃, 可能被利用
 
// ============================================
// Rust 的解决方案 (编译时检查)
// ============================================
//
//
// Rust 的所有权系统保证:
// 1. 要访问 Mutex 保护的数据, 必须先获取锁
//    let data = dev.lock.lock().unwrap();
//    data.counter += 1;  // 安全: 编译器保证锁已获取
//    // data 在作用域结束时自动释放锁 (MutexGuard Drop)
//
// 2. 防止数据在锁外被访问
//    // 以下在 Rust 中是编译错误!
//    // let tmp = dev.lock.lock().unwrap().ptr;
//    // drop(dev.lock);  // 锁释放
//    // use(tmp);        // 编译错误: tmp 引用了被释放的锁保护数据
//
// 3. 所有权 + 生命周期防止 UAF
//    let buf = dev.lock.lock().unwrap().buf;
//    // 另一个线程不能释放 buf, 因为 buf 仍在借用
//
// 4. Send / Sync trait 保证线程安全
//    编译器自动检查类型是否可以在线程间传递
//
// 当然, unsafe Rust 中所有这些保证都可以被绕过
// 这就是为什么内核中 Rust 代码中的 unsafe 块需要格外审查

章节测试


判断题 1

RCU 允许读者在无锁开销的情况下读取共享数据,并且不需要任何 rcu_read_lock/rcu_read_unlock 调用。 ( )

  • 正确

  • 错误

判断题 2

持有 spinlock 时调用 kmalloc(…, GFP_KERNEL) 可能导致内核 sleep-while-atomic 警告。 ( )

  • 正确

  • 错误

判断题 3

x86 架构的内存模型是弱一致的,需要频繁使用内存屏障来保证顺序。 ( )

  • 正确

  • 错误

判断题 4

spin_lock_irqsave 在获取锁的同时禁用本地中断,并保存之前的中断状态。 ( )

  • 正确

  • 错误

判断题 5

atomic_t 中 atomic_readatomic_set 操作也是原子的(在 SMP 系统上)。 ( )

  • 正确

  • 错误

判断题 6

mutex_lock 可以在中断上下文中安全使用。 ( )

  • 正确

  • 错误

判断题 7

读-写自旋锁 (rwlock) 允许多个读者同时持有锁,但写者是互斥的。 ( )

  • 正确

  • 错误

判断题 8

AB-BA 死锁发生在两个锁以相同顺序被获取时。 ( )

  • 正确

  • 错误

判断题 9

rcu_dereference 只会做一个简单的指针读取,没有额外行为。 ( )

  • 正确

  • 错误

判断题 10

C 语言的 volatile 关键字可以替代内核的锁来保证多线程数据安全。 ( )

  • 正确

  • 错误


选择题 1

以下哪个不是内核并发源?

  • A. SMP(多核同时执行)

  • B. 内核抢占(preemption)

  • C. 函数回调(callback functions)

  • D. 硬件中断

选择题 2

持有 spinlock 时可以使用哪个 flags 安全地分配内存?

  • A. GFP_KERNEL

  • B. GFP_ATOMIC

  • C. GFP_USER

  • D. GFP_FS

选择题 3

RCU 最适合哪种场景?

  • A. 写多读少

  • B. 读多写少

  • C. 读写频率相同

  • D. 只写不读

选择题 4

以下哪个 API 用于在进程上下文中等待一个条件(可中断睡眠)?

  • A. spin_lock_irqsave

  • B. wait_event_interruptible

  • C. atomic_dec_and_test

  • D. rcu_read_lock

选择题 5

atomic_cmpxchg(ptr, old, new) 的语义是?

  • A. 总是将 *ptr 设置为 new

  • B. 如果 *ptr == old,则设置 *ptr = new,并返回 old;否则不修改

  • C. 如果 *ptr == old,则比较 *ptr 与 old 然后什么都不做

  • D. 将 *ptr 设置为 old,然后检查是否等于 new

选择题 6

synchronize_rcu() 会做什么?

  • A. 释放所有 RCU 保护的指针

  • B. 等待所有已进入 RCU 读临界区的读者退出,然后返回

  • C. 强制所有 CPU 进入睡眠状态

  • D. 同步所有磁盘写入

选择题 7

内核中检测死锁的主要工具是什么?

  • A. valgrind

  • B. lockdep

  • C. gdb

  • D. perf

选择题 8

以下关于 spinlock 和 mutex 的区别,哪个是正确的?

  • A. spinlock 可以在持有期间睡眠,mutex 不能

  • B. mutex 持有者可以睡眠,spinlock 持有者不能睡眠

  • C. mutex 可以在中断上下文中使用,spinlock 不能

  • D. 没有任何区别,可以互换使用

选择题 9

在 x86-64 架构上,哪个操作隐含了完整的内存屏障(full memory barrier)?

  • A. atomic_read

  • B. spin_lock (获取)

  • C. atomic_set

  • D. ACCESS_ONCE

选择题 10

Rust 如何防止数据竞争?(多选最佳选项)

  • A. 通过垃圾回收(GC)自动管理内存

  • B. 通过所有权系统和借用检查器在编译时验证

  • C. 通过运行时引用计数检查

  • D. 通过使用 volatile 关键字


️ 编程练习题

练习题 1:锁性能对比()

难度:

编写内核模块,测量 spinlock vs mutex vs RCU 在不同场景下的性能:

  1. 创建 N 个 reader 线程(N=1, 2, 4, 8, 16)
  2. 每个 reader 循环 100万次读操作
  3. 一个 writer 线程每 100ms 更新数据一次
  4. 分别使用 spinlock, mutex, RCU 实现
  5. 用 ktime 测量总耗时和平均操作时间
  6. 解释为什么 RCU 在读多写少场景下碾压其他方案

练习题 2:实现 lock-free 栈()

难度:

使用 atomic_t 和 atomic_cmpxchg 实现无锁栈:

  1. push: 使用 CAS 实现无锁头插入
  2. pop: 使用 CAS 实现无锁头删除
  3. 处理 ABA 问题(使用 tag 或 double-word CAS)
  4. 用多个线程测试正确性和性能

提示: 研究 Linux 内核中 include/linux/llist.h(lock-less linked list)。

练习题 3:编写死锁检测脚本()

难度:

  1. 编写一个 Python/bash 脚本,解析 lockdep 输出
  2. 自动提取循环依赖信息并以 Graphviz DOT 格式可视化
  3. 在 QEMU 中运行多个内核模块测试,收集 lockdep 报告
  4. 分析 Linux 内核中的已知死锁案例
    (git log —grep=“lockdep” —oneline kernel/locking/)

知识网络