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 内核中,自旋锁持有期间禁止抢占,进一步放大了延迟风险。
适用场景
| 场景 | 说明 |
|---|---|
| 中断上下文 | 中断处理函数中不能睡眠,只能使用自旋锁 |
| 极短临界区 | 仅修改几个寄存器或标志位,耗时在微秒级 |
| 多核实时系统 | 需要低延迟且临界区确定性强 |
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) |
| 需要优先级继承 | 实时系统中防止优先级反转的必备选择 |
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)作为替代方案。
适用场景
| 场景 | 说明 |
|---|---|
| 读多写少 | 配置表、路由表等频繁读取、极少更新的数据 |
| 读者可并发 | 多核系统中读者不互相阻塞,提升并行度 |
| 写者延迟可容忍 | 写操作不频繁,且对写延迟要求不苛刻 |
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 值验证。写者之间使用自旋锁互斥,写者持有时间必须短。
适用场景
| 场景 | 说明 |
|---|---|
| 读极多写极少 | 系统时间、统计计数器、配置快照 |
| 读者可接受重试 | 重试概率低(写频率低),重试开销可忽略 |
| 写者优先级高 | 写者不能被读者阻塞,保证写操作的实时性 |
2.5 四种锁机制对比总结
| 特性 | 自旋锁 | 互斥锁 | 读写锁 | 顺序锁 |
|---|---|---|---|---|
| 等待行为 | 忙等(自旋) | 睡眠 | 读者:并发;写者:睡眠 | 读者:重试;写者:互斥 |
| 上下文限制 | 中断上下文可用 | 仅进程上下文 | 中断上下文(读者) | 中断上下文(读者) |
| 优先级反转 | 可能(需禁止抢占) | 需优先级继承 | 可能(写者饥饿) | 写者无阻塞 |
| 临界区长度 | 极短(< 10 μs) | 任意(可长) | 读者:任意;写者:短 | 写者:短 |
| 多核扩展性 | 差(所有核自旋) | 好(睡眠不占 CPU) | 好(读者并行) | 极好(读者无锁) |
| 实时推荐 | raw_spinlock_t | rt_mutex | 谨慎使用 | 读多写少场景 |
选型建议:在实时内核抢占模型中,优先使用
raw_spinlock_t 保护极短临界区,使用 rt_mutex 保护可能睡眠的长临界区。读写锁和顺序锁仅在读多写少且写延迟不敏感的场景下使用,并配合实时分析工具验证最坏情况延迟。