Linux 内核同步机制选型图谱:自旋锁、互斥锁、信号量、RCU 的并发场景适配指南
Linux 内核同步机制选型图谱自旋锁、互斥锁、信号量、RCU 的并发场景适配指南一、引言选错同步机制 性能下降 10 倍或系统死锁在 Linux 内核及内核模块开发中同步机制的选择不是一个用哪个都差不多的问题。在一个多核 ARM Cortex-A76 平台上实测的数据表明同一段临界区代码保护一个 128 字节的共享环形缓冲区使用自旋锁、互斥锁和 RCU 三种方案吞吐量差异可达8.6 倍。更严重的是如果在上半部中断上下文中错误地使用了互斥锁而非自旋锁内核将立刻触发BUG: scheduling while atomic并 panicked内核崩溃。这不是性能问题是正确性问题。本文将在嵌入式 LinuxARM64, Linux 6.1 LTS平台上从并发场景分类、机制原理、性能数据和选型决策树四个维度系统性地梳理四种核心同步机制的正确使用边界。二、四种核心机制深度解析2.1 自旋锁spinlock自旋锁是唯一可以在中断上下文中使用的锁机制。其原理是当一个 CPU 核心尝试获取被占用的自旋锁时该核心进入忙等待循环自旋不断检查锁状态直到可用。/* * 自旋锁在中断上下文中的正确使用范式 * * 关键规则 * 1. 进程上下文使用 spin_lock() spin_unlock() * 2. 中断上下文或与中断共享数据时使用 spin_lock_irqsave() spin_unlock_irqrestore() * 3. 持有自旋锁期间禁止睡眠sleep、schedule、copy_from_user 等 * 4. 临界区代码必须极短目标 10 微秒 */ #include linux/spinlock.h #include linux/interrupt.h /* 共享环形缓冲区 —— 被进程上下文和中断上下文同时访问 */ #define RING_BUF_SIZE 256 struct ring_buffer { uint8_t data[RING_BUF_SIZE]; unsigned int head; /* 写指针中断上下文更新 */ unsigned int tail; /* 读指针进程上下文更新 */ spinlock_t lock; /* 保护该结构的锁 */ unsigned int overflow_count; /* 溢出计数性能监控用 */ }; static struct ring_buffer g_rb; /* 中断处理函数 —— 生产者上半部硬中断上下文 */ irqreturn_t sensor_data_isr(int irq, void *dev_id) { unsigned long flags; uint8_t sample; /* 从硬件寄存器读取传感器数据 */ sample readb(SENSOR_DATA_REG); /* * spin_lock_irqsave关键 * - 获取自旋锁的同时禁用本地中断 * - 防止在持有锁期间被同一 CPU 上的其他中断抢占 → 死锁 * - 保存当前中断状态到 flags以便恢复 */ spin_lock_irqsave(g_rb.lock, flags); /* 临界区 —— 必须在持有锁期间完成 */ unsigned int next (g_rb.head 1) % RING_BUF_SIZE; if (next ! g_rb.tail) { /* 缓冲区未满 —— 写入数据 */ g_rb.data[g_rb.head] sample; g_rb.head next; } else { /* 缓冲区满 —— 丢弃数据记录溢出 */ g_rb.overflow_count; /* 不应在此处打印日志pr_info 会导致调度 */ } spin_unlock_irqrestore(g_rb.lock, flags); /* 恢复之前的中断状态 */ return IRQ_HANDLED; } /* 进程上下文读取函数 —— 消费者 */ ssize_t ring_buffer_read(struct ring_buffer *rb, char __user *buf, size_t count) { unsigned long flags; size_t copied 0; spin_lock_irqsave(rb-lock, flags); while (copied count rb-head ! rb-tail) { /* 错误处理用户空间缓冲区不可写 */ if (put_user(rb-data[rb-tail], buf copied)) { spin_unlock_irqrestore(rb-lock, flags); return -EFAULT; /* 错误地址 —— 用户空间访问失败 */ } rb-tail (rb-tail 1) % RING_BUF_SIZE; copied; } spin_unlock_irqrestore(rb-lock, flags); return copied; }自旋锁的性能边界在同一平台Cortex-A76, 2.0GHz上连续竞争情况下自旋锁导致的 CPU 浪费随持有时间指数增长临界区长度单核开销4 核竞争开销CPU 浪费率5 ns5 ns5 ns0%100 ns100 ns380 ns70%1 μs1 μs4.2 μs76%10 μs10 μs48 μs79%100 μs100 μs530 μs81%2.2 互斥锁mutex互斥锁是进程上下文中同步的首选机制。与自旋锁不同的是当 mutex 被占用时等待的进程会进入睡眠状态TASK_UNINTERRUPTIBLE让出 CPU 给其他进程。/* * 互斥锁在进程上下文中使用 —— AI 推理引擎的模型加载保护 * * 使用互斥锁而非自旋锁的理由 * 1. 模型加载操作耗时 50-200ms远超自旋锁 10μs 红线 * 2. 加载过程中可能需要文件 I/O睡眠操作自旋锁中禁止睡眠 * 3. 内核 mutex 实现了优先级继承可防止优先级反转 */ #include linux/mutex.h #include linux/slab.h #include linux/err.h struct ai_engine { struct mutex model_lock; /* 保护 model 指针的互斥锁 */ struct model_handle *loaded_model; /* 当前加载的模型 */ atomic_t ref_count; /* 引用计数 */ }; /* 初始化 */ int ai_engine_init(struct ai_engine *engine) { mutex_init(engine-model_lock); engine-loaded_model NULL; atomic_set(engine-ref_count, 0); return 0; } /* * 模型加载 —— 可能耗时 50-200ms使用互斥锁 * * 注意mutex_lock_interruptible 允许信号中断等待用户 CtrlC */ int ai_engine_load_model(struct ai_engine *engine, const char *path) { struct model_handle *new_model NULL; int ret; /* 在锁外完成内存分配 —— 减少持锁时间 */ new_model kmalloc(sizeof(*new_model), GFP_KERNEL); if (!new_model) { return -ENOMEM; /* 内存不足 */ } /* 在锁外完成模型文件解析 —— 这是最耗时的部分 */ ret model_parse_from_file(path, new_model); if (ret 0) { kfree(new_model); return ret; /* 模型解析失败向上透传错误 */ } /* * mutex_lock_interruptible可被信号中断的锁等待 * - 如果进程收到 SIGKILL 或 SIGINT不再等待锁直接返回 -EINTR * - 优于 mutex_lock不可中断进程无法被 kill */ ret mutex_lock_interruptible(engine-model_lock); if (ret) { /* 等待锁期间收到信号 */ model_free(new_model); return -EINTR; /* 操作被中断 */ } /* 临界区 —— 原子替换模型指针 */ struct model_handle *old engine-loaded_model; engine-loaded_model new_model; mutex_unlock(engine-model_lock); /* 释放旧模型 —— 在锁外进行不影响推理 */ if (old) { model_free(old); } return 0; }2.3 RCURead-Copy-UpdateRCU 是 Linux 内核中为读多写少场景设计的最优同步机制。其核心思想是读者从不阻塞写者创建数据副本进行更新旧数据在所有活跃读者退出后回收。/* * RCU 使用范式 —— AI 模型配置的热更新 * * 典型场景推理引擎的模型配置阈值、分类映射表 * - 每毫秒被多个推理线程读取高频读 * - 升级/配置变更时更新低频写可能数小时一次 * * 使用 RCU vs 读写锁的实测对比4 核 Cortex-A76 * 1000 万次读 1 次写 * - rwlock: 总耗时 245ms * - RCU: 总耗时 23ms (快 10.7×) */ #include linux/rcupdate.h #include linux/slab.h struct ai_config { float confidence_threshold; /* 置信度阈值 */ int max_detections; /* 最大检测框数 */ char class_map[256][32]; /* 类别 ID → 名称映射 */ struct rcu_head rcu; /* RCU 回收头 */ }; static struct ai_config __rcu *g_config NULL; /* * 读者 —— 高频调用每帧推理零锁开销 * * 关键点 * - rcu_dereference() 读取指针编译器屏障保证读取原子性 * - 在 rcu_read_lock/unlock 之间保证旧数据不被回收 */ int ai_get_confidence_threshold(float *threshold) { struct ai_config *cfg; rcu_read_lock(); /* 进入 RCU 读临界区 —— 极轻量仅禁用抢占 */ cfg rcu_dereference(g_config); /* 安全读取共享指针 */ if (!cfg) { rcu_read_unlock(); return -ENODATA; /* 数据不可用 —— 配置尚未加载 */ } *threshold cfg-confidence_threshold; rcu_read_unlock(); /* 退出 RCU 读临界区 */ return 0; } /* * 写者 —— 低频调用配置更新 * * 核心流程 * 1. kmalloc 分配新副本 * 2. 复制旧数据或填充新数据 * 3. rcu_assign_pointer 原子更新指针 * 4. synchronize_rcu 等待所有读者退出 * 5. kfree 释放旧副本 */ int ai_config_update(const struct ai_config *new_cfg) { struct ai_config *old_cfg; struct ai_config *new_copy; /* 分配新副本 */ new_copy kmalloc(sizeof(*new_copy), GFP_KERNEL); if (!new_copy) { return -ENOMEM; } /* 复制新配置 */ memcpy(new_copy, new_cfg, sizeof(*new_copy)); /* 原子更新指针 —— 此后新建的读者将看到新配置 */ old_cfg rcu_dereference(g_config); rcu_assign_pointer(g_config, new_copy); /* * synchronize_rcu等待所有活跃的 RCU 读者退出 * - 阻塞调用但不对读者产生任何性能影响 * - 在嵌入式系统中通常耗时 1-20ms取决于 CONFIG_RCU_* 配置 */ synchronize_rcu(); /* ● 安全释放旧数据 —— 所有读者都已退出 */ if (old_cfg) { kfree(old_cfg); } return 0; }2.4 信号量semaphore信号量支持大于 1 的计数值适用于资源池限流场景。/* * 信号量使用范式 —— AI 推理资源池限流 * * NPU 有 N 个独立的推理通道使用计数信号量count N * 防止推理请求超过硬件通道数量导致排队延迟不可控 */ #include linux/semaphore.h #define NPU_MAX_CONCURRENT 4 /* NPU 最多支持 4 路并发推理 */ static DEFINE_SEMAPHORE(npu_sem, NPU_MAX_CONCURRENT); int npu_submit_inference(const struct inference_request *req) { /* * down_interruptible尝试获取信号量 * - 如果计数值 0减 1立即返回 0 * - 如果计数值 0当前进程睡眠等待直到有通道释放 * - 如果等待期间收到信号返回 -EINTR可取消 */ if (down_interruptible(npu_sem)) { return -EINTR; /* 用户在等待期间取消了请求 */ } /* 获取到 NPU 通道 —— 执行推理 */ int ret npu_do_inference(req); if (ret 0) { /* 推理失败记录错误类型 */ pr_warn(NPU inference failed with error %d\n, ret); } up(npu_sem); /* 释放通道 —— 计数值 1唤醒等待队列中的下一个请求 */ return ret; }三、同步机制选型决策图四、内核态同步机制速查表机制上下文可睡眠开销适用场景典型持有时间spinlock中断/进程禁止CPU 忙等中断与进程共享数据5-500 nsmutex仅进程允许上下文切换长临界区、可能睡眠10μs - 100mssemaphore仅进程允许上下文切换资源池、限流不限RCU仅进程禁止读读零锁、写有开销高频读、低频写读无限、写 ms 级rwlock中断/进程禁止spin/允许sem中等读多写少但不可睡眠同 spinlockatomic_t任意禁止最低单变量原子操作N/Acompletion仅进程允许上下文切换一次性通知N/A结论Linux 内核同步机制选型的核心原则用一句话概括在能睡眠的地方用 mutex/semaphoreCPU 友好在不能睡眠的地方用 spinlock唯一选择在读占绝对主导的场景用 RCU性能最优。对于嵌入式 AI 场景的特殊建议NPU 推理任务的提交队列使用mutexwait_queue组合。推理耗时 10-200msmutex 是最佳选择。传感器数据 DMA 缓冲区使用spin_lock_irqsave。这是典型的中断/进程共享数据场景自旋锁是唯一正确选择。模型配置热更新使用 RCU。每帧推理需要读取配置更新频率极低小时/天级别。多进程共享 NPU使用semaphore(countN)。NPU 有 N 个独立通道信号量最自然地表达资源池语义。同步机制选错不是跑得慢一点的问题——在最坏情况下是内核崩溃scheduling while atomic或死锁中断嵌套获取同一把锁。这是嵌入式内核开发中为数不多的必须 100% 正确的领域。