2、内核锁机制基础:自旋锁、互斥锁、读写锁、顺序锁的底层实现与适用场景

在实时内核抢占模型中,锁机制是保护共享资源、防止竞态条件的核心手段。不同的锁原语在性能、延迟和并发度上存在显著差异,错误的选择可能导致优先级反转、死锁或非确定性延迟。本节深入分析四种基础锁的底层实现原理与适用场景。

核心原则:实时系统中,锁的选择应遵循“最小临界区、最短持有时间、最确定行为”原则。自旋锁适用于极短临界区,互斥锁适用于可能睡眠的场景,读写锁和顺序锁则针对读多写少场景优化。

2.1 自旋锁 (Spinlock)

自旋锁是最简单的锁机制,当锁被持有时,等待线程会持续循环检测(“自旋”)直到锁可用。在单核非抢占内核中,自旋锁通常退化为空操作;在多核或抢占内核中,它依赖原子指令实现。

底层实现

  • 原子操作:基于 CPU 提供的原子指令(如 x86 的 LOCK CMPXCHG、ARM 的 LDREX/STREX)实现锁状态的原子修改。
  • 内存屏障:自旋锁操作隐含内存屏障(Memory Barrier),保证临界区内的内存访问不会重排序到锁获取之前或释放之后。
  • 抢占控制:在 Linux 实时内核(PREEMPT_RT)中,自旋锁被实现为 raw_spinlock,持有期间会禁止本地 CPU 的抢占,防止持有锁的线程被高优先级任务抢占。
// Linux 自旋锁核心实现(简化伪代码)
typedef struct {
    volatile unsigned int lock;
} spinlock_t;

void spin_lock(spinlock_t *lock) {
    while (__atomic_test_and_set(&lock->lock, 1)) {
        // 自旋等待,CPU 忙等
        cpu_relax();  // 提示 CPU 当前处于自旋循环
    }
    // 获取锁后隐含 acquire 内存屏障
    __atomic_thread_fence(__ATOMIC_ACQUIRE);
}

void spin_unlock(spinlock_t *lock) {
    // 释放锁前隐含 release 内存屏障
    __atomic_thread_fence(__ATOMIC_RELEASE);
    __atomic_clear(&lock->lock, __ATOMIC_RELAXED);
}
实时影响:自旋锁持有时间必须极短(通常 < 10 μs),否则会导致高优先级任务被低优先级任务阻塞,引发优先级反转。在 PREEMPT_RT 内核中,自旋锁持有期间禁止抢占,进一步放大了延迟风险。

适用场景

场景说明
中断上下文中断处理函数中不能睡眠,只能使用自旋锁
极短临界区仅修改几个寄存器或标志位,耗时在微秒级
多核实时系统需要低延迟且临界区确定性强
自旋锁工作流程 CPU 0: 请求锁 锁可用? 是 进入临界区 否 自旋等待 ⚠ 自旋锁持有期间禁止抢占,临界区必须极短。实时系统中建议使用 raw_spinlock_t 避免优先级反转。

2.2 互斥锁 (Mutex)

互斥锁(Mutex)是睡眠锁,当锁被持有时,等待线程会进入睡眠状态,让出 CPU。在实时内核中,互斥锁必须支持优先级继承协议(Priority Inheritance Protocol, PIP)以防止优先级反转。

底层实现

  • 快速路径:锁未被持有时,通过原子操作直接获取,无上下文切换开销。
  • 慢速路径:锁已被持有时,当前线程加入等待队列并调用调度器睡眠,持有者释放锁时唤醒等待队列中的最高优先级线程。
  • 优先级继承:当低优先级任务持有锁,高优先级任务等待时,低优先级任务临时继承高优先级任务的优先级,直到释放锁。
// Linux RT-Mutex 核心结构(简化)
struct rt_mutex {
    raw_spinlock_t      wait_lock;    // 保护等待队列的自旋锁
    struct rb_root_cached   waiters;  // 红黑树等待队列(按优先级排序)
    struct task_struct      *owner;   // 当前持有者,低 bit 表示是否有等待者
};

// 优先级继承核心逻辑(伪代码)
void rt_mutex_setprio(struct task_struct *task, int prio) {
    int old_prio = task->prio;
    task->prio = prio;
    // 如果 task 当前正在运行,触发抢占调度
    if (task->on_rq)
        resched_curr(task_rq(task));
}
关键陷阱:互斥锁不能在中断上下文中使用(可能睡眠)。实时系统中必须使用支持优先级继承的 RT-Mutex,普通 Mutex 会导致不可控的优先级反转。

适用场景

场景说明
进程上下文任务可以安全睡眠等待锁
临界区较长临界区执行时间 > 上下文切换开销(通常 > 100 μs)
需要优先级继承实时系统中防止优先级反转的必备选择
互斥锁优先级继承流程 任务 A (优先级 10) 持有锁 M 任务 B (优先级 50) 请求锁 M → 阻塞 优先级继承 A 提升至 50 A 释放锁 M 优先级恢复为 10 B 获取锁 M 继续执行 💡 优先级继承防止了无限期阻塞:低优先级任务 A 临时提升优先级, 使其能被调度执行并尽快释放锁,高优先级任务 B 的阻塞时间被限制在 A 的临界区内。

2.3 读写锁 (rwlock)

读写锁允许多个读者同时访问共享资源,但写者必须独占访问。在实时内核中,读写锁的设计需要平衡读者并发性与写者饥饿问题。

