☰
【linux内核专栏 07】同步与并发
2026/10/15 12:29:04 网站建设 项目流程

📚 Linux 内核专栏 · 持续更新中

本系列从底层原理到源码实战,逐篇拆解内核机制。

👉 想系统追更,戳这里查看完整目录:
【Linux 内核专栏 00】总目录


本篇定位:Linux 并发比裸机复杂百倍——SMP 多核 + 抢占 + 中断 + 软中断同时存在,共享数据不加保护 = 竞态 = 灵异 bug。 本篇把"Linux 同步"对你已知的(如FreeRTOS)增量讲透:多核要自旋锁(不是信号量)、关抢占/关中断的组合、RCU 独门、percpu 免锁、内存屏障。读完能选对同步原语、能读懂 RCU、能定位死锁/竞态。

FreeRTOS 同步是单核版,Linux 是多核版,核心增量是"自旋锁"
FreeRTOS 信号量/互斥:单核,临界区靠"关中断"实现。Linux 多核(SMP),关一个核的中断没用,另一个核照样访问——多核要"自旋锁"(其他核忙等)。这是最大转折。另外 Linux 有抢占/软中断/硬中断多套上下文,每套有对应的"关"组合。理解"自旋锁 + 上下文组合",就理解 Linux 同步大半。


一、Linux 并发的来源(为什么比裸机复杂)

Linux 共享数据可能被以下并发访问:

并发源说明
SMP 多核多核同时跑,访问同一数据
抢占内核可抢占,任务 A 执行中被 B 抢占
硬中断随时打断当前代码
软中断(softirq/tasklet)中断返回时执行,打断普通代码
用户进程syscall多进程通过 syscall 进内核,并发

任一共享数据,只要可能被上述任两者并发访问,就要保护。

1.1 临界区与竞态

  • 临界区(critical section):访问共享数据的代码段
  • 竞态(race condition):并发访问导致结果依赖时序
  • 竞态症状:数据错乱/链表破坏/计数器错/偶发死机

1.2 对照 FreeRTOS

FreeRTOS(单核)Linux(SMP+抢占)
并发源任务切换 + 中断多核 + 抢占 + 中断 + 软中断
临界区关中断(taskENTER_CRITICAL)自旋锁(多核)/关抢占(单核上下文)
信号量阻塞等(切其他任务)信号量(可睡)/自旋锁(忙等)
互斥mutex(优先级继承)mutex(可睡,优先级继承)

二、自旋锁(spinlock)⭐ 核心

2.1 什么是自旋锁

  • 多核同步原语
  • 锁被占时,其他核忙等(循环测试锁,不睡)
  • 持锁期间不能睡(忙等 + 睡 = 死锁,因为睡切走,锁不释放,其他核永远等)

2.2 基本用法

spinlock_tmy_lock;spin_lock_init(&my_lock);// 临界区spin_lock(&my_lock);// 访问共享数据(不能睡!)shared_data++;spin_unlock(&my_lock);

2.3 自旋锁的变种(关中断/关软中断/关抢占)

自旋锁本身防多核并发,但还要防"本核中断/软中断打断":

变种关什么场景
spin_lock()关抢占(SMP)+ 持锁普通上下文(进程上下文)
spin_lock_irqsave()关抢占 +关本核硬中断共享数据会被硬中断访问
spin_lock_bh()关抢占 +关本核软中断共享数据会被 softirq/tasklet 访问
spin_lock_irq()关抢占 + 关中断(已知中断状态)确知中断已开

2.4 怎么选

共享数据会被硬中断访问? 是 ──> spin_lock_irqsave / spin_lock_irq 否 ──> 会被软中断(softirq/tasklet)访问? 是 ──> spin_lock_bh 否 ──> spin_lock(普通,进程上下文)

2.5 irqsave 为什么要存 flags

unsignedlongflags;spin_lock_irqsave(&lock,flags);// 临界区spin_unlock_irqrestore(&lock,flags);
  • irqsave保存当前中断状态到 flags(可能已关中断的嵌套场景)
  • irqrestore恢复原状态
  • 防嵌套关中断时把"本来关着的"开成"开着"

