如果操作GPIO可能导致休眠,那么同步机制绝不能采用spinlock

最近看到了这样一句话:如果操作 GPIO 可能导致休眠,那么同步机制绝不能采用 spinlock。要想彻底领悟这句话的含义,我们要将它拆分成三个部分,分别是 spinlock 的本质,休眠的本质以及 GPIO 操作为什么会导致休眠。最后我们再将三者结合,分析出如果违背了这句话,最终会导致怎么样的后果。

1. spinlock的底层逻辑

spinlock 也就是 自旋锁 是为了多核处理器环境设计的一种 极轻量级、极短时间 的同步机制。

自旋锁有以下几个特点:

  • 当线程 A 获取了某个自旋锁,线程 B 再去尝试获取同一个自旋锁时,线程 B 不会去休眠,而是占用 CPU 执行循环,也就是原地打转,疯狂检查这个自旋锁有没有被释放。
  • 在 Linux 内核中,一旦一个线程获取了自旋锁,内核就会关闭当前 CPU 的抢占功能。这也就是说,只要这个线程还持有这个自旋锁,操作系统的调度器就不能把这个线程从 CPU 上踢走,换成别的线程。除非发生了更高优先级的中断,但是这也取决于自旋锁的变体,如果使用的是 spin_lock_irqsave,连中断也会被关闭。
  • 被自旋锁保护的代码区域称为 原子上下文,也就是说代码必须像原子一样不可分割,绝不能出现任何可能让出 CPU 控制权的操作。

通过上面几个自旋锁的特点,我们基本能够看出来:如果一个线程拿到了自旋锁,那么基本上可以等同于它霸占了当前 CPU 的绝对使用权,站在系统管理层面来看,这就意味着该线程必须尽快执行完毕并释放自旋锁

2. 休眠的底层逻辑

在操作系统中,休眠通常意味着 线程主动或者被动的交出 CPU 的使用权

当一个线程调用了可能导致休眠的函数,它实际上是在告诉操作系统的调度器,自己现在已经无法使用 CPU 了,然后调度器就会把它放到 等待队列 中,换成别的已经 就绪 线程继续使用 CPU。

可以说,休眠的本质就是触发系统调度,从而把 CPU 让给别的线程。

3. 为什么操作GPIO可能导致休眠

有不少人对 CPU 的印象还停留在下面的场景:他们一般认为,控制 GPIO 不就是往寄存器里写个 0 或 1 吗?这样速度是极快的,怎么会导致休眠呢?

在 SoC 芯片 内部原生 的 GPIO 上确实如此,这种操作的确是不会导致休眠的。

但是,现代硬件系统中往往存在着 GPIO 扩展芯片。

比如,主控芯片的 GPIO 不够用了,硬件工程师加了一片 I2C 接口或 SPI 接口的 GPIO 扩展芯片。

当你通过代码去设置这种 GPIO 扩展芯片上的 GPIO 引脚时,问题就出现了:

  • 为了控制扩展芯片上的引脚,CPU 必须通过 I2C 或者 SPI 总线向扩展芯片发送控制命令。
  • 但是 I2C 和 SPI 总线的通信速度相对于 CPU 来说是极其缓慢的。
  • 为了不浪费 CPU 资源,I2C 或者 SPI 控制器的底层驱动会在发起总线传输之后,让当前线程休眠,等待硬件中断的唤醒。

所以,如果你操作的 GPIO 是通过低速总线扩展出来的,底层驱动必然会调用导致休眠的函数

4. 违反规则的后果

现在,我们把上面的逻辑拼接起来,看看如果你用 spinlock 保护一段可能休眠的 GPIO 操作,会发生什么样的后果。

假设我们有两个线程:线程 A 和线程 B。系统有两个 CPU 核:CPU 0 和 CPU 1。线程 A 运行在 CPU 0,线程 B 运行在 CPU 1。

