当前位置: 首页 > news >正文

[特殊字符]‍[特殊字符]Linux驱动开发入门 | 并发与互斥机制详解

文章目录

  • 👨‍💻Linux驱动开发入门 | 并发与互斥机制详解
    • 📌为什么驱动中需要并发和互斥控制?
    • 💡常见的并发控制机制
    • 🔐自旋锁和信号量通俗理解
      • 🌀自旋锁(Spinlock)——“厕所排队锁”
      • 🚦信号量(Semaphore)——“停车场智能显示器”
    • 🆚 自旋锁 vs 信号量
    • 读写锁是什么?
    • 🚨死锁问题和解决策略
      • 一、预防死锁
        • 1. 资源一次性分配(破坏请求与保持条件)
        • 2. 可剥夺资源(破坏不可剥夺条件)
        • 3. 资源有序分配法(破坏循环等待条件)
      • 二、避免死锁
        • 银行家算法
      • 三、死锁检测
        • 步骤如下:
      • 四、解除死锁
        • 1. 剥夺资源
        • 2. 撤消进程
      • 五、避免死锁的编程实践
        • 1. 加锁顺序(Lock Ordering)
        • 2. 加锁时限(Try Lock with Timeout)
        • 3. 死锁检测机制
      • 总结
    • 🧪真实驱动例子:互斥访问设备寄存器
      • 临界区:
    • 🧠Q&A 常见问题
    • ✅总结

👨‍💻Linux驱动开发入门 | 并发与互斥机制详解

📌为什么驱动中需要并发和互斥控制?

我们知道,在多线程或多任务并行执行的操作系统中,比如Linux内核,多个执行单元(线程或中断)可能同时访问共享资源(如全局变量、设备寄存器、缓冲区等),这就带来了“竞态条件(Race Condition)”的风险。

举个简单的例子:
假如两个线程A和B几乎在同一时间读取同一个计数变量x的值为10,然后各自+1并写回。你期望x变为12,但结果可能还是11。

这类问题就需要“互斥”机制来保护——确保同一时刻只能有一个执行单元访问共享资源,其访问的代码区域称为临界区(Critical Section)


💡常见的并发控制机制

Linux驱动开发中,常见的互斥控制方式有以下几种:

互斥机制特点场景
中断屏蔽禁止中断上下文干扰适用于简单、快速完成的临界区
原子操作使用CPU原子指令保证变量操作完整操作变量极少时
自旋锁(spinlock)自旋等待,适合短时间锁定中断/进程上下文
信号量(semaphore)可睡眠等待,适合长时间持锁进程上下文,驱动任务中常用
互斥锁(mutex)是信号量的简化版本一般用于用户态/驱动模块

🔐自旋锁和信号量通俗理解

🌀自旋锁(Spinlock)——“厕所排队锁”

把共享资源想象成一个单人厕所。

  • 线程A进入厕所,并锁门(获取锁);
  • 线程B也想用厕所,只能在门口一直转圈圈(不停检查锁状态);
  • A出来后释放锁,B才能进去。

自旋锁适合锁定时间非常短的临界区,因为等待期间线程一直占用CPU,不睡觉!

✅ 优点:

  • 实时性好(适合中断上下文)
  • 实现简单

❌ 缺点:

  • CPU占用率高,锁持有久了会浪费资源
  • 不可在临界区使用可能睡眠的代码!

🚦信号量(Semaphore)——“停车场智能显示器”

假设一个停车场有100个车位,信号量就相当于入口处的电子屏:

  • 显示“当前车位:20”,车还能进;
  • 显示“满”,车就得等;
  • 有车离开,车位更新,通知其他等车入场。

信号量适合临界区操作时间较长、可能会阻塞的场景

✅ 优点:

  • 可睡眠等待,不占CPU
  • 适合处理资源池问题,如连接池、缓存池等

