3.4.1.1. linux oops问题分析

Oops是当内核运行时出现严重的错误时,输出的一段包含丰富信息的错误报告,oops可以看成内核级的Segmentation Fault. 例如空指针引用、 非法内存访问、内核堆栈溢出等无法正常处理的错误情况时,就会输出Oops信息

这些信息涵盖了导致出现错误的代码位置,以及当时的寄存器状态,堆栈信息等

3.4.1.1.1. linux内核三种异常

linux内核三种异常: BUGoopscrash

3.4.1.1.1.1. BUG

BUG是指那些不符合内核的正常设计,但内核能够检测出来并且对系统运行不会产生影响的问题,比如在原子上下文中休眠,在内核中使用BUG标识.

这里的BUG和软件缺陷不是一回事,其实是kernel中用于拦截内核程序超出预期的行为,属于软件主动汇报异常的一种机制。一般来说有两种用到的情况

  • 软件开发中,若发现代码逻辑出现致命fault后就可以调用BUG()来让kernel死掉,这样方便定位问题

  • 另一种情况是由于某种特殊原因(通常是为了debug而需要抓ramdump),我们需要 进入kernel panic情况下使用

arm64中BUG()定义如下

#define __BUG_FLAGS(flags)                          \
    asm volatile (__stringify(ASM_BUG_FLAGS(flags)));

#define BUG() do {                                  \
    __BUG_FLAGS(0);                                 \
    unreachable();                                  \
} while (0)

__BUG_FLAGS(0)可翻译为 brk 0x800

brk指令会触发一个同步异常,内核异常处理流程中会进入 panic() ,从而导致kernel panic

3.4.1.1.1.2. oops

Oops就意味着内核出现异常,此时会将异常时出错的原因,CPU状态、出错的指令地址、数据地址及其他寄存器,函数调用的顺序 甚至栈里面的内容打印出来

例如在编写驱动或者内核模块时,当对指针进行非法取值时导致内核发生一个oops错误。

arch/arm64/mm/fault.c
static void die_kernel_fault(const char *msg, unsigned long addr,
     unsigned int esr, struct pt_regs *regs)
{
     bust_spinlocks(1);

     pr_alert("Unable to handle kernel %s at virtual address %016lx\n", msg,
     addr);

     mem_abort_decode(esr);

     show_pte(addr);
     die("Oops", regs, esr);
     bust_spinlocks(0);
     do_exit(SIGKILL);
}

通过die()会进行oops异常处理。

void die(const char *str, struct pt_regs *regs, int err)
{
    int ret;
    unsigned long flags;

    raw_spin_lock_irqsave(&die_lock, flags);

    oops_enter();

    console_verbose();
    bust_spinlocks(1);
    ret = __die(str, err, regs);

    if (regs && kexec_should_crash(current))
        crash_kexec(regs);

    bust_spinlocks(0);
    add_taint(TAINT_DIE, LOCKDEP_NOW_UNRELIABLE);
    oops_exit();

    if (in_interrupt())
        panic("Fatal exception in interrupt");
    if (panic_on_oops)
        panic("Fatal exception");

    raw_spin_unlock_irqrestore(&die_lock, flags);

    if (ret != NOTIFY_STOP)
        do_exit(SIGSEGV);
}
static int __die(const char *str, int err, struct pt_regs *regs)
{
    static int die_counter;
    int ret;

    pr_emerg("Internal error: %s: %x [#%d]" S_PREEMPT S_SMP "\n",
         str, err, ++die_counter);

    /* trap and error numbers are mostly meaningless on ARM */
    ret = notify_die(DIE_OOPS, str, regs, err, 0, SIGSEGV);
    if (ret == NOTIFY_STOP)
        return ret;

    print_modules();
    show_regs(regs);

    dump_kernel_instr(KERN_EMERG, regs);

    return ret;
}
  • notify_die会通知所有对oops感兴趣的模块并进行callback

  • print_modules打印模块状态不为MODULE_STATE_UNFORMED的模块信息

  • show_regs打印PC、LR、SP等寄存器信息,同时打印调用堆栈信息

  • dump_kernel_instr打印PC指针和前4条指令

3.4.1.1.1.3. panic

panic本身时”恐慌”的意思,这里指的是kernel发生了致命错误导致无法继续进行下去的情况。根据实际情况Oops最终也可能会导致panic的发生