下面开始分析:

  • 线程 A 调用 spin_lock 获取了自旋锁,CPU 0 的抢占被关闭,调度器在 CPU 0 上失效。
  • 线程 A 开始操作一个 I2C 总线上的 GPIO。
  • 线程 A 在等待 I2C 总线传输时,底层驱动让它休眠了。此时矛盾爆发了:一来线程 A 持自旋锁告诉内核关闭了 CPU 0 的抢占,二来又主动调用调度器要求休眠,切换成别的线程 。在未开启内核调试检查的情况下,调度器可能被迫强行切换上下文,把 CPU 0 让给了其他线程,但此时,自旋锁依然被线程 A 持有着
  • 线程 B 此时也要操作这个设备,于是调用 spin_lock 尝试获取同一把自旋锁。
  • 这时,线程 B 发现锁被占用了,于是开始在 CPU 1 上自旋,等待锁被释放。
  • 重点来了:持有锁的线程 A 在休眠,他要等 I2C 中断到来,并且调度器重新让它使用 CPU 时,他才能释放锁。如果在这个时间段中恰好触发了一个复杂的调度情况,系统的整体状态将陷入僵死。即便线程 A 能够醒来,这种 带着自旋锁休眠 的行为也会导致其他等待该锁的 CPU 核,比如运行线程 B 的 CPU1,浪费大量的时钟周期,完全违背了自旋锁设计的初衷

因为这种错误极其致命,Linux 内核设计了严格的防范机制,如果你在编译内核时开启了 CONFIG_DEBUG_SPINLOCK_SLEEP 选项,内核会在每次发生休眠时 检查当前线程是否持有自旋锁

如果发现持有自旋锁,内核会打印出崩溃信息:BUG: scheduling while atomic (错误:在原子上下文中进行了调度)

随后,系统通常会直接崩溃,必须修复代码才能重新运行。

5. 正确处理方法

既然不能使用 spinlock ,那到底该怎么做呢?

如果你的代码在 进程上下文 中运行,并且需要保护一段 可能休眠 的代码,应该使用 mutex。当线程 B 尝试获取已被线程 A 持有的 mutex 时,线程 B 不会自旋 ,而是会 主动去休眠等待 。这样不仅不浪费 CPU,而且完全符合 允许休眠 的逻辑。

此外,为了提醒开发者,Linux 内核特意提供了两套 GPIO 操作接口:

  • gpio_set_value:用于操作原生的、绝对不会休眠 的 GPIO。如果你传入了一个会休眠的扩展 GPIO,内核会报警告。
  • gpiod_set_value_cansleep:明确告诉内核或开发者这个 GPIO 可能导致休眠 。看到这个函数,任何有经验的内核开发者都会立刻明白:外层绝对不能用 spinlock,必须用 mutex

如果是在中断处理函数中怎么办? 要知道中断上下文中是 绝对不允许休眠 的,更不用说加 mutex 了。如果你必须在中断里操作 I2C GPIO,必须使用 Threaded IRQ(中断线程化)Workqueue(工作队列) ,将这部分可能休眠的操作放到进程上下文中去执行。

相关推荐
li星野2 小时前
RTOS面试完整模拟题(嵌入式系统方向)
arm开发·面试·职场和发展
RisunJan2 小时前
Linux命令-mkbootdisk(可建立目前系统的启动盘)
linux·运维·服务器
MekoLi292 小时前
Spring AI 与 LangChain4j 从入门到精通:Java 后端开发者的 AI 实战手册
后端·面试
朽棘不雕3 小时前
Linux工具(上)
linux·运维·服务器
平江鱼3 小时前
Android 组件初始化顺序详解
面试
BestOrNothing_20153 小时前
Ubuntu 22.04 下调整 VS Code 界面及字体教程
linux·vscode·ubuntu22.04·界面调整
桌面运维家3 小时前
Windows/Linux云桌面:高校VDisk方案部署指南
linux·运维·windows
mzhan0174 小时前
Linux:intel:Cache Allocation tech
linux·cpu
肆忆_4 小时前
【面试】手撕线程池
面试