❌ 缺点:

  • 实时性差,不可用于中断处理
  • 实现复杂,需考虑死锁
  • 锁被短时间持有时,使用信号量就不太适宜了,因为睡眠引起的耗时可能比锁被占用的全部时间还要长。

🆚 自旋锁 vs 信号量

对比项自旋锁信号量
是否睡眠❌ 不可睡眠✅ 可睡眠等待
适用上下文中断上下文进程上下文
临界区时长极短可长
是否允许抢占❌ 不允许(禁抢)✅ 允许抢占
用于中断中✅ 可以❌ 禁止
是否可重入❌ 否✅ 是(看实现)

在你占用信号量的同时不能占用自旋锁,因为在你等待信号量时可能会睡眠,而在持有自旋锁时是不允许睡眠的。


读写锁是什么?

当临界区的一个文件可以被同时读取,但是并不能被同时读和写。如果一个线程在读,另一个线程在写,那么很可能会读取到错误的不完整的数据。读写自旋锁是可以允许对临界区的共享资源进行并发读操作的。但是并不允许多个线程并发读写操作

🚨死锁问题和解决策略

在操作系统或并发编程中,**死锁(Deadlock)**是一个经典问题。本文将带你由浅入深地了解死锁的处理方式,主要包括四种:预防死锁、避免死锁、检测死锁以及解除死锁。


一、预防死锁

死锁产生的四个必要条件是:互斥、不可剥夺、请求与保持、循环等待。

为了预防死锁,我们可以通过破坏其中一个或多个条件来避免死锁的发生。

1. 资源一次性分配(破坏请求与保持条件)

当一个进程申请资源时,必须一次性申请它执行所需的所有资源。如果一次申请不到,就什么也不分配,避免持有部分资源再申请其他资源。

2. 可剥夺资源(破坏不可剥夺条件)

允许系统在资源不足时,强行从某些进程中回收已分配的资源,重新分配给其他更需要的进程。

3. 资源有序分配法(破坏循环等待条件)

为所有资源编号,进程必须按编号递增的顺序申请资源。释放时则按编号递减顺序释放。这样可以避免资源请求形成闭环。


二、避免死锁

相比预防死锁,避免死锁不要求完全避免死锁条件的成立,而是在每次资源分配时判断是否安全。

银行家算法

预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。
这是最经典的死锁避免算法。

  • 系统在每次资源分配前,模拟本次资源分配是否会导致系统进入不安全状态。
  • 如果安全,则分配资源;否则让进程等待。

三、死锁检测

死锁检测是允许死锁发生,但系统会定期检查是否有死锁存在,一旦检测到就进行处理。

步骤如下:
  1. 系统记录所有进程与资源的指定一个唯一的号码,构建资源分配图或等待图。
  2. 检查是否存在环路(循环等待)结构。
  3. 若有环路,即可判定发生了死锁。

四、解除死锁

当检测到死锁后,需要采取措施解除死锁状态。常见方法如下:

1. 剥夺资源

从非死锁进程中剥夺资源分配给死锁进程,让后者能继续运行,释放资源。

2. 撤消进程
  • 终止死锁进程或一些代价较小的进程,释放资源。
  • 代价可以依据优先级、运行时间、完成率、业务重要性来评估。

五、避免死锁的编程实践

在多线程编程中(如Java、C++),我们还可以通过一些实际的编程技巧避免死锁:

1. 加锁顺序(Lock Ordering)

确保所有线程在获取多个锁时,始终按照固定顺序获取。例如:线程要获取锁A和锁B,必须先获取编号小的锁A,再获取锁B。

// Thread 1:
synchronized(lockA) {synchronized(lockB) {// do something}
}
// Thread 2: 也必须先获取lockA,再获取lockB

按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁,并对这些锁做适当的排序),但总有些时候是无法预知的。

2. 加锁时限(Try Lock with Timeout)

设置锁获取的超时时间,如果无法在一定时间内获取到锁,就放弃。

