资讯动态

【linux内核专栏 07】同步与并发

发布时间:2026/10/15 12:29:09 来源:尧图企业网站定制
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 对照 FreeRTOSFreeRTOS(单核)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 为什么要存 flagsunsignedlongflags;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 semaphoremutex 是二值信号量的特化,但mutex 有优先级继承,semaphore 没有mutex 强调谁加锁谁解锁(同上下文),semaphore 可跨上下文(up 和 down 不必同人)新代码互斥用 mutex,信号量用于资源计数(如限制同时 N 个访问)嵌入式视角:对照 FreeRTOS 信号量FreeRTOS 二值/计数信号量,Linuxsemaphore概念同。但Linux mutex 比 semaphore 更适合互斥(优先级继承/debug/约束)。FreeRTOS 习惯信号量做互斥,Linux 改用 mutex。五、原子操作(atomic_t)5.1 单变量原子操作atomic_tcounterATOMIC_INIT(0);atomic_inc(counter);// counter(原子)atomic_dec(counter);// --counteratomic_add(5,counter);// 5atomic_sub(3,counter);intvatomic_read(counter);// 读atomic_set(counter,10);// 写// 条件操作if(atomic_dec_and_test(counter)){// --,测试是否 0// counter 归 0}// CAS(Compare-And-Swap)intoldatomic_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*prcu_dereference(global_ptr);// 读(无锁)use(p-field);// 改structdata*newcreate_new();rcu_assign_pointer(global_ptr,new);// 原子替换synchronize_rcu();// 等旧读者退出kfree(old);// 安全释放6.3 RCU APIAPI用途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();prcu_dereference(gptr);// 无锁,只防编译器重排do_something(p);rcu_read_unlock();读侧不持锁,不睡(读临界区不能睡,否则 grace period 拖久)多核读并发,零开销6.5 RCU 写侧newkmalloc(...);*new*old;// 复制new-fieldnewval;// 改副本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 静态 percpustaticDEFINE_PER_CPU(int,my_counter);// 静态定义// 本核访问this_cpu_inc(my_counter);// 本核 intvthis_cpu_read(my_counter);// 读本核// 所有核求和inttotal0;intcpu;for_each_possible_cpu(cpu)totalper_cpu(my_counter,cpu);7.3 动态 percpuint*palloc_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/编译器可能重排内存访问。多核同步场景,重排会破坏逻辑。// 生产者data42;flag1;// 期望 data 先于 flag// 无屏障,硬件可能先写 flag 再写 data,消费者看到 flag1 但 data 旧9.2 Linux 内存屏障 API屏障作用smp_mb()全内存屏障(读写都保序)smp_rmb()读屏障(读不跨屏障)smp_wmb()写屏障(写不跨屏障)smp_store_release(p, v)release 写(之前操作先于此)smp_load_acquire(p)acquire 读(此操作先于之后)barrier()编译器屏障(只防编译器重排,不防 CPU)// 用屏障修正上面data42;smp_wmb();// 写屏障:保证 data 写先于 flagflag1;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*darg;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:计数,可睡,资源计数用;互斥改用 mutexatomic_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技术之路漫漫分享是为了更好地交流。如果本文的内容对你有启发希望能得到你的点赞 和收藏 ⭐。如果你在调试过程中遇到了其他问题欢迎在评论区 留言我们一起探讨。也欢迎关注 我一起交流底层开发的那些事儿。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