Linux驱动编程:内核同步的艺术-从互斥到底半部

目录

一、等待的艺术------互斥锁(Mutex)与自旋锁(Spinlock)

[1. 互斥锁 (Mutex)](#1. 互斥锁 (Mutex))

[2. 自旋锁 (Spinlock)](#2. 自旋锁 (Spinlock))

3.对比表格

4.如何选择

5.总结

二、中断环境下的同步挑战

[1、 共享资源的竞争与保护](#1、 共享资源的竞争与保护)

[1.1. 开关中断(最底层的保护)](#1.1. 开关中断(最底层的保护))

[1.2. 自旋锁(Spinlock)](#1.2. 自旋锁(Spinlock))

[1.3. 自旋锁 + 开关中断(多核场景)](#1.3. 自旋锁 + 开关中断(多核场景))

[2、 中断子系统与底半部](#2、 中断子系统与底半部)

[2.1. 中断的拆分:顶半部 vs 底半部](#2.1. 中断的拆分:顶半部 vs 底半部)

[2.2. 底半部的实现机制对比](#2.2. 底半部的实现机制对比)

[2.3. 关键函数与概念](#2.3. 关键函数与概念)

2.4.总结


一、等待的艺术------互斥锁(Mutex)与自旋锁(Spinlock)

应用场景:用于在多线程或中断环境中保护共享资源,防止数据竞争。它们最核心的区别在于当锁被占用时,等待锁的线程/进程的行为方式不同

1. 互斥锁 (Mutex)

核心行为:睡眠等待

当线程尝试获取一个已被占用的互斥锁时,该线程会被操作系统挂起(进入睡眠状态),并放入等待队列。直到锁被释放时,操作系统才会唤醒其中一个等待线程。

  • 适用场景 :锁可能被持有较长时间(例如,执行复杂的文件操作、网络请求)。

  • 优点:等待期间不占用CPU,节省资源。

  • 缺点:线程切换(睡眠和唤醒)有较大的开销。

  • 类比 :你在银行柜台前排队,发现前面有人正在办理复杂的业务。你选择取个号,然后坐在旁边的椅子上睡觉(睡眠)。等叫到你的号时,工作人员再把你叫醒。

2. 自旋锁 (Spinlock)

核心行为:忙等待

当线程尝试获取一个已被占用的自旋锁时,该线程会在一个循环中不断检查锁的状态("自旋"),直到锁被释放。在此期间,线程始终处于运行状态,持续占用CPU。

  • 适用场景 :锁被持有的时间极短(例如,仅修改一个指针或一个整数),且不允许睡眠的上下文(如中断处理程序)。

  • 优点:避免了线程切换的开销,响应速度极快。

  • 缺点:在锁被长期占有时,会白白浪费CPU周期。

  • 类比 :你在超市收银台前排队,前面的人只是忘记拿一个商品,马上回来。你选择站在原地,不停地探头看他回来了没有(自旋),而不是离开队伍。

3.对比表格

特性 互斥锁 (Mutex) 自旋锁 (Spinlock)
等待机制 睡眠等待。线程被挂起,不占用CPU。 忙等待(自旋)。线程持续运行并检查锁状态。
开销 上下文切换开销大(睡眠、唤醒操作)。 无上下文切换开销,但空转消耗CPU。
持有时间 适用于锁持有时间较长的场景(>上下文切换时间)。 适用于锁持有时间极短的场景(<上下文切换时间)。
可睡眠性 可以睡眠。获取锁后,线程允许被调度出去。 禁止睡眠。持有自旋锁的线程必须尽快完成操作并释放锁。
使用上下文 可用于进程上下文(如用户态线程)。 主要用于中断上下文、内核底层代码等不允许睡眠的场景。
实现级别 通常由操作系统内核提供调度支持 通常依赖CPU的原子指令(如CAS, TAS)实现。
可能导致的问题 死锁、优先级反转。 CPU使用率飙升、死锁(尤其在单核上需禁用中断)。

4.如何选择

记住一个简单的原则:"让等待者睡觉,除非你确信它很快就能醒来"

  1. 用互斥锁,如果

    • 你在用户态编程。

    • 锁被持有的时间不确定或可能较长

    • 可以接受睡眠(即不在中断处理程序、原子上下文中)。

  2. 用自旋锁,如果

    • 你在内核态或中断上下文中编程(这些地方不能睡眠)。

    • 100%确定 锁只会被持有极短的时间(比如几条指令)。

    • 你追求极致的性能 ,且锁竞争不激烈。

5.总结

互斥锁是"等不了就睡",自旋锁是"死等到底" 。选择哪一个,取决于你愿意用"切换开销"换"CPU时间",还是反过来。

二、中断环境下的同步挑战

在嵌入式 Linux 驱动开发中,并发(Concurrency) ​ 和 **中断(Interrupt)**​ 是两个最核心的概念。当多个执行单元(进程、线程、中断)同时访问共享资源时,如果没有妥善的同步机制,就会引发不可预知的"竞态条件"。

结合学习笔记,以下将从资源竞争处理中断子系统 两个方面,梳理 Linux 内核是如何优雅地解决这些问题的。

1、 共享资源的竞争与保护

在多核 CPU 或单核开启了抢占的环境下,内核开发者必须考虑对共享数据(如全局变量、硬件寄存器)的保护。

1.1. 开关中断(最底层的保护)

这是最快、最直接的手段,主要用于单核处理器环境下的临界区保护。

  • local_irq_enable/ local_irq_disable :直接开启或关闭本地 CPU 的中断。简单粗暴,但如果在关闭期间发生中断,中断将永远丢失(直到重新开启),因此通常只在极短时间内使用。

  • local_irq_save/ local_irq_restore :保存当前的中断状态并关闭中断,恢复时还原之前的状态。这是更安全的做法,常用于嵌套临界区。

1.2. 自旋锁(Spinlock)

当临界区代码执行时间较短时,如果让线程去"睡眠"(如互斥锁那样),线程切换的开销反而比执行代码本身还大。这时候就需要自旋锁。

  • 原理:当获取不到锁时,线程会在原地循环(Busy-waiting),不断尝试获取锁,就像一个人守着门口反复确认"好了吗?好了吗?"。

  • 特点忙等。线程不会让出 CPU,响应极快,但会消耗 CPU 资源。

  • 适用场景:临界区极小,且不能发生进程调度或睡眠的场合。

1.3. 自旋锁 + 开关中断(多核场景)

在多核系统中,单纯的关中断只能保护自己所在的核。如果多个核之间也会竞争同一个变量,就需要结合自旋锁。

  • spin_lock_irqsave/ spin_unlock_irqrestore:这是最常见的组合。它不仅锁定自旋锁,还关闭中断。

  • 作用 :既防止了多核之间的并发,又防止了单核上的中断打断当前的临界区操作。这是驱动开发中最常用的锁机制之一。


2、 中断子系统与底半部

Linux 内核将中断的处理分为上下两部分,以兼顾中断响应的实时性和处理任务的复杂度。

2.1. 中断的拆分:顶半部 vs 底半部

当一个硬件中断发生时,内核的执行流程如下:

  • 顶半部(Top Half / 上半部)"必要操作"

    • 这是中断触发后立刻执行的代码。

    • 必须快速完成,主要任务是登记中断、清除中断标志位、读取关键状态。

    • 限制不允许睡眠、不允许调度、不允许阻塞

  • 底半部(Bottom Half / 下半部)"费时操作"

    • 将顶半部中耗时的、非紧急的操作推迟到底半部执行。

    • 特点:可以睡眠、可以延时、可以被调度。

2.2. 底半部的实现机制对比

底半部主要有两种实现方式,它们最大的区别在于执行上下文

  • Tasklet(小任务)

    • 上下文中断上下文

    • 特点 :执行时机不确定(通常是最近的可调度时机),但在中断上下文中运行意味着它依然不能睡眠、不能调度

    • 适用性 :适用于耗时较短,且绝对不能睡眠的任务。

  • Workqueue(工作队列)

    • 上下文进程上下文

    • 特点 :Workqueue 是将任务交给内核线程(kworker)去执行。因为在进程上下文中,它可以睡眠、可以延时、可以被更高优先级的进程抢占

    • 适用性 :适用于耗时较长,或者可能需要调用可能导致睡眠的函数(如申请内存、文件读写)的任务。

2.3. 关键函数与概念

  • request_irq :用于向内核注册一个中断处理函数(通常就是顶半部)。

  • 中断上下文 (Interrupt Context):执行中断服务程序的过程。此时 CPU 正在处理硬件事件,没有对应的进程 PCB,因此不能调度其他进程。

  • 进程上下文 (Process Context):普通进程或内核线程的运行状态。此时 CPU 正在执行某个进程的指令,可以进行任务切换。


2.4.总结

在实际的驱动开发中,我们需要根据场景选择合适的内核机制:

  1. 防并发 :如果只是简单修改变量,考虑原子操作;如果需要保护大段代码,根据是否关中断的需求选择自旋锁。

  2. 处理中断 :紧急事情放顶半部 (硬中断),耗时处理放底半部 。如果底半部需要睡眠,果断选择 Workqueue ;如果必须马上执行且不能睡,选择 Tasklet

相关推荐
他是龙5511 小时前
DVWA SQL 注入全级别通关笔记(Low / Medium / High / Impossible)
数据库·笔记·sql
山甫aa1 小时前
多叉树定义与遍历-----从零开始的数据结构
开发语言·c++·二叉树·多叉树
永远睡不够的入1 小时前
C++11新特性(2):深入 C++ 参数传递黑盒:从引用折叠到完美转发,再到可变参数模板
开发语言·c++
idolao1 小时前
CentOS 7 安装 jprofiler_linux64_7_2_3.tar.gz 详细步骤(解压、配置、远程连接)
linux·python·centos
qq_206901392 小时前
如何创建CDB公共用户_C##前缀强制规则与CONTAINER=ALL.txt
jvm·数据库·python
无限进步_2 小时前
【C++】寻找数组中出现次数超过一半的数字:三种解法深度剖析
开发语言·c++·git·算法·leetcode·github·visual studio
深邃-2 小时前
【Web安全】-Kali,Linux配置(1):Kali网络配置,LinuxEnvConfig配置脚本,APT源的讲解,Kali设置中文
linux·运维·开发语言·网络·安全·web安全·网络安全
code bean2 小时前
MySQL 远程访问实战:从基础操作到真实踩坑记录
数据库·mysql
江山与紫云2 小时前
告别重复造轮子:Codex写脚本
开发语言·python