什么是OOPS
Oops是美国人比较常有的口语。就是有点意外,吃惊,或突然的意思。“oops”并不是很严重.对于linux内核来说,Oops就意外着内核出了异常,此时会将产生异常时出错原因,CPU的状态,出错的指令地址、数据地址及其他寄存器,函数调用的顺序甚至是栈里面的内容都打印出来,然后根据异常的严重程度来决定下一步的操作:杀死导致异常的进程或者挂起系统。
最典型的异常是在内核态引用了一个非法地址,通常是未初始化的野指针Null,这将导致页表异常,最终引发Oops。
Linux系统足够健壮,能够正常的反应各种异常。异常通常导致当前进程的死亡,而系统依然能够继续运转,但是这种运转都处在一种不稳定的状态,随时可能出问题。对于中断上下文的异常及系统关键资源的破坏,通常会导致内核挂起,不再响应任何事件。
内核的异常级别
内核的异常主要有三个级别,按照严重程度加深,可以分为BUG级->Oops级->Panic级。
BUG级:
Bug是指那些不符合内核的正常设计,但内核能够检测出来并且对系统运行不会产生影响的问题,比如在原子上下文中休眠,再内核中用BUG标识。
Oops
程序在内核态时,进入一种异常情况,比如引用非法指针导致的数据异常,数组越界导致的取指异常,此时异常处理机制能够捕获此异常,并将系统关键信息打印到串口上,正常情况下Oops消息会被记录到系统日志中去,可以通过journalctl -k命令查看.
Oops发生时,进程处在内核态,很可能正在访问系统关键资源,并且获取了一些锁,当进程由于Oops异常退出时,无法释放已经获取的资源,导致其他需要获取此资源的进程挂起,对系统的正常运行造成影响。通常这种情况,系统处在不稳定的状态,很可能崩溃。
Panic
当Oops发生在中断上下文中或者在进程0和1中,系统将彻底挂起,因为中断服务程序异常后,将无法恢复,这种情况即称为内核panic。另外当系统设置了panic标志时,无论Oops发生在中断上下文还是进程上下文,都将导致内核Panic。由于在中断复位程序中panic后,系统将不再进行调度,rsyslogd将不会再运行,因此这种情况下,Oops的消息仅仅打印到串口上,不会被记录在系统日志中。
oops要不要panic依据几个体哦见,分别是:
-
在中断中oops.
-
通过设置/proc/sys/kernel/panic_on_oops将panic_on_oops置位。
-
进程为0号或者1号进程.
kexec_should_crash(current)会判断进程是否为0号或者1号进程,如果是的话,oops会转变为panic.
panic的执行逻辑
流程中的细节要点:
-
代码尽其所能避免复杂性和可能的死锁
-
KERN_EMERG 内核打印信息 “Kernel panic - not syncing”
-
panic_print_sys_info();确定并显示更多系统信息–例如所有任务信息、内存、计时器、锁、ftrace信息和所有内核打印。具体取决于panic_print的bitmask
-
函数所做的最后一件事就是在单个启用的处理器核上无限循环;在循环中,它重置非屏蔽中断(NMI),然后定期调用一个名为架构依赖的panic_blink函数;在x86上,该事件会引起键盘LED会闪烁.
-
由于在函数入口处关闭了调度,所以控制台不会在有反应,系统卡死,只能冷上电。
oops要不要转成panic
前面说到,内核文件系统有一个节点/proc/sys/kernel/panic_on_oops,可以控制在oops执行结束前,要不要执行panic.
代码中的逻辑如下:
当检测到设置了panic_on_oops时,执行panic
打开/proc/sys/kernel/panic_on_oops测试,触发越界访问的oops导致的panic.PC直接挂死,电脑无反应。
CONFIG_PANIC_ON_OOPS_VALUE is the default value of this file node.
panic_timeout
panic_timeout通过 /proc/sys/kernel/panic 节点去设置,代表panic后等待几秒系统重启,默认情况为0代表不会重启,一直等待。
重启调用接口emergency_restart。
其它panic控制变量还包括如下例表中的项:
-rw-r--r-- 1 root root 0 2月 1 19:26 /proc/sys/kernel/panic
-rw-r--r-- 1 root root 0 2月 1 19:26 /proc/sys/kernel/panic_on_io_nmi
-rw-r--r-- 1 root root 0 2月 1 19:26 /proc/sys/kernel/panic_on_oops
-rw-r--r-- 1 root root 0 2月 1 19:26 /proc/sys/kernel/panic_on_rcu_stall
-rw-r--r-- 1 root root 0 2月 1 19:26 /proc/sys/kernel/panic_on_unrecovered_nmi
-rw-r--r-- 1 root root 0 2月 1 19:26 /proc/sys/kernel/panic_on_warn
-rw-r--r-- 1 root root 0 2月 1 19:26 /proc/sys/kernel/panic_print
这些参数不但可以通过sysctl建立的procfs去设置,还可以通过启动命令行控制。
以qemu为例,启动参数命令行中传入panic=88
qemu-system-arm -M vexpress-a9 -m 1024M -kernel arch/arm/boot/zImage -append "rdinit=/linuxrc console=ttyAMA0 loglevel=8 panic=88" -dtb arch/arm/boot/dts/vexpress-v2p-ca9.dtb -nographic
panic_timeout can also be set from config macro.int panic_timeout = CONFIG_PANIC_TIMEOUT;
驱动注册自定义的panic函数
驱动可以通过panic_notifier_list 链表注册自己的panic通知函数,示例代码如下:
#define pr_fmt(fmt) "%s:%s(): " fmt, KBUILD_MODNAME, __func__
#include <linux/init.h>
#include <linux/module.h>
#include <linux/notifier.h>
#include <linux/delay.h>
static void dev_ring_alarm(void)
{
pr_emerg("!!! ALARM !!!\n");
}
static int mypanic_handler(struct notifier_block *nb, unsigned long val, void *data)
{
//执行的函数
dev_ring_alarm();
dump_stack();
return NOTIFY_OK;
}
//结构体
static struct notifier_block mypanic_nb = {
.notifier_call = mypanic_handler,//回调函数
// .priority = INT_MAX
};
static int __init panic_notifier_init(void)
{
//注册函数
atomic_notifier_chain_register(&panic_notifier_list, &mypanic_nb);
pr_info("Registered panic notifier.\n");
return 0;
}
static void __exit panic_notifier_exit(void)
{
atomic_notifier_chain_unregister(&panic_notifier_list, &mypanic_nb);
pr_info("Unregistered panic notifier\n");
}
module_init(panic_notifier_init);
module_exit(panic_notifier_exit);
MODULE_LICENSE("Dual MIT/GPL");
panic_timeout
the panic_timeout reprents the delay seconds before reboot during panic.
![](https://i-blog.csdnimg.cn/blog_migrate/58225ada8bdc73de60345c408c7e6479.png)
it can be set by config coniguration "CONFIG_PANIC_TIMEOUT" or "/proc/sys/kernel/panic" file.
take above for example, in gui mode it will delay 100ms before reboot after panic was triggered.
in console mode you will get the last callstack before panic, which will not be recorded in system log file kern.log.
让PANIC不再PANIC
panic内陷入死循环
再进一步,仍然是死循环,可以看到打印,
这是因为还没有执行到下一步的smp_send_stop或者crash_smp_send_stop,其它的CPU仍然可以调度DMESG输出LOG信息。
如果把打印放到下面则将直接重启,不会得到任何打印,如上面所说的,此时所有的核都已经挂了。
置于panic中的一些打印,不会输出到系统日志的原因也清楚了,由于系统所有的核都被STOP,所以日志进程不再被调度。自然LOG也不会被记录进LOG文件。
经过测试,如果我们注释掉下面这几行,panic_timeout将会生效。说明之前的测试,可能在进入panic_timeout前进入了导致重启的其他流程。
panic尾部设置的suppress_printk也会禁止printk继续往__log_buf写数据的操作。
这样将会在如下打印接口的位置禁止打印
这样设置suppress_printk和关闭调度,分别从源头上和接收端禁止了后续打印的输出。
kmsg_dump is related to pstore mechanism, please refer Candy.
notify_die
内核page fault调用堆栈
无论哪种HANG死,系统最后一口气一般都要执行__die函数,执行die的原因有多种,以X86为例,定义如下
其它内核调试技巧
1.console=tty0 nomodeset
kernel printk will show on the display.
2.earlyprintk=vga,keep
earlyprintk is useful when the kernel crashes before the normal console is initialized.
3.panic = n
timeout > 0: seconds efore rebooting
timeout=0: wait forever
timeout < 0; reboot immediately.
console commands
4.sysctl -a : list all kernel parameters.
5. sysctl variable=value : set sysctl variable.
1):sysctl kernel.printk "7 7 7 7"
2):sysctl kernel.panic_on_oops = 1
3):sysctl kernel.panic_on_rcu_stall=1
4):sysctl kernel.panic_on_unrecovered_nmi = 1
5):sysctl kernel.softlockup_panic = 1
6):sysctl kernel.panice_on_io_nmi =1
7):sysctl kernel.unknown_nmi_panic=1
8):sysctl kernel.oops_all_cpu_backtrace=1
9):sysctl kernel.hardlockup_panic=1
10):sysctl kernel.hardlockup_all_cpu_backtrace=1
参考文章
dmesg&printk的工作原理_屏蔽dmesg打印_papaofdoudou的博客-CSDN博客