2.6 持自旋锁的禁忌

  • ❌不能睡(msleep/mutex/kmalloc GFP_KERNEL/schedule)
  • ❌ 不能持锁太久(忙等浪费 CPU,影响实时性)
  • ❌ 不能递归持同锁(死锁)
  • ❌ 不能持锁时调可能睡的函数(哪怕不明显,如 copy_from_user 可能缺页睡)

持自旋锁不能睡是铁律
这是 Linux 比 FreeRTOS 严格的点。FreeRTOS 临界区(关中断)里能调阻塞 API(切其他任务)。Linux 持自旋锁绝对不能睡——睡了 schedule 切走,锁不释放,其他核忙等死锁。你写驱动持锁的临界区,反复检查每行不能有睡的可能。


三、互斥锁(mutex,可睡)

3.1 什么时候用 mutex

  • 临界区需要睡(等 IO/分配 GFP_KERNEL/调可能睡的函数)
  • 临界区较长(自旋锁忙等浪费,mutex 让等待者睡)

3.2 用法

structmutexmy_mutex;mutex_init(&my_mutex);mutex_lock(&my_mutex);// 临界区(可睡!)process_data();// 内部可 msleep/kmalloc GFP_KERNELmutex_unlock(&my_mutex);
  • mutex_lock:拿不到就睡,等解锁唤醒
  • mutex_unlock:唤醒一个等待者
  • 持 mutex 时可睡(和自旋锁最大区别)

3.3 mutex 的特性

  • 优先级继承:高优先级等 mutex 时,持有者优先级提升到等待者级别(防优先级反转)
  • 只能进程上下文用(不能在中断/softirq 里,因为它们不能睡)
  • debug 友好:CONFIG_MUTEX_DEBUG 检测死锁/未释放

3.4 mutex vs spinlock 决策

维度spinlockmutex
等待方式忙等睡眠
持锁能睡❌✅
用在哪任何上下文(含中断)仅进程上下文
临界区短长
多核必备可用
优先级继承❌✅

[!important] 选 spinlock 还是 mutex

  • 临界区短 + 不能睡(中断上下文/数据结构操作)→spinlock
  • 临界区长或要睡(等 IO/分配/用户交互)→mutex
  • 能睡就用 mutex(省 CPU),不能睡才自旋

四、信号量(semaphore)

4.1 信号量是什么

  • 计数同步原语
  • down()减 1,为 0 则睡;up()加 1,唤醒等待者
  • 可用于互斥(初值 1)或资源计数(初值 N)

4.2 用法

structsemaphoresem;sema_init(&sem,1);// 二值(互斥)down(&sem);// 拿,拿不到睡(可睡)// 临界区up(&sem);// 释放// 变种down_interruptible(&sem);// 可被信号打断(返回 -ERESTARTSYS)down_trylock(&sem);// 非阻塞,拿不到立即返回down_timeout(&sem,HZ);// 超时

4.3 mutex vs semaphore

  • mutex 是"二值信号量"的特化,但mutex 有优先级继承,semaphore 没有
  • mutex 强调"谁加锁谁解锁"(同上下文),semaphore 可跨上下文(up 和 down 不必同人)
  • 新代码互斥用 mutex,信号量用于"资源计数"(如限制同时 N 个访问)

嵌入式视角:对照 FreeRTOS 信号量
FreeRTOS 二值/计数信号量,Linuxsemaphore概念同。但Linux mutex 比 semaphore 更适合互斥(优先级继承/debug/约束)。FreeRTOS 习惯信号量做互斥,Linux 改用 mutex。


五、原子操作(atomic_t)

5.1 单变量原子操作

