Linux驱动开发:锁与上下文的核心心法

📅 2026/7/6 21:20:09 👁️ 阅读次数 📝 编程学习
Linux驱动开发:锁与上下文的核心心法
编写 Linux 驱动或修改内核代码时,锁的选择上下文的判断是开发者最核心的两个“心法”。如果选错了锁,轻则性能下降,重则导致死锁或系统崩溃(Panic)。
你可以通过以下这套“分层思考法”来构建开发思维:

第一步:精准识别“上下文” (Context)

在 Linux 中,代码要么在为“人”(进程)工作,要么在为“事”(硬件中断)工作。
1. 进程上下文 (Process Context)
  • 特征:由系统调用或内核线程触发。它拥有一个明确的“身份证”——struct task_struct(即current指针有效)。
  • 权利可以睡眠。它可以等待 I/O、等待内存、或者获取一个可能导致阻塞的信号量。
  • 如何识别:调用!in_interrupt()为真。
2. 中断上下文 (Interrupt Context)
  • 特征:由硬件强行打断 CPU 执行流触发。它没有固定的“身份证”,它借用当前被中断进程的堆栈。
  • 禁忌绝对不能睡眠。一旦在中断中睡眠,调度器无法找回它,系统直接卡死。
  • 细分
    • 硬中断 (Hard IRQ):处理最紧急的硬件信号,执行极快。
    • 软中断/Tasklet (SoftIRQ/Tasklet):处理稍不紧急的任务,虽然也在中断上下文中,但允许被硬中断打断。
  • 如何识别:调用in_interrupt()为真。
3. 原子上下文 (Atomic Context)
这是一个更广的概念。所有中断上下文都属于原子上下文,同时,持有自旋锁的进程上下文也进入了原子上下文。在原子上下文中,任何可能导致放弃 CPU 的行为(如msleep)都是禁止的。

第二步:锁的选择逻辑 (The Locking Strategy)

当你确定了上下文,就可以按照这个决策树来选择锁:
情况 A:资源在“进程上下文”之间共享
  • 场景:两个驱动函数都被用户空间的应用程序调用(如两个线程同时写/dev/my_dev)。
  • 推荐mutex(互斥锁)
  • 原因:临界区可能很大(比如需要拷贝大量数据),让后面来的进程睡觉比让 CPU 瞎转(自旋)更划算。
情况 B:资源在“进程上下文”与“中断上下文”之间共享
  • 场景:进程在写缓冲区,而中断处理程序(ISR)在读这个缓冲区。
  • 推荐spin_lock_irqsave()
  • 原因
  1. 中断不能睡,所以必须用自旋锁。
  2. 为了防止当前核的进程拿锁时被本地中断打断(导致死锁),必须在拿锁的同时关闭本地中断。
情况 C:资源在“中断上下文”之间共享
  • 场景:两个不同的中断或者同一个中断在不同 CPU 上同时运行(处理共享寄存器)。
  • 推荐spin_lock()
  • 原因:大家都不能睡,只能自旋。

第三步:思考维度总结表

可以把这张表印在脑子里:
保护对象涉及的最高优先级上下文
推荐使用的锁
本质理由
硬中断 (Hard IRQ)
spin_lock_irqsave
必须防止多核竞争 + 本地硬中断重入
软中断 (SoftIRQ/Tasklet)
spin_lock_bh
防止多核竞争 + 本地软中断重入
进程 (仅需同步)
mutex
允许睡眠,节省 CPU 功耗
只读频繁,写很少
RCUrwlock
提高读取的并发性能,减少缓存颠簸

第四步:高级进阶思考(Cache 与 性能)

当你习惯了基本的锁,就要开始从Cache 一致性的角度优化:
  1. 锁的粒度 (Lock Granularity)
  • 不要用一把大锁保护整个驱动。尝试把数据结构拆分,每个结构体用自己的小锁。这样不同 CPU 访问不同结构时,Cache Line 不会互相冲突。
  1. 避免虚假共享 (False Sharing)
  • 如果两个锁在物理内存上挨得太近(在同一个 64 字节的 Cache Line 里),即使它们保护不同的数据,多核竞争时也会触发大量的 MESI 缓存失效。
  • 解决:使用____cacheline_aligned宏让锁在内存中对齐。
  1. 尽量使用 Per-CPU 变量
  • 如果数据可以按 CPU 分开(比如统计每个核处理了多少包),就完全不需要锁。每个 CPU 只改自己的 Cache,效率达到最高。

总结:你的开发检查清单

  1. 这个函数会在中断里跑吗?会?只能用自旋锁,且不能有任何睡眠函数。
  2. 锁的临界区里有 I/O 或内存申请吗?有?考虑用mutex,并确保不会在中断里调用。
  3. 这个变量被访问的频率极高吗?是?考虑RCU或者将变量Per-CPU化,避开昂贵的原子指令。