BUG: scheduling while atomic 分析 and 为什么中断不能睡眠

遇到一个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,还是内核牛批啊。

所以说也不能老是听大佬说中断不能调睡眠函数又不去想为啥,还是得看代码怎么实现的,它万一有一天客户拿枪顶着我的脑袋让我解释个中原因呢?

 我赌你枪里没有子弹!

 

参考文章:

中断上下文中调度会怎样?

linux kernel的中断子系统之(八):softirq

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值