atomic_tcounter=ATOMIC_INIT(0);atomic_inc(&counter);// ++counter(原子)atomic_dec(&counter);// --counteratomic_add(5,&counter);// += 5atomic_sub(3,&counter);intv=atomic_read(&counter);// 读atomic_set(&counter,10);// 写// 条件操作if(atomic_dec_and_test(&counter)){// --,测试是否 0// counter 归 0}// CAS(Compare-And-Swap)intold=atomic_cmpxchg(&counter,5,10);// 若==5 则设 10,返回旧值

5.2 64 位原子

atomic64_tbig;atomic64_inc(&big);

5.3 位原子

set_bit(5,&flags);// 置位clear_bit(5,&flags);test_bit(5,&flags);test_and_set_bit(5,&flags);// 测试并置位(原子)

5.4 何时用原子

  • 单变量操作(计数器/标志位),不需要锁
  • 比锁快(无锁,靠原子指令如 RISC-Vamoadd.w)

原子操作用 RISC-V amo 指令
[[08-扩展生态与 march-mabi 匹配]] 学的 A 扩展(amoadd.w/amoswap.w)——Linux atomic_t 在 RISC-V 上就靠这些实现。atomic_inc→amoadd.w。你已经懂底层,这里看上层 API。


六、RCU(Read-Copy-Update)⭐ Linux 独门

6.1 RCU 是什么

  • 读无锁(读者不加锁,极快)
  • 写者复制:写时复制一份,改副本,再原子替换指针
  • 延迟回收:等所有"旧读者"退出,才释放旧数据

适合读多写少的数据(路由表/设备列表/配置)。

6.2 RCU 基本模式

structdata*p=rcu_dereference(global_ptr);// 读(无锁)use(p->field);// 改structdata*new=create_new();rcu_assign_pointer(global_ptr,new);// 原子替换synchronize_rcu();// 等旧读者退出kfree(old);// 安全释放

6.3 RCU API

API用途
rcu_read_lock()/rcu_read_unlock()进入/退出 RCU 读临界区
rcu_dereference(p)读 RCU 保护指针
rcu_assign_pointer(p, v)写 RCU 保护指针
synchronize_rcu()等所有旧读者退出(阻塞)
call_rcu(callback)异步等退出后调 callback(不阻塞)
list_for_each_entry_rcuRCU 遍历链表
list_add_rcu/list_del_rcuRCU 链表操作

6.4 RCU 读侧极快

rcu_read_lock();p=rcu_dereference(gptr);// 无锁,只防编译器重排do_something(p);rcu_read_unlock();
  • 读侧不持锁,不睡(读临界区不能睡,否则 grace period 拖久)
  • 多核读并发,零开销

6.5 RCU 写侧

new=kmalloc(...);*new=*old;// 复制new->field=newval;// 改副本rcu_assign_pointer(gptr,new);// 原子换指针synchronize_rcu();// 等旧读者kfree(old);// 释放
  • 写者开销大(复制 + 等 grace period)
  • 但读者极快,适合读多写少

RCU 是 Linux 性能法宝
FreeRTOS 无此物(单核不需要)。Linux 多核服务器,路由表/进程列表/设备列表读极频繁,用自旋锁读会堵。RCU 让读无锁,大幅提速。你读内核代码(尤其 net/fs),RCU 满地都是。你写驱动读多写少的数据(如配置),考虑 RCU。


七、percpu 变量(免锁)

7.1 percpu 是什么

  • 每个 CPU 一份变量副本
  • 各核访问自己的副本,无竞争,免锁
  • 适合"每核统计/计数器"

7.2 静态 percpu

staticDEFINE_PER_CPU(int,my_counter);// 静态定义// 本核访问this_cpu_inc(my_counter);// 本核 ++intv=this_cpu_read(my_counter);// 读本核// 所有核求和inttotal=0;intcpu;for_each_possible_cpu(cpu)total+=per_cpu(my_counter,cpu);

7.3 动态 percpu

int*p=alloc_percpu(int);this_cpu_inc(*p);free_percpu(p);

7.4 何时用 percpu

  • 计数器/统计(每核独立累加,要总时汇总)
  • 避免高频计数器的锁竞争

嵌入式视角:percpu 是多核免锁利器
FreeRTOS 单核无此概念。Linux 多核,高频计数器(如网络包计数)用自旋锁会堵,用 percpu 每核独立累加,汇总时才读全部——免锁且快。你写多核驱动高频统计,优先 percpu。


八、关抢占 / 关中断

8.1 关抢占

preempt_disable();// 本核不会切走(但中断还来)preempt_enable();// 重新检查 need_resched
  • 防本核被抢占(单核上保护数据)
  • 不能睡(睡了 schedule 切走,但关了抢占切不回)
  • 持自旋锁隐含关抢占

8.2 关中断

unsignedlongflags;local_irq_save(flags);// 关本核中断,保存状态// 临界区(本核中断不来)local_irq_restore(flags);// 或已知状态local_irq_disable();local_irq_enable();
  • 最强保护(本核绝对安静)
  • 影响实时性(中断延迟),慎用长临界区

8.3 关软中断

local_bh_disable();// 关本核软中断(softirq/tasklet)// 临界区local_bh_enable();
  • 防软中断打断
  • spin_lock_bh隐含此操作

8.4 组合决策表

保护需求用什么
防本核抢占(进程上下文短临界区)preempt_disable 或 spin_lock
防本核硬中断local_irq_save 或 spin_lock_irqsave
防本核软中断local_bh_disable 或 spin_lock_bh
防多核并发spin_lock(任何变种)
全防护(多核+中断)spin_lock_irqsave

九、内存屏障(Memory Barrier)

9.1 为什么需要屏障

你 [[06b-内存架构深挖 PMA-ePMP-Cache-CMO-MMU-RVWMO]] 学过 RVWMO 弱内存模型——CPU/编译器可能重排内存访问。多核同步场景,重排会破坏逻辑。

// 生产者data=42;flag=1;// 期望 data 先于 flag// 无屏障,硬件可能先写 flag 再写 data,消费者看到 flag=1 但 data 旧

9.2 Linux 内存屏障 API

屏障作用
smp_mb()全内存屏障(读写都保序)
smp_rmb()读屏障(读不跨屏障)
smp_wmb()写屏障(写不跨屏障)
smp_store_release(p, v)release 写(之前操作先于此)
smp_load_acquire(p)acquire 读(此操作先于之后)
barrier()编译器屏障(只防编译器重排,不防 CPU)
// 用屏障修正上面data=42;smp_wmb();// 写屏障:保证 data 写先于 flagflag=1;

9.3 何时用

  • 自定义同步原语(不靠锁,自己实现发布-等待)
  • 无锁数据结构
  • 大部分情况:用锁/RCU/atomic 就隐含了正确屏障,不用手动加

优先用现成原语,少手动加屏障
自旋锁/mutex/atomic/RCU 内部已加正确屏障。你写驱动 99% 不用手动 smp_mb——用现成原语即可。只在写无锁数据结构/性能极致优化时才手动加,且要极其小心。你 06b RVWMO 知识在这里是"理解为什么需要",不是"天天用"。


十、completion(完成量)

10.1 什么时候用

  • 等一次性事件(如线程启动完成、初始化完成)
  • 比 semaphore 更语义化
structcompletiondone;init_completion(&done);// 等待方wait_for_completion(&done);// 阻塞等// 完成方complete(&done);// 唤醒等待者

10.2 典型场景

// 内核线程启动后等structcompletionstartup_done;init_completion(&startup_done);kthread_run(thread_fn,&startup_done,"my");wait_for_completion(&startup_done);// 等线程就绪// 线程里intthread_fn(void*arg){structcompletion*d=arg;init_my_stuff();complete(d);// 通知主线程我好了...}

十一、死锁与调试

11.1 死锁类型

死锁原因
自递归同核持锁后又去取同锁
AB-BA核A持锁1等锁2,核B持锁2等锁1
持锁睡持自旋锁睡(schedule 切走,锁不释放)
中断里持锁中断打断持锁者,中断里又取同锁

11.2 lockdep(锁依赖检测)