void panic(const char *fmt, ...)
{
    static char buf[1024];
    va_list args;
    long i, i_next = 0, len;
    int state = 0;
    int old_cpu, this_cpu;
     bool _crash_kexec_post_notifiers = crash_kexec_post_notifiers;

    // 禁止本地中断,避免出现死锁,因为无法防止中断处理程序(在获取panic锁后运行),再次调用panic
    local_irq_disable();
    //禁止任务抢占
    preempt_disable_notrace();
    //通过this_cpu确认是否调用panic的cpu是否为panic_cpu
    this_cpu = raw_smp_processor_id();
    old_cpu  = atomic_cmpxchg(&panic_cpu, PANIC_CPU_INVALID, this_cpu);

    if (old_cpu != PANIC_CPU_INVALID && old_cpu != this_cpu)
        panic_smp_self_stop();
    //把console的打印级别放开
    console_verbose();
    bust_spinlocks(1);
    va_start(args, fmt);
    len = vscnprintf(buf, sizeof(buf), fmt, args);
    va_end(args);

    if (len && buf[len - 1] == '\n')
        buf[len - 1] = '\0';
    //解析panic所携带的mesage,
    pr_emerg("Kernel panic - not syncing: %s\n", buf);
#ifdef CONFIG_DEBUG_BUGVERBOSE
    /*
     * Avoid nested stack-dumping if a panic occurs during oops processing
     */
    if (!test_taint(TAINT_DIE) && oops_in_progress <= 1)
        dump_stack();
#endif

    //如果kgdb使能,在停掉所有的CPU之前,跳转kgdb断点云南行kgdb_panic
    kgdb_panic(buf);

    //根据当前是否设置了转储内核 (使能CONFIG_KEXEC_CORE)确定是否实际执行转储操作
    //如果执行转储操作会通过kexec将系统切换到新的kdump内核 ,并不会返回
    if (!_crash_kexec_post_notifiers) {
        printk_safe_flush_on_panic();
        __crash_kexec(NULL);

        smp_send_stop();
    } else {
        //停掉其他CPU,只留当前CPU干活
        crash_smp_send_stop();
    }

    //通知所有对panic感兴趣的模块进行回调,添加一些kmsg信息到输出
    atomic_notifier_call_chain(&panic_notifier_list, 0, buf);

    /* Call flush even twice. It tries harder with a single online CPU */
    printk_safe_flush_on_panic();
    //dump内核log buffer中的log信息
    kmsg_dump(KMSG_DUMP_PANIC);

    if (_crash_kexec_post_notifiers)
        __crash_kexec(NULL);

#ifdef CONFIG_VT
    unblank_screen();
#endif
    console_unblank();

    //关掉所有debug锁
    debug_locks_off();
    console_flush_on_panic(CONSOLE_FLUSH_PENDING);

    panic_print_sys_info();

    if (!panic_blink)
        panic_blink = no_blink;
    //如果配置了panic_timeout则在超时后重启系统
    if (panic_timeout > 0) {
        pr_emerg("Rebooting in %d seconds..\n", panic_timeout);

        for (i = 0; i < panic_timeout * 1000; i += PANIC_TIMER_STEP) {
            touch_nmi_watchdog();
            if (i >= i_next) {
                i += panic_blink(state ^= 1);
                i_next = i + 3600 / PANIC_BLINK_SPD;
            }
            mdelay(PANIC_TIMER_STEP);
        }
    }
    if (panic_timeout != 0) {
        if (panic_reboot_mode != REBOOT_UNDEFINED)
            reboot_mode = panic_reboot_mode;
        emergency_restart();
    }
#ifdef __sparc__
    {
        extern int stop_a_enabled;
        /* Make sure the user can actually press Stop-A (L1-A) */
        stop_a_enabled = 1;
        pr_emerg("Press Stop-A (L1-A) from sun keyboard or send break\n"
             "twice on console to return to the boot prom\n");
    }
#endif
#if defined(CONFIG_S390)
    disabled_wait();
#endif
    pr_emerg("---[ end Kernel panic - not syncing: %s ]---\n", buf);

    /* Do not scroll important messages printed above */
    suppress_printk = 1;
    local_irq_enable();
    for (i = 0; ; i += PANIC_TIMER_STEP) {
        touch_softlockup_watchdog();
        if (i >= i_next) {
            i += panic_blink(state ^= 1);
            i_next = i + 3600 / PANIC_BLINK_SPD;
        }
        mdelay(PANIC_TIMER_STEP);
    }
}

备注

  • oops发生时,内核检测到发生了无法恢复的错误,但整个系统可能仍然能够继续运行。典型触发场景为 空指针解引用非法内存访问某些驱动程序错误 . 会产生oops日志,可以通过dmesg看到,如果oops发生在内核关键路径,可能导致内核奔溃

  • panic是比oops更严重的错误,表示系统进入不可恢复的状态,需要立即停止运行。典型触发场景为 多次oops触发panic关键内核数据结构破环panic显示调用