4. 中断上下文限制
中断处理程序运行在一种特殊的执行环境中——原子上下文。在这种上下文中,内核不允许执行任何可能导致进程切换或睡眠的操作。违反这些规则会导致系统死锁、内核崩溃或数据损坏。本章将深入剖析原子上下文的规则、可睡眠函数的禁用原则以及自旋锁在中断安全中的正确使用。
4.1 原子上下文规则
原子上下文的核心特征是:当前执行路径不能被调度器打断。中断处理程序(包括顶半部和底半部)、软中断、tasklet 以及持有自旋锁的代码路径都属于原子上下文。
核心规则:在原子上下文中,禁止调用任何可能触发
schedule() 或等待资源的函数。
以下行为在原子上下文中是严格禁止的:
- 睡眠操作:
msleep()、usleep_range()、wait_event() - 内存分配:使用
GFP_KERNEL标志的kmalloc()(可能触发直接回收) - 信号量操作:
down()、down_interruptible()(信号量允许睡眠) - 用户空间访问:
copy_from_user()、copy_to_user()(可能触发缺页异常) - 互斥锁:
mutex_lock()(互斥锁实现中包含睡眠)
调试技巧:内核提供了
might_sleep() 宏,可以在可能睡眠的路径上插入检查。如果该宏在原子上下文中被调用,内核会打印警告信息。在驱动开发中,建议在可能被中断调用的函数入口处添加 might_sleep() 进行防御性检查。
4.2 可睡眠函数禁用清单
下表列出了驱动开发中常见的函数及其在原子上下文中的可用性:
| 函数类别 | 函数示例 | 原子上下文可用性 | 替代方案 |
|---|---|---|---|
| 内存分配 | kmalloc(size, GFP_KERNEL) |
❌ 不可用 | kmalloc(size, GFP_ATOMIC) |
| 内存分配 | kzalloc(size, GFP_KERNEL) |
❌ 不可用 | kzalloc(size, GFP_ATOMIC) |
| 锁机制 | mutex_lock() |
❌ 不可用 | spin_lock() |
| 锁机制 | down_interruptible() |
❌ 不可用 | spin_lock_irqsave() |
| 延迟 | msleep() |
❌ 不可用 | udelay() / ndelay() |
| 延迟 | usleep_range() |
❌ 不可用 | mdelay()(短延迟) |
| 用户空间访问 | copy_from_user() |
❌ 不可用 | 使用共享内存或 ioremap |
| 调度 | schedule() |
❌ 不可用 | 无(必须避免) |
| 等待队列 | wait_event_interruptible() |
❌ 不可用 | 使用 tasklet 或工作队列 |
4.3 自旋锁与中断安全
自旋锁是原子上下文中唯一的锁机制。它的核心特点是:如果锁被占用,当前 CPU 会原地自旋等待,不会睡眠。然而,在中断上下文中使用自旋锁时,必须考虑中断处理程序与进程上下文之间的死锁风险。
4.3.1 经典死锁场景
考虑以下场景:进程上下文持有自旋锁 lock,此时中断发生,中断处理程序尝试获取同一个 lock。由于进程上下文不会释放锁(被中断打断),中断处理程序将永远自旋等待,导致系统死锁。
关键问题:中断处理程序与进程上下文共享自旋锁时,必须在进程上下文中使用
spin_lock_irqsave() 来禁用本地 CPU 的中断,防止死锁。
4.3.2 中断安全的自旋锁变体
Linux 内核提供了多种自旋锁变体来应对不同的中断场景:
spin_lock()/spin_unlock():基本自旋锁,不涉及中断状态。适用于所有竞争者都在非中断上下文的情况。spin_lock_irq()/spin_unlock_irq():获取锁时禁用本地 CPU 中断,释放时恢复。适用于已知中断状态为开启的情况。spin_lock_irqsave()/spin_unlock_irqrestore():获取锁时保存当前中断状态并禁用本地中断,释放时恢复保存的状态。这是最安全的变体,推荐在进程上下文中使用。spin_lock_bh()/spin_unlock_bh():获取锁时禁用底半部(softirq/tasklet),适用于进程上下文与底半部共享锁的场景。
4.3.3 中断上下文中的自旋锁使用规则
在中断处理程序(顶半部)中,只能使用 spin_lock(),因为本地中断已经被硬件禁用。如果在中断处理程序中调用 spin_lock_irqsave(),会导致中断状态被错误保存和恢复。
最佳实践:
- 进程上下文:使用
spin_lock_irqsave()保护共享数据 - 中断顶半部:使用
spin_lock()访问同一共享数据 - 底半部(tasklet/softirq):使用
spin_lock_bh()保护共享数据
4.4 原子上下文与自旋锁交互流程
下图展示了进程上下文与中断上下文通过自旋锁保护共享资源的正确交互流程:
4.5 常见故障模式与调试方法
在中断上下文调试中,以下故障模式最为常见:
| 故障模式 | 症状 | 内核错误信息 | 调试方法 |
|---|---|---|---|
| 原子上下文睡眠 | 系统挂起或重启 | BUG: sleeping function called from invalid context |
检查调用栈,定位睡眠函数;使用 might_sleep() 辅助定位 |
| 自旋锁死锁 | CPU 软锁或硬锁 | BUG: soft lockup - CPU#0 stuck for 22s! |
使用 lockdep 检测锁依赖;检查中断安全变体使用 |
| 中断丢失 | 设备无响应 | irq event stamp 异常 |
检查 spin_lock_irqsave() 是否错误地长时间禁用中断 |
| 内存分配失败 | 驱动行为异常 | page allocation failure |
确认在原子上下文中使用了 GFP_ATOMIC 标志 |
调试利器:内核的
lockdep 子系统是检测自旋锁死锁和中断安全问题的强大工具。在编译内核时启用 CONFIG_PROVE_LOCKING,lockdep 会在运行时自动检测锁依赖违规并输出详细的调用栈信息。建议在开发阶段始终开启此选项。
4.6 原子上下文规则总结
中断上下文限制是驱动开发中最容易出错的地方之一。以下是需要牢记的关键原则:
- 不睡眠:原子上下文中禁止任何可能导致睡眠的操作
- 不等待:禁止使用等待队列、信号量等同步机制
- 不分配:内存分配必须使用
GFP_ATOMIC标志 - 不访问用户空间:用户空间地址可能触发缺页异常
- 锁安全:进程上下文使用
spin_lock_irqsave(),中断上下文使用spin_lock() - 短执行:中断处理程序应尽快完成,耗时操作推迟到底半部
经验教训:在调试过程中,如果遇到难以复现的系统挂起或随机崩溃,首先检查中断处理程序中是否调用了可睡眠函数。使用
ftrace 的 irqsoff 追踪器可以记录中断被禁用的时间,帮助定位长时间持有自旋锁或禁用中断的代码路径。