# 内核配置 CONFIG_LOCKDEP# 开启后,内核跟踪所有锁的获取顺序,检测潜在死锁dmesg|grep-ilockdep# 报告:# [ BUG: possible deadlock ] 潜在 AB-BA# [ BUG: sleeping in atomic context ] 原子上下文睡了

11.3 调竞态

  • 开 CONFIG_DEBUG_ATOMIC_SLEEP(检测原子上下文睡)
  • 开 CONFIG_PROVE_LOCKING(lockdep)
  • 用 KASAN(内存竞态检测)
  • stress test + ftrace 跟踪

嵌入式视角:lockdep 是你调试利器
调 FreeRTOS 死锁靠肉眼 + 经验。Linuxlockdep 自动检测潜在死锁——开发时开 lockdep 跑一遍,大部分死锁自动暴露。你 BSP 开发驱动,务必开 lockdep 跑测试,省下大量调试时间。


十二、同步原语选择决策树

要睡吗? 是 ──> mutex(进程上下文,优先级继承) 否 ──> 临界区多长? 短(<几行)──> 看并发源 多核/中断/软中断? 多核 ──> spinlock 仅本核抢占 ──> preempt_disable 中断会访问 ──> spin_lock_irqsave 软中断会访问 ──> spin_lock_bh 长/读多写少 ──> RCU(读无锁) 单变量? ──> atomic_t / bit ops 每核统计? ──> percpu 等一次性事件? ──> completion

十三、对照总表

概念FreeRTOSRISC-V bare-metalLinux
临界区taskENTER_CRITICAL(关中断)关中断spinlock/preempt_disable
互斥mutex(优先级继承)关中断mutex(可睡)
信号量二值/计数你写semaphore(可睡)
自旋锁N/A(单核)N/Aspinlock(多核忙等)
原子__atomicamoadd.watomic_t(amoadd.w)
RCUN/AN/ARCU(读无锁)
percpuN/AN/Apercpu(每核一份)
持锁能睡临界区能睡关中断不能自旋不能,mutex 能
死锁检测无无lockdep

十四、本篇小结

  • Linux 并发源:SMP 多核 + 抢占 + 硬中断 + 软中断,任一共享数据可能被并发访问
  • 自旋锁:多核忙等,持锁不能睡;变种spin_lock_irqsave(关中断)/_bh(关软中断)
  • mutex:可睡,优先级继承,仅进程上下文用,长临界区
  • semaphore:计数,可睡,资源计数用;互斥改用 mutex
  • atomic_t:单变量原子,靠 amo 指令
  • RCU:读无锁,写复制延迟回收,读多写少法宝
  • percpu:每核一份,免锁统计
  • 关抢占(preempt_disable)/关中断(local_irq_save)/关软中断(local_bh_disable)组合
  • 内存屏障smp_mb/rmb/wmb:防重排,优先用现成原语(隐含屏障)
  • completion:等一次性事件
  • 死锁:lockdep 自动检测;开发必开
  • 持自旋锁/原子上下文绝对不能睡

速查表

场景用什么
短临界区不能睡,多核spin_lock
短临界区,会被中断访问spin_lock_irqsave
短临界区,会被软中断访问spin_lock_bh
长临界区或要睡mutex
单变量计数atomic_inc/atomic_add
读多写少RCU(rcu_read_lock + rcu_dereference)
每核统计percpu(this_cpu_inc)
等一次性事件completion
关本核抢占preempt_disable(不能睡)
关本核中断local_irq_save
关本核软中断local_bh_disable
检测死锁lockdep(CONFIG_LOCKDEP)
检测原子上下文睡CONFIG_DEBUG_ATOMIC_SLEEP

💡技术之路漫漫,分享是为了更好地交流。如果本文的内容对你有启发,希望能得到你的点赞 👍和收藏 ⭐。

如果你在调试过程中遇到了其他问题,欢迎在评论区 💬留言,我们一起探讨。也欢迎关注 👀我,一起交流底层开发的那些事儿。


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询