遇到一个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

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)) copy from: http://www.wowotech.net/kernel_synchronization/spinlock.html/comment-page-2#comments作者:linuxer 发布于:2015-4-22 12:22 分类:内核同步机制 在linux kernel的实现中,经常会遇到这样的场景:共享数据被中断上下文和进程上下文访问,该如何保护呢?如果只 [26578.636839] C1 [ swapper/1] BUG: scheduling while atomic: swapper/1/0/0x00000002 [26578.636869] C0 [ kworker/u:1] CPU1 is up [26578.636900] C1 [ swapper/1] Modu linux内核打印"BUG: scheduling while atomic"和"bad: scheduling from the idle thread"错误的时候,通常是在中断处理函数中调用了可以休眠的函数,如semaphore,mutex,sleep之类的可休眠的函数,而linux内核要求在中断处理的时候,不允许系统调度,不允许抢占,要等到中断处理完成才能做其他事情。因此,要充分考虑中断处理... 这个问题实际上是一个老生常谈的问题,答案也很简单,Linux在软中断上下文中是不能睡眠的,原因在于Linux的软中断实现上下文有可能是中断上下文,如果在中断上下文中睡眠,那么会导致Linux无法调度,直接的反应是系统KernelPanic,并且提示dequeue_task出错。所以,在软中断上下文中,我们不能使用信号量等可能导致睡眠的函数,这一点在编写IO回调函数时需要特别注意。在最近的一个项目中 在Linux内核的五大组成部分(进程管理、内存管理、设备驱动、文件系统、网络协议)中,进程管理是非常重要的一部分,它虽然不像内存管理、虚拟文件系统那样复杂,也不像进程间通信那样条理化,但作为五大内核组成部分之一,进程管理对我们理解内核的运作、对于我们今后的编程非常重要。同时,作为五大组成部分的核心,它与其他四个模块都有联系。 因此,对于进程管理是必须要充分理解和掌握的。本文对进程管理进行详细的介绍,首先介绍进程及其相关的概念,而后介绍进程的创建、退出等基本操作以及线程的相关知识。 中断处理的时候,不应该发生进程切换,因为在中断context中,唯一能打断当前中断handler的只有更高优先级的中断,它不会被进程打断(这点对 于softirq,tasklet也一样,因此这些bottom half也不能休眠),如果在中断context中休眠,则没有办法唤醒它,因为所有的 wake_up_xxx都是针对某个进程而言的,而在中断context中,没有进程的概念,没有一个task_... Linux内核把进程称为任务(task),进程的虚拟地址空间分为用户虚 拟地址空间和内核虚拟地址空间,所有进程共享内核虚拟地址空间,每个 进程有独立的用户空间虚拟地址空间。 进程有两种特殊形式:没有用户虚拟地址空间的进程称为内核线程, 共享用户虚拟地址空间的进程称为用户线程。通用在不会引起混淆的情况下把用户线程简称为线程。共享同一个用户虚拟地址空间的所有用户线程组成一个线程组。 2、Linux进程四要素: a.有一段程序供其执行。 b.有进程专用的系...