底层实现

  • 读者计数:使用原子变量维护当前读者数量,读者获取锁时原子递增,释放时递减。
  • 写者独占:写者获取锁前必须等待所有读者释放锁,且后续读者会被阻塞直到写者完成。
  • 公平性:实时内核中通常实现写者偏向(Writer-Preference)策略,防止写者被源源不断的读者饿死。
// Linux rwlock_t 核心实现(简化)
typedef struct {
    atomic_t        cnts;       // 低 24 位:读者计数;高 8 位:写者标志
    raw_spinlock_t  wait_lock;  // 保护等待队列
    struct list_head    writer_wait;
    struct list_head    reader_wait;
} rwlock_t;

void read_lock(rwlock_t *lock) {
    if (atomic_add_unless(&lock->cnts, 1, 0x01000000)) {
        // 无写者持有,快速获取读锁
        return;
    }
    // 有写者,加入读者等待队列并睡眠
    __read_lock_slowpath(lock);
}

void write_lock(rwlock_t *lock) {
    if (atomic_cmpxchg(&lock->cnts, 0, 0x01000000) == 0) {
        // 无读者且无写者,快速获取写锁
        return;
    }
    // 加入写者等待队列并睡眠
    __write_lock_slowpath(lock);
}
实时考量:读写锁在读者密集场景下性能优异,但写者可能面临高延迟。实时系统中建议限制读者数量或使用 RCU(Read-Copy-Update)作为替代方案。

适用场景

场景说明
读多写少配置表、路由表等频繁读取、极少更新的数据
读者可并发多核系统中读者不互相阻塞,提升并行度
写者延迟可容忍写操作不频繁,且对写延迟要求不苛刻
读写锁状态转换 空闲 cnts = 0 读锁定 cnts = N (N>0) 写锁定 cnts = 0x01000000 read_lock 所有读者 unlock write_lock write_unlock write_unlock ⚠ 写者偏向策略:当写者等待时,新读者被阻塞,防止写者饥饿。 实时系统中需监控写者等待时间,避免写延迟超过任务截止时间。

2.4 顺序锁 (seqlock)

顺序锁是一种乐观锁机制,读者不阻塞写者,但读者需要验证读取过程中是否有写者介入。如果检测到写者修改,读者必须重试。顺序锁适用于读者多、写者少且写操作原子性要求不严格的场景。

底层实现

  • 序列计数器:使用一个递增的序列号(sequence number),写者获取锁时递增,释放时再次递增。奇数表示写者正在修改。
  • 读者验证:读者在读取前后分别读取序列号,如果两次读取的序列号不同或为奇数,说明数据不一致,需要重试。
  • 无读者阻塞:读者不会阻塞写者,写者也不会阻塞读者(仅在写者之间互斥)。
// Linux seqlock_t 核心实现(简化)
typedef struct {
    struct seqcount {
        unsigned sequence;  // 序列计数器
    } seqcount;
    spinlock_t lock;        // 写者之间的互斥锁
} seqlock_t;

// 读者路径
unsigned read_seqbegin(const seqlock_t *sl) {
    unsigned seq;
    do {
        seq = READ_ONCE(sl->seqcount.sequence);
        if (unlikely(seq & 1))  // 奇数表示写者正在修改
            cpu_relax();
    } while (seq & 1);
    return seq;
}

bool read_seqretry(const seqlock_t *sl, unsigned start) {
    return READ_ONCE(sl->seqcount.sequence) != start;
}

// 写者路径
void write_seqlock(seqlock_t *sl) {
    spin_lock(&sl->lock);
    WRITE_ONCE(sl->seqcount.sequence, sl->seqcount.sequence + 1);  // 变为奇数
    smp_wmb();  // 写内存屏障
}

void write_sequnlock(seqlock_t *sl) {
    smp_wmb();  // 写内存屏障
    WRITE_ONCE(sl->seqcount.sequence, sl->seqcount.sequence + 1);  // 变为偶数
    spin_unlock(&sl->lock);
}
使用限制:顺序锁要求读者能够容忍重试开销,且读取的数据必须是一致性快照。如果读者需要多次读取关联数据,必须使用同一个 seq 值验证。写者之间使用自旋锁互斥,写者持有时间必须短。

适用场景

场景说明
读极多写极少系统时间、统计计数器、配置快照
读者可接受重试重试概率低(写频率低),重试开销可忽略
写者优先级高写者不能被读者阻塞,保证写操作的实时性
顺序锁读者-写者交互时序 时间 写者: seq=1→2 写者: seq=3→4 读者: 读取 读者: 重试 seq=1 seq=2 seq=3 seq=4 seq=2 ✓ seq=4 ≠ 3 ✗ 💡 读者在 seq=2 时读取成功;在 seq=3 时读取,但写者修改导致 seq 变为 4,读者检测到不一致后重试。

2.5 四种锁机制对比总结

特性自旋锁互斥锁读写锁顺序锁
等待行为忙等(自旋)睡眠读者:并发;写者:睡眠读者:重试;写者:互斥
上下文限制中断上下文可用仅进程上下文中断上下文(读者)中断上下文(读者)
优先级反转可能(需禁止抢占)需优先级继承可能(写者饥饿)写者无阻塞
临界区长度极短(< 10 μs)任意(可长)读者:任意;写者:短写者:短
多核扩展性差(所有核自旋)好(睡眠不占 CPU)好(读者并行)极好(读者无锁)
实时推荐raw_spinlock_trt_mutex谨慎使用读多写少场景
选型建议:在实时内核抢占模型中,优先使用 raw_spinlock_t 保护极短临界区,使用 rt_mutex 保护可能睡眠的长临界区。读写锁和顺序锁仅在读多写少且写延迟不敏感的场景下使用,并配合实时分析工具验证最坏情况延迟。