if(lock.tryLock(500, TimeUnit.MILLISECONDS)) {try {// do something} finally {lock.unlock();}
} else {// 获取锁失败,执行其他逻辑或重试
}

这种方式可以有效避免长时间等待。

3. 死锁检测机制

针对上面两种不适用的场景。那些不可能实现按序加锁并且锁超时也不可行的场景
使用数据结构记录线程和资源的持有与请求状态,在失败时主动检查是否形成了等待环。

当检测到环路时:

  • 某些线程主动释放锁虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁,原因同超时类似,不能从根本上减轻竞争
  • 或者优先级较低的线程撤退一段时间再重试

这种方式适合无法提前安排加锁顺序的复杂应用场景。


总结

方法是否允许死锁发生是否易于实现是否影响性能
预防死锁较简单
避免死锁
死锁检测
解除死锁复杂低(只在死锁发生时影响)

🧪真实驱动例子:互斥访问设备寄存器

假设我们要编写一个字符设备驱动,多个进程可能并发调用 read() 操作,访问同一片寄存器区域。

临界区:

static DEFINE_SPINLOCK(my_lock);ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *off) {unsigned long flags;spin_lock_irqsave(&my_lock, flags);// 临界区:访问共享寄存器data = ioread32(dev->reg_base);spin_unlock_irqrestore(&my_lock, flags);return 0;
}

注意:用 spin_lock_irqsave 是因为中断中也可能调用,必须禁止中断防止死锁!


🧠Q&A 常见问题

Q:单核CPU还需要加锁吗?
A:需要!因为即使单核,操作系统依然可以通过抢占调度让线程切换,导致共享变量被多个线程交叉访问。

Q:信号量可以用在中断中吗?
A:不能!因为信号量可能会休眠,而中断处理函数不能休眠,否则整个中断系统会挂死。

Q:spin_lock能不能睡眠?
A:不能!因为它禁止抢占,如果睡眠,系统可能无法调度其他任务,导致死锁。


✅总结

  • 多线程 + 共享资源 = 必须互斥
  • 自旋锁适合临界区非常短的场景;信号量适合长时间、可睡眠的场景
  • 死锁问题复杂,要尽量规避:统一加锁顺序、设置超时、图算法检测
  • 在驱动中使用锁时要特别考虑上下文(中断/进程)和是否可休眠

相关文章:

  • ActiveMQ 核心概念与消息模型详解(二)
  • centos7 设置EMQX开机自启动(创建init.d脚本来设置emqx的自启动服务)
  • 文档构建:Sphinx全面使用指南 — 实战篇
  • leetcode 二分查找应用
  • 未曾设想的道路1
  • idea无法下载源代码
  • 使用Python模拟子弹与子弹的碰撞
  • 蓝桥杯 19. 最大比例
  • 阳光乳业2024年营收、利润双降:囿于一隅,市场单一化困局何解?
  • 抱佛脚之学SSM五
  • Rust 学习笔记:安装 Rust
  • Qt实现语言切换的完整方案
  • 一,开发环境安装
  • Java 实现单链表翻转(附详细注释)
  • redis 使用 Docker 部署 简单的Redis 集群(包括哨兵机制)
  • Qt基础007(Tcp网络编程)
  • 32单片机——GPIO的工作模式
  • Redis的string类型使用
  • Redis核心技术知识点全集
  • 第五章:5.3 ESP32物联网应用:阿里云IoT平台与腾讯云IoT平台的数据上传与远程控制
  • 一季度沪苏浙皖GDP增速均快于去年全年,新兴动能持续壮大
  • 重庆一幼儿园回应招聘硕士幼教:统一标准,江北区学前教育岗的硬性要求
  • 李公明|“小时光”与大时代中的地铁阅读者
  • 广西三江通报“网约车司机加价”:对网约车平台进行约谈
  • 人民日报评“我愿意跟他挨着”:城市要善待奋斗者,惩治作恶者
  • 安且吉兮,西泠印社雅集吴昌硕故里