Linux 内核 8 类同步机制详解(原理、场景与示例)

Linux 内核同步原语详解(原理、场景与示例)

本文系统性讲解 Linux 4.4 内核的 8 类同步机制:

  • 原子操作(atomic_t, atomic64_t 等)
  • 自旋锁(spinlock_t / raw_spinlock_t
  • 读-写自旋锁(rwlock_t
  • 信号量(struct semaphore
  • 读-写信号量(struct rw_semaphore
  • 互斥体(struct mutex
  • 顺序锁(seqlock_t / seqcount_t
  • 禁止抢占(preempt_disable() / preempt_enable()

每一节包含:实现原理、工作机制、适用场景、注意事项,以及可运行的核心示例片段(放在文档中,便于理解)。同时插入了对应的流程图帮助快速把握结构。代码基于内核 4.4 API,路径引用以本仓库为准。

图表阅读说明:

  • 蓝色框:操作/接口(API 调用)
  • 橙色框:上下文/约束(是否可睡眠、中断上下文等)
  • 绿色框:数据/资源(共享变量、队列、结构体)
  • 紫色框:内存序/屏障(Acquire/Release、smp_* 屏障)
  • 箭头:流程或依赖关系;图仅用于直观理解,不代表全部细节

目录结构与参考文件:

  • 头文件:include/linux/atomic.h, include/linux/spinlock.h, include/linux/rwlock.h, include/linux/semaphore.h, include/linux/rwsem.h, include/linux/mutex.h, include/linux/seqlock.h, include/linux/preempt.h
  • 关键实现:
    • RW 信号量:kernel/locking/rwsem-xadd.c, kernel/locking/rwsem-spinlock.c
    • 互斥体:kernel/locking/mutex.c(内部辅助:kernel/locking/mutex.h
    • 自旋锁与读写锁:各架构特有实现(例如 arch/x86/include/asm/spinlock.h),通用接口在 include/linux/spinlock.h
    • 顺序锁:include/linux/seqlock.h

1. 原子操作(Atomic Operations)

示意图:

原理与机制:

  • 原子操作在 CPU/总线层面保证单步读写的不可分割性。常用类型:atomic_t(32 位)、atomic64_t(64 位)。
  • 常用接口:atomic_read(), atomic_set(), atomic_inc(), atomic_dec(), atomic_add_return(), atomic_cmpxchg() 等。
  • 内存序:大多数原子操作本身不隐含"全面的内存屏障"。读/写本身是原子的,但并不保证与其他内存访问的顺序;需要配合 smp_mb()/smp_rmb()/smp_wmb() 或者使用"有 acquire/release 语义"的变体(具体到架构)。

适用场景:

  • 轻量计数器(引用计数、状态标志)。
  • 与锁配合的快速路径(例如尝试锁定或状态检查)。
  • CAS(比较并交换)实现无锁队列/栈的关键原语(需谨慎,复杂场景建议使用现成内核结构)。

注意事项:

  • atomic_read()/atomic_set()不提供内存序屏障;若需与并发访问建立发布/获取关系,请显式添加屏障或使用更高层同步原语。
  • 原子操作不等于无锁即安全;可见性与顺序仍需正确设计。

示例:简单引用计数与条件翻转

c 复制代码
#include <linux/atomic.h>

static atomic_t refcnt = ATOMIC_INIT(0);
static atomic_t flag = ATOMIC_INIT(0);

void acquire_resource(void)
{
    atomic_inc(&refcnt); // 原子增加
}

void release_resource(void)
{
    if (atomic_dec_and_test(&refcnt)) {
        // 计数归零,安全释放资源
    }
}

bool try_set_flag(void)
{
    // 当 flag == 0 时置为 1,成功返回 true
    return atomic_cmpxchg(&flag, 0, 1) == 0;
}

2. 自旋锁(Spinlock)

示意图:

原理与机制:

  • 自旋锁通过忙等(busy-wait)在短临界区内实现互斥。持锁期间禁止调度,不能睡眠。
  • 典型接口:spin_lock(), spin_unlock(), spin_lock_irqsave(), spin_unlock_irqrestore();原始版本:raw_spin_lock()
  • 内存序:spin_lock()提供 acquire 语义,spin_unlock()提供 release 语义;在多数架构上等价于全面的临界区内存有序性保障。

适用场景:

  • 极短的临界区、对延迟敏感(尤其在中断上下文)。
  • 保护 per-CPU 数据、队列头、状态位等快速更新路径。

注意事项:

  • 持锁期间不得调用可能睡眠的 API(如 mutex_lock()down())。
  • 中断上下文使用 spin_lock_irqsave(),确保不会被本地中断打断且正确恢复标志位。
  • 避免长临界区与重入,防止严重的活锁/延迟。
  • 严格遵守锁顺序,防止死锁;启用 lockdep 可在开发期辅助检查。

示例:保护链表头(中断安全)

c 复制代码
#include <linux/spinlock.h>

static spinlock_t lock; // 需要 init,例如在模块初始化中 spin_lock_init(&lock)
static struct list_head my_list; // 需 INIT_LIST_HEAD(&my_list)

void push_item(struct my_node *n)
{
    unsigned long flags;
    spin_lock_irqsave(&lock, flags);
    list_add(&n->link, &my_list);
    spin_unlock_irqrestore(&lock, flags);
}

3. 读-写自旋锁(RW Spinlock)

示意图:

原理与机制:

  • rwlock_t 允许多个读者并发,写者独占,基于自旋实现。
  • 典型接口:read_lock()/read_unlock()write_lock()/write_unlock();对应的 irqsave 变体同理。
  • 读锁不会阻塞其他读者;写锁需要等待所有读者释放。

适用场景:

  • 读多写少且临界区非常短的场景,例如读取共享配置、统计头信息等。

注意事项:

  • 与普通自旋锁相同,持锁期间不得睡眠。
  • 写优先或读优先具体依赖架构与实现细节,可能存在饥饿风险;临界区需尽量短。

示例:读多写少的配置访问

c 复制代码
#include <linux/rwlock.h>

static rwlock_t cfg_lock; // rwlock_init(&cfg_lock)
static int cfg_value;

int read_cfg(void)
{
    unsigned long flags;
    int v;
    read_lock_irqsave(&cfg_lock, flags);
    v = cfg_value;
    read_unlock_irqrestore(&cfg_lock, flags);
    return v;
}

void write_cfg(int newv)
{
    unsigned long flags;
    write_lock_irqsave(&cfg_lock, flags);
    cfg_value = newv;
    write_unlock_irqrestore(&cfg_lock, flags);
}

4. 信号量(Semaphore)

示意图:

原理与机制:

  • struct semaphore 是可阻塞的计数型锁,线程在无法获取时会睡眠,适合较长临界区。
  • 典型接口:sema_init(), down(), down_interruptible(), down_trylock(), up()
  • 实现依赖等待队列和调度,可能包含唤醒策略与公平性处理。

适用场景:

  • 需要睡眠等待的资源访问,跨调用链的较长操作。
  • 计数型资源(N 个并发许可)。但在现代内核中计数型资源更常用更高层抽象或特定子系统接口。

注意事项:

  • 相比 mutex,信号量更通用但也更容易误用;若只是二值互斥,优先考虑 mutex
  • 不可在中断上下文使用(会睡眠)。

示例:二值信号量充当互斥(不建议,示例仅为说明)

c 复制代码
#include <linux/semaphore.h>

static struct semaphore sem;

static int init_sem(void)
{
    sema_init(&sem, 1); // 二值互斥
    return 0;
}

void critical_section(void)
{
    if (down_interruptible(&sem))
        return; // 被信号打断
    // ... 执行较长的可睡眠操作 ...
    up(&sem);
}

何时选 mutex / 何时选 semaphore(针对本小节)

  • 优先选 mutex:单持有者互斥、可睡眠、临界区较长;需要所有权约束与更强的误用检测(如禁止递归、跨线程解锁)。
  • semaphore:明确的"计数型资源"(N>1 并发许可),如连接槽位、缓冲区配额;不建议用二值信号量充当互斥,改用 mutex 更安全。
  • 共同约束:两者都可能睡眠,不能在中断上下文使用;持有自旋锁时不可调用 down*()/mutex_lock()
  • 快速映射:
    • 二值互斥 → mutex
    • 计数配额(N>1)→ semaphore
    • 中断上下文/极短不可睡眠 → spinlock_t/rwlock_t
    • 读多写少且较长 → rw_semaphore

5. 读-写信号量(RW Semaphore)

示意图:

原理与机制:

  • struct rw_semaphore 支持读并发、写独占,但读写都可能睡眠等待。适合"读多写少且临界区较长"的场景。
  • 典型接口:init_rwsem(), down_read(), up_read(), down_write(), up_write(), downgrade_write()
  • 4.4 内核实现包含争用处理与优化(如乐观自旋),参考:kernel/locking/rwsem-xadd.c, kernel/locking/rwsem-spinlock.c

适用场景:

  • 页缓存、VFS、内存管理子系统中读多写少的路径;需要睡眠等待且临界区可能较长。

注意事项:

  • 写方可能唤醒队列中的读者或写者,唤醒策略会影响公平性与吞吐。
  • 与自旋版 RW 锁相比,这里是可睡眠的;不可在中断上下文使用。

示例:元数据读写

c 复制代码
#include <linux/rwsem.h>

static DECLARE_RWSEM(meta_rwsem);
static struct meta { int a; long b; } M;

int read_meta_copy(struct meta *out)
{
    down_read(&meta_rwsem);
    *out = M; // 复制较长结构
    up_read(&meta_rwsem);
    return 0;
}

void update_meta(int a, long b)
{
    down_write(&meta_rwsem);
    M.a = a;
    M.b = b;
    up_write(&meta_rwsem);
}

6. 互斥体(Mutex)

示意图:

原理与机制:

  • struct mutex 是最常用的可睡眠互斥原语,提供简单的锁定语义与(可选的)自旋优化。接口:mutex_init(), mutex_lock(), mutex_lock_interruptible(), mutex_trylock(), mutex_unlock()
  • 内存序:mutex_lock()/mutex_unlock()具有 acquire/release 语义。

适用场景:

  • 线程上下文中的较长临界区;API 设计更安全、易读。

注意事项:

  • 不可在中断上下文使用(会睡眠)。
  • 谨防递归加锁(会触发 DEBUG_MUTEXES 报错)。
  • 锁持有期间避免调用可能导致不可控延迟的耗时操作;必要时拆分临界区。

示例:典型互斥保护

c 复制代码
#include <linux/mutex.h>

static DEFINE_MUTEX(mtx);
static long shared_state;

void do_work(long v)
{
    mutex_lock(&mtx);
    shared_state += v; // 较长但可睡眠的路径
    mutex_unlock(&mtx);
}

何时选 mutex / 何时选 semaphore(针对本小节)

  • mutex:需要严格"互斥"语义(一次仅一个持有者)、临界区可睡眠且较长、希望有更好的调试支持与所有权约束。
  • 改用 semaphore 的情况:确实存在"资源配额(N>1 并发许可)"的需求;如果只是二值互斥,应继续使用 mutex
  • 共同约束:不可在中断上下文使用;避免在持自旋锁区域调用可能睡眠的 API。
  • 快速映射:
    • 单持有者互斥(可睡眠)→ mutex
    • 多许可并发(可睡眠)→ semaphore
    • 读多写少且较长 → rw_semaphore
    • 中断/不可睡眠短临界区 → spinlock_t/rwlock_t

7. 顺序锁(Seqlock / Seqcount)

示意图:

原理与机制:

  • 顺序锁通过"写方增加序号,读方无锁读取并重试"的模式,实现读路径的无锁一致性检查。
  • 写路径独占(使用自旋或禁抢占/禁中断),读路径不加锁但可能多次重试。
  • 典型接口:
    • seqlock_twrite_seqlock(), write_sequnlock(), read_seqbegin(), read_seqretry() 等。
    • seqcount_t:更细粒度的序号计数,不绑定具体锁策略,由调用者保证写侧的互斥。
  • 用途示例:时钟、统计信息、轻量结构体快照(读多写少)。

适用场景:

  • 读路径需要无锁快速读取,允许重试;写路径短而可控。

注意事项:

  • 读方可能重试多次,适合小结构体和轻量拷贝。
  • 不适合读方需要"实时"一致的复杂结构(如链表遍历)。
  • 写方必须保证互斥(通常配合自旋锁或禁抢占)。

示例:读取时间戳快照

c 复制代码
#include <linux/seqlock.h>

static seqlock_t ts_lock = SEQLOCK_UNLOCKED;
struct ts { u64 tsc; u64 jiffies; } TS;

struct ts read_ts(void)
{
    struct ts snapshot;
    unsigned seq;
    do {
        seq = read_seqbegin(&ts_lock);
        snapshot = TS; // 无锁读
    } while (read_seqretry(&ts_lock, seq));
    return snapshot;
}

void update_ts(u64 tsc, u64 j)
{
    write_seqlock(&ts_lock);
    TS.tsc = tsc;
    TS.jiffies = j;
    write_sequnlock(&ts_lock);
}

8. 禁止抢占(preempt_disable / preempt_enable)

示意图:

原理与机制:

  • 抢占是调度器在任意合适点切换当前任务的能力。preempt_disable() 禁止当前 CPU 上的内核抢占,preempt_enable() 恢复。
  • 常用于保护 per-CPU 数据访问的短窗口(避免迁移到其他 CPU 导致并发问题)。
  • 与中断不同:禁抢占不禁止中断;如需同时禁止本地中断,使用 local_irq_save()/local_irq_restore()spin_lock_irqsave()

适用场景:

  • 极短的窗口访问 __percpu 数据或 CPU 本地状态,无需锁但需要保证不被调度迁移。

注意事项:

  • 禁抢占不提供内存屏障;仅防止任务切换。必要时使用锁或显式内存屏障保证有序。
  • 窗口必须非常短,避免影响系统实时性。

示例:安全访问 per-CPU 计数

c 复制代码
#include <linux/preempt.h>
#include <linux/percpu.h>

DEFINE_PER_CPU(unsigned long, pcpu_hits);

void bump_hits(void)
{
    preempt_disable();
    this_cpu_inc(pcpu_hits); // 当前 CPU 局部更新
    preempt_enable();
}

选择指南(何时用哪种?)

总览矩阵(更直观的选择方式):

另见:曲线决策树(按问题逐步选择):

快速映射:

  • 极短、不可睡眠:优先 spinlock_t;读多写少且短:rwlock_t
  • 可睡眠、较长临界区:优先 mutex;读多写少且较长:rw_semaphore
  • 读方无锁快照、允许重试:seqlock/seqcount
  • 简单计数或状态位:atomic_t/atomic64_t 配合屏障。
  • 仅防迁移的极短窗口:preempt_disable()

常见模式:

  • 设备中断处理:spin_lock_irqsave() 保护共享队列。
  • 配置读取(读多写少):用户态接口读路径用 rw_semaphore,写路径独占更新。
  • 时间/统计快照:使用 seqlock,读侧无锁重试。
  • 内核线程长操作:使用 mutex,避免自旋造成 CPU 浪费。

内存序与屏障简表

  • spin_lock():Acquire;spin_unlock():Release;临界区内形成有序性。
  • mutex_lock()/unlock():Acquire/Release。
  • rwlock/rw_semaphore:按读/写加解锁,整体遵循 Acquire/Release 语义。
  • atomic_*:原子性 != 完整有序;必要时使用 smp_mb()smp_rmb()smp_wmb()
  • seqlock:写侧递增序号并提供必要屏障;读侧需 read_seqretry() 检测并重试。
  • preempt_disable():仅禁止抢占,不是屏障。

开发注意事项(最佳实践)

  • 锁定粒度:尽量缩小临界区,减少持锁时间;必要时按数据分片使用不同锁。
  • 锁顺序:统一的全局锁顺序,避免循环等待;调试期启用 lockdep
  • 避免在持自旋锁或禁中断期间调用可能睡眠的函数。
  • 中断上下文只能使用不可睡眠的原语(自旋锁、seqlock 写侧自旋等)。
  • 性能权衡:读多写少优先读写锁或 seqlock;写多读少倾向普通互斥。
  • 结合 RCU:只读路径且可接受延迟释放,考虑 RCU;与本文原语互补。

演示实例集合(文档内嵌)

以下片段汇总了上文示例的关键用法,便于一次性对比:

原子计数与 CAS:

c 复制代码
static atomic_t refcnt = ATOMIC_INIT(0);
static atomic_t flag = ATOMIC_INIT(0);
atomic_inc(&refcnt);
if (atomic_dec_and_test(&refcnt)) { /* release */ }
bool set = (atomic_cmpxchg(&flag, 0, 1) == 0);

自旋锁(中断安全):

c 复制代码
spinlock_t lock; unsigned long flags;
spin_lock_irqsave(&lock, flags);
/* short critical section */
spin_unlock_irqrestore(&lock, flags);

读写自旋锁:

c 复制代码
rwlock_t rwl; unsigned long flags; int v;
read_lock_irqsave(&rwl, flags); v = cfg_value; read_unlock_irqrestore(&rwl, flags);
write_lock_irqsave(&rwl, flags); cfg_value = v; write_unlock_irqrestore(&rwl, flags);

信号量(可睡眠):

c 复制代码
struct semaphore sem; sema_init(&sem, 1);
if (!down_interruptible(&sem)) { /* long section */ up(&sem); }

读写信号量:

c 复制代码
DECLARE_RWSEM(meta_rwsem);
down_read(&meta_rwsem); /* copy */ up_read(&meta_rwsem);
down_write(&meta_rwsem); /* update */ up_write(&meta_rwsem);

互斥体:

c 复制代码
DEFINE_MUTEX(mtx);
mutex_lock(&mtx); /* long but sleepable */ mutex_unlock(&mtx);

顺序锁:

c 复制代码
seqlock_t sl = SEQLOCK_UNLOCKED; unsigned seq;
do { seq = read_seqbegin(&sl); snapshot = S; } while (read_seqretry(&sl, seq));
write_seqlock(&sl); S = newS; write_sequnlock(&sl);

禁止抢占与 per-CPU:

c 复制代码
preempt_disable(); this_cpu_inc(pcpu_hits); preempt_enable();

结语

选择同步原语的核心是:理解"是否可睡眠、临界区长短、读写比例、是否在中断上下文、是否需要无锁读快照以及内存序要求"。遵循上述指引与注意事项,可在内核 4.4 环境下写出既正确又高效的并发代码。

如需进一步结合具体子系统(如 VFS、MM、RCU)展开,可在本仓库的相关文档与源码路径中交叉阅读:

  • fs/(VFS/文件系统)、mm/(内存管理)、kernel/locking/(锁与同步)、kernel/rcu/(RCU)。
相关推荐
YJlio2 小时前
PsService·下(7.21):Find/SetConfig 与服务的启动/停止/重启/暂停/恢复
linux·运维·服务器
波诺波2 小时前
环境管理器
linux·前端·python
_OP_CHEN3 小时前
Linux系统编程:(六)深入理解 Linux 软件包管理器——从原理到 yum 实战全攻略
linux·运维·服务器·yum·软件包管理器·linux生态
人工智能训练3 小时前
Ubuntu系统中Docker的常用命令总结
linux·运维·人工智能·ubuntu·docker·ai
KYGALYX11 小时前
在Linux中备份msyql数据库和表的详细操作
linux·运维·数据库
余—笙11 小时前
Linux(docker)安装搭建CuteHttpFileServer/chfs文件共享服务器
linux·服务器·docker
lang2015092811 小时前
Linux高效备份:tar与gzip完全指南
linux·运维·服务器
IDOlaoluo11 小时前
OceanBase all-in-one 4.2.0.0 安装教程(CentOS 7/EL7 一键部署详细步骤)
linux·centos·oceanbase
catoop12 小时前
在 WSL 的 Ubuntu 中安装和配置 SSH 服务
linux·ubuntu·ssh