BUG: scheduling while atomic检测机制原理与问题处理

本文所有代码分析均基于linux-5.4.18

1 BUG: scheduling while atomic

        当在任务切换过程中,如果调度器检测到切换前的环境属于原子操作,是禁止发生任务切换的,便会输出“BUG: scheduling while atomic:”等异常打印,之后会根据配置决定是否触发panic,重启系统。

1.1 内核中监测机制原理

        Linux内核调度器中,每次任务调度发生时,都会检查当前是否处于原子操作环境中,如果是,则打印出“BUG: scheduling while atomic:***”异常信息,打印出调度栈。如果此时,内核中配置了“panic_on_warn”,则会触发kernel panic,造成系统重启。

        在内核调度器的代码实现中,__schedule()为任务调度的关键函数,发生任务调度时,最终都会执行到此函数,此函数中通过执行schedule_debug(prev),来判断是否发生了“BUG: scheduling while atomic:***”。

        代码实现流程如下:

1.1.1 __schedule()

        在调度关键函数__schedule()进行任务切换前,调用schedule_debug(prev)进行相关检查,prev为任务切换时正在运行的任务。

        这里需要注意两点:1)__schedule()函数执行前,必须是禁止抢占的;2)__schedule()函数用static声明,只在本文件中使用。

        第一点可以解释,下文in_atomic_preempt_off()中判断原子操作时,为什么要将preempt_count计数与“1”进行对比,而不是“0”,原因就是__schedule()执行前,执行了禁止抢占操作,preempt_count进行了加“1”操作。

        第二点用“static”声明,函数只在本文件中使用,其他地方进行任务调度操作,都是使用封装好的函数,函数中进行禁止抢占、__scheduler()、打开抢占等操作。

/*
 * __schedule() is the main scheduler function.
 * WARNING: must be called with preemption disabled!
 */
static void __sched notrace __schedule(bool preempt)
{
    。。。 。。。
    /* 进行任务切换环境监测,包括“BUG: scheduling while atomic:***”检查*/
    schedule_debug(prev);
    。。。 。。。
}

1.1.2  schedule_debug()

       schedule_debug()函数中进行“BUG: scheduling while atomic:***”检查。

        首先通过in_atomic_preempt_off()检查当前环境是否属于原子操作,主要通过preempt_count() != PREEMPT_DISABLE_OFFSET进行判断,只有preempt_count计数等于PREEMPT_DISABLE_OFFSET(等于1)时,才能发生任务切换,否则认为处于原子操作中(in_atomic_preempt_off)。

        preempt_count可以理解为一个32位的per-CPU变量,不同架构中实现方法不同(arm64架构中,放在任务相关的thread_info结构体中),但都可以使用preempt_count()方法获取。preempt_count变量不同位分别表示PREEMPT_MASK、SOFTIRQ_MASK、HARDIRQ_MASK等,禁止/打开抢占、关/开中断等操作,都只对preempt count中对应的位进行操作,具体请查阅“include/linux/preempt.h”。

        如果当前处于原子操作,则调用__schedule_bug(prev),打印出异常信息,并且执行panic()。

        相关代码实现如下:

static inline void schedule_debug(struct task_struct *prev)
{
    。。。 。。。
    /*
     * 判断当前是否处于原子操作
     * preempt_count = PREEMPT_DISABLE_OFFSET,非原子操作,直接退出检查
     * preempt_count != PREEMPT_DISABLE_OFFSET 原子操作,输出异常及panic()
     */
    if (unlikely(in_atomic_preempt_off())) {
        /* 打印出异常信息,并执行panic() */
        __schedule_bug(prev);
        preempt_count_set(PREEMPT_DISABLED);
    }
    。。。 。。。
}
  • 1.1.3 __schedule_bug()

  •         出现问题后操作__schedule_bug(prev),打印出异常信息并执行panic操作,具体实现如下:
/*
 * Print scheduling while atomic bug:
 */
static noinline void __schedule_bug(struct task_struct *prev)
{
    。。。 。。。
    /* 1)打印出“BUG: scheduling while atomic:”相关信息*/
    printk(KERN_ERR "BUG: scheduling while atomic: %s/%d/0x%08x\n",
                        prev->comm, prev->pid, preempt_count());

    /* 2)异常出现后,打印出相关信息 */
    debug_show_held_locks(prev);
    print_modules();

    /* 3)如果配置panic_on_warn,则执行panic()动作 */
    if (panic_on_warn)
        panic("scheduling while atomic\n");

    /* 4)dump 调度栈 */
    dump_stack();
}

