为何irqs_disabled()是禁用抢占的不安全方式?
Great question—this is a super common pitfall when working with preemptible Linux kernels, and it boils down to a key misunderstanding: disabling IRQs does not equal disabling preemption. Let's break this down step by step:
1. 抢占的核心控制是抢占计数,而非IRQ状态
在可抢占内核中,决定是否允许抢占的核心开关是抢占计数(一个CPU本地的整数)。当计数大于0时,抢占被阻止;计数归0时,抢占被允许。
调用local_irq_disable()只是关闭当前CPU的硬件中断(包括定时器中断),但它完全不会修改抢占计数。所以即使IRQs被禁用,只要抢占计数是0,内核依然认为抢占是允许的。
2. spin_unlock()即使在IRQs禁用时也可能触发调度
假设你用spin_lock_irq()保护临界区,这个函数会做两件事:禁用IRQs,同时增加抢占计数(阻止抢占)。当你后续调用spin_unlock_irqrestore()时,它会恢复原本的IRQ状态,同时减少抢占计数。
如果这次递减让抢占计数归0,内核会立刻检查是否有更高优先级的任务在等待运行。如果有,就会触发preempt_schedule()——哪怕此时IRQs仍然是禁用的。这种调度不是由定时器中断触发的,而是内核主动检查并切换任务,完全绕开了IRQ禁用的限制。
3. 像printk()这类“无害”函数也可能触发抢占
还有一个容易踩的坑:printk()看起来没什么风险,但它内部可能会调用console_unlock()来刷新日志。在某些场景下(比如等待控制台缓冲区空闲),console_unlock()会显式调用schedule()让出CPU。
如果你只禁用了IRQs(但抢占计数还是0),这个schedule()调用会正常执行——你的任务会被抢占,哪怕你以为IRQs禁用就能避免这种情况。这会直接破坏你原本以为安全的临界区,导致竞态条件和数据结构损坏。
总结
禁用IRQs只能阻止由硬件中断触发的抢占(比如定时器 tick 或者设备中断),但无法阻止内核主动发起的调度(比如显式的schedule()调用,或者spin_unlock()的副作用)。
要在可抢占内核中安全禁用抢占,应该使用能修改抢占计数的机制:
- 对基于自旋锁的临界区,使用
spin_lock()(它会增加抢占计数) - 直接调用
preempt_disable()来阻止抢占,完成后用preempt_enable()恢复
内容的提问来源于stack exchange,提问作者sherlock

