遇到一个BUG: scheduling while atomic: kworker/0:2/370/0x00000002;看了这篇文章BUG: scheduling while atomic 分析,是因为在原子操作上下文或者中断上下文进行了调度引起的。
先看下为什么会打印出这句:
schedule() -> __schedule() -> schedule_debug()
static inline void schedule_debug(struct task_struct *prev)
{
#ifdef CONFIG_SCHED_STACK_END_CHECK
if (task_stack_end_corrupted(prev))
panic("corrupted stack end detected inside scheduler\n");
#endif
if (unlikely(in_atomic_preempt_off())) {
__schedule_bug(prev);
preempt_count_set(PREEMPT_DISABLED);
}
rcu_sleep_check();
profile_hit(SCHED_PROFILING, __builtin_return_address(0));
schedstat_inc(this_rq()->sched_count);
}
/*
* Print scheduling while atomic bug:
*/
static noinline void __schedule_bug(struct task_struct *prev)
{
/* Save this before calling printk(), since that will clobber it */
unsigned long preempt_disable_ip = get_preempt_disable_ip(current);
if (oops_in_progress)
return;
printk(KERN_ERR "BUG: scheduling while atomic: %s/%d/0x%08x\n",
prev->comm, prev->pid, preempt_count());
debug_show_held_locks(prev);
print_modules();
if (irqs_disabled())
print_irqtrace_events(prev);
if (IS_ENABLED(CONFIG_DEBUG_PREEMPT)
&& in_atomic_preempt_off()) {
pr_err("Preemption disabled at:");
print_ip_sym(preempt_disable_ip);
pr_cont("\n");
}
if (panic_on_warn)
panic("scheduling while atomic\n");
dump_stack();
add_taint(TAINT_WARN, LOCKDEP_STILL_OK);
}
也就是 if (unlikely(in_atomic_preempt_off())) 这句成立,就会报这个错误。
#define in_atomic_preempt_off() (preempt_count() != PREEMPT_DISABLE_OFFSET)
preempt_count() 读取 preempt_count,这个成员被用来判断当前进程是否可以被抢占,这个值是一个 task 的 thread info 中的一个成员变量。也就是 preempt_count 被改变不等于 PREEMPT_DISABLE_OFFSET 之后就会报这个错。可能是代码调用 preempt_disable 显式的禁止了抢占,也可能是处于中断上下文等。其中 preempt_disable 和 preempt_enable 成对出现是对 preempt_count 进行加一和减一的操作。
那么 preempt_count 在什么情况下会发生改变呢?
1.原子操作上下文中,比如spin_lock。
spin_lock()->raw_spin_lock()->_raw_spin_lock()->__raw_spin_lock()
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
preempt_disable();
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}
2.中断上下文中。
handle_IRQ()->__handle_domain_irq()->irq_enter()->__irq_enter()
#define __irq_enter() \
do { \
account_irq_enter_time(current); \
preempt_count_add(HARDIRQ_OFFSET); \
trace_hardirq_enter(); \
} while (0)
硬件中断来的时候会调用 handle_IRQ 进中断处理函数,会调用到 preempt_count_add(HARDIRQ_OFFSET),这个函数虽然不是像 preempt_disable 将 preempt_count 加一,但是它过分的是将其加 HARDIRQ_OFFSET(1UL << 16),这个时候显然是不等于 PREEMPT_DISABLE_OFFSET,如果调用 schedule 就会报 bug。
这也同时让我想起之前说为什么中断不能睡眠,网上多数分析的原因都是些主观上的不能睡眠调度的原因,并没有从代码上分析。其中让我夜不能寐一句说:
2.4内核中schedule()函数本身在进来的时候判断是否处于中断上下文:
if(unlikely(in_interrupt()))
BUG();
因此,强行调用schedule()的结果就是内核BUG,但我看2.6.18的内核schedule()的实现却没有这句,改掉了。
其实2.6以上是有实现的,就是上面的 flow 触发。2.4是分别判断 in_atomic 和 in_interrupt (咱也没找到2.4的kernel code,咱就敢说,你找去吧,反正你也找不到),2.6以后将所有不允许调度的情况都集合在 preempt_count 这个变量上,用不同的位来代表不同的情况,具体preempt_count的数据格式可以参考下图:
preemption count 用来记录当前被显式的禁止抢占的次数,这就和代码里一致了:中断中是加 HARDIRQ_OFFSET(1UL << 16) 对应 bit16,原子上下文是加1对应 bit0。
我们再看那句log:
printk(KERN_ERR "BUG: scheduling while atomic: %s/%d/0x%08x\n",
prev->comm, prev->pid, preempt_count());
最后会将 preemption count 打印出来,这样就能通过这个值来看是因为在哪种情况下调用 schedule 而导致的 kernel crash,还是内核牛批啊。
所以说也不能老是听大佬说中断不能调睡眠函数又不去想为啥,还是得看代码怎么实现的,它万一有一天客户拿枪顶着我的脑袋让我解释个中原因呢?
我赌你枪里没有子弹!
参考文章: