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()。
- 原因:
- 中断不能睡,所以必须用自旋锁。
- 为了防止当前核的进程拿锁时被本地中断打断(导致死锁),必须在拿锁的同时关闭本地中断。
情况 C:资源在“中断上下文”之间共享
- 场景:两个不同的中断或者同一个中断在不同 CPU 上同时运行(处理共享寄存器)。
- 推荐:spin_lock()。
- 原因:大家都不能睡,只能自旋。
第三步:思考维度总结表
可以把这张表印在脑子里:
保护对象涉及的最高优先级上下文 | 推荐使用的锁 | 本质理由 |
硬中断 (Hard IRQ) | spin_lock_irqsave | 必须防止多核竞争 + 本地硬中断重入 |
软中断 (SoftIRQ/Tasklet) | spin_lock_bh | 防止多核竞争 + 本地软中断重入 |
进程 (仅需同步) | mutex | 允许睡眠,节省 CPU 功耗 |
只读频繁,写很少 | RCU或rwlock | 提高读取的并发性能,减少缓存颠簸 |
第四步:高级进阶思考(Cache 与 性能)
当你习惯了基本的锁,就要开始从Cache 一致性的角度优化:
- 锁的粒度 (Lock Granularity):
- 不要用一把大锁保护整个驱动。尝试把数据结构拆分,每个结构体用自己的小锁。这样不同 CPU 访问不同结构时,Cache Line 不会互相冲突。
- 避免虚假共享 (False Sharing):
- 如果两个锁在物理内存上挨得太近(在同一个 64 字节的 Cache Line 里),即使它们保护不同的数据,多核竞争时也会触发大量的 MESI 缓存失效。
- 解决:使用____cacheline_aligned宏让锁在内存中对齐。
- 尽量使用 Per-CPU 变量:
- 如果数据可以按 CPU 分开(比如统计每个核处理了多少包),就完全不需要锁。每个 CPU 只改自己的 Cache,效率达到最高。
总结:你的开发检查清单
- 这个函数会在中断里跑吗?会?只能用自旋锁,且不能有任何睡眠函数。
- 锁的临界区里有 I/O 或内存申请吗?有?考虑用mutex,并确保不会在中断里调用。
- 这个变量被访问的频率极高吗?是?考虑RCU或者将变量Per-CPU化,避开昂贵的原子指令。
