Linux 内核 BUG: scheduling while atomic 或者 double unlock 的排查经验

13次阅读

遇到 scheduling while atomic 或 double unlock 错误,说明在原子上下文中调用了可能睡眠的函数或重复释放锁;需结合 panic 日志分析上下文、调用及锁类型匹配性,并启用 CONFIG_DEbug_ATOMIC_SLEEP、CONFIG_DEBUG_SPINLOCK 等选项辅助定位。

Linux 内核 BUG: scheduling while atomic 或者 double unlock 的排查经验

遇到 BUG: scheduling while atomicdouble unlock 这类内核错误,通常说明驱动或内核模块在原子上下文(atomic context)中做了不该做的事,比如调用了可能睡眠的函数,或者重复释放了同一把锁。这类问题隐蔽性强、复现不稳定,但只要抓住关键线索,就能快速定位。

看清楚错误发生的位置和上下文

内核 panic 日志里会明确指出出错的函数、调用(call trace)、CPU、抢占/中断状态等信息。重点关注:

  • 出错时是否处于中断上下文(in_interrupt() == true)或软中断(in_softirq()) —— 这决定了能否调用 msleepwait_eventmutex_lock 等可能调度的函数;
  • 调用栈中是否有明显“睡眠型”函数,如 wait_event_timeoutkmalloc(GFP_KERNEL)copy_to_userflush_work 等;
  • 是否在 spinlock 持有期间调用了可能阻塞的函数 —— 即使没直接 sleep,某些函数内部也可能触发调度(例如某些设备驱动中的 wait_event 变体)。

检查锁的使用是否匹配上下文

linux 提供多种锁机制,选错类型是常见根源:

  • spinlock / rwlock:只适用于短临界区,且必须在原子上下文中使用(不能 sleep),释放必须与获取严格配对;
  • mutex / semaphore:允许睡眠,只能在进程上下文使用,不能在中断、softirq 或持有 spinlock 时调用;
  • rcu_read_lock():不可阻塞,但也不能嵌套写侧操作,且不能在 spinlock 保护区内调用 synchronize_rcu
  • 特别注意:mutex_unlock 在中断上下文中调用会静默失败,而 spin_unlock 被重复调用则直接触发 double unlock panic。

警惕隐式睡眠和跨上下文调用

有些函数看似“安全”,实则暗藏调度点:

  • kmalloc(GFP_KERNEL) 在内存紧张时可能等待页回收,绝对禁止在中断/softirq 中使用,应改用 GFP_ATOMIC
  • printk() 一般安全,但若启用了 console_lock 且日志量大,也可能间接导致调度(尤其在早期内核);
  • 自定义 workqueue 函数(如 schedule_work 的 handler)运行在进程上下文,但如果它被误从 atomic 上下文直接调用(而非 schedule),就会出问题;
  • 驱动中常见的陷阱:在 ->probe()->remove() 中调用 wait_event 是允许的,但在 ->irq_handler()NAPI poll 中调用就是致命错误。

工具辅助验证和复现

静态检查 + 动态检测能大幅缩短排查周期:

  • 打开内核配置CONFIG_DEBUG_ATOMIC_SLEEP=y(捕获 scheduling while atomic)、CONFIG_DEBUG_SPINLOCK=yCONFIG_DEBUG_MUTEXES=y(捕获 double unlock / unlock without lock);
  • 启用 lockdepCONFIG_LOCKDEP=y,配合 lockdep_assert_held() 在关键路径加断言;
  • 使用 dump_stack() 插桩:在可疑函数入口/出口打印栈,确认调用来源是否符合预期上下文;
  • 在模拟环境中注入延迟或强制抢占(如用 cond_resched() 触发调度点),可让原本偶发的问题稳定复现。
text=ZqhQzanResources