1.2 案例一:

1.2.1问题描述:

        客户应用场景下,设备异常重启,经查验log,发现出现“BUG: scheduling while atomic:”异常打印后,内核panic重启。

1.2.2 log

[ 2004.917769][ 14] BUG: scheduling while atomic: ktimersoftd/14/131/0x00000002
...  ...
[ 2004.917816][ 14] CPU: 14 PID: 131 Comm: ktimersoftd/14 Not tainted 4.19.90-25.2.rt.2101.gfb01.ky10.aarch64 #1
...  ...
[ 2004.917820][ 14] Call trace:
[ 2004.917829][ 14]  dump_backtrace+0x0/0x170
[ 2004.917830][ 14]  show_stack+0x24/0x30
[ 2004.917834][ 14]  dump_stack+0xa4/0xe8
[ 2004.917838][ 14]  __schedule_bug+0x68/0x88
[ 2004.917840][ 14]  __schedule+0x618/0x748
[ 2004.917841][ 14]  schedule+0x48/0x100
[ 2004.917844][ 14]  rt_spin_lock_slowlock_locked+0x110/0x2c0
[ 2004.917845][ 14]  rt_spin_lock_slowlock+0x54/0x70
[ 2004.917847][ 14]  rt_spin_lock+0x60/0x70
[ 2004.917850][ 14]  timerfd_triggered+0x24/0x60
[ 2004.917851][ 14]  timerfd_tmrproc+0x20/0x30
[ 2004.917854][ 14]  __hrtimer_run_queues+0xf8/0x388
[ 2004.917855][ 14]  hrtimer_run_softirq+0x78/0xf0
[ 2004.917858][ 14]  do_current_softirqs+0x1c4/0x3e8
[ 2004.917859][ 14]  run_ksoftirqd+0x30/0x50
[ 2004.917860][ 14]  smpboot_thread_fn+0x1a8/0x2a0
[ 2004.917862][ 14]  kthread+0x134/0x138
[ 2004.917864][ 14]  ret_from_fork+0x10/0x18
[ 2006.350914][ 14] Kernel panic - not syncing: Fatal exception

1.2.3 原因分析:

        通过log确认,原因为:在软中断处理线程中使用了timerfd定时器,timerfd中使用了可休眠的spin_lock。

        在Preempt RT linux中,软中断放在内核线程ksoftirqd中进行处理的,本来是可以休眠的,但是之前一个修改判断hrtimer定时器如果在软中断处理中,则使用raw_spin_lock对软中断处理进行锁定,这个操作会将preempt_count计数进行增加。

        而本次软中断处理线程run_ksoftirqd中,处理的软中断中ktimersoftd机制,又使用了可休眠的spin_lock,在spin_lock获取锁失败后,当前任务休眠发生任务切换,切换过程中__schedule()中,通过preempt_count() != PREEMPT_DISABLE_OFFSET,判断当前处于原子操作中,所以报出错误,panic后进行了重启操作。

1.2.4 解决方案

        Preempt RT linux改造方案中,将spin_lock的实现,改为了rt_spin_lock,从而使spin_lock变为可休眠,其本身和其保护的临界区都可以被抢占,进而可以保证系统的实时性。

        但是在linux内核的具体实现中,一些地方必须使用原子操作,这些地方是禁止休眠禁止任务切换的,这种情况下使用可休眠的spin_lock,便会出现问题。因此Preempt RT linux方案中,仍然保留了不可休眠不可抢占的spin_lock机制:raw_spin_lock,在一些禁止任务切换的场景下,需要使用raw_spin_lock来替换可抢占的spin_lock。

        解决方案:针对当前问题,在对应禁止休眠禁止抢占的区域,使用不可休眠的raw_spin_lock来保护临界区资源,而非可休眠的spin_lock。

        部分修改如下图:

另外,在非PREEMPT RT Linux中,中断顶半部和spinlock保护的临界区中必然是不能使用可能产生任务切换(主动休眠或者被动让出CPU)的函数。不过印象中,在中断顶半部实现中,如果使用可休眠的函数,编译是会直接报错的,不确定是否记错,有时间可以做下验证。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值