分类: LINUX
2012-02-29 12:17:43
内核在发生严重错误的时候要“X屏”一下,以windows为例就是蓝屏,而 linux却是黑屏,windows仅仅提供一个出错码和对应地址,十分难以 理解,而linux却可能提供整个寄存器和堆栈,它不隐瞒一切,代码公开,当然出错时的环境就没有必要隐瞒了,首先看一下panic:
NORET_TYPE void panic(const char * fmt, ...)
{
long i;
static char buf[1024];
va_list args;
preempt_disable(); //关闭抢占,否则此cpu执行到后面的死循环时还是有可能被抢占的,被抢占意味着可以切换到别的进程,但是在panic里,这是不希望的,企图出去 panic函数的任何行为都是不希望的。注意,这里关闭了抢占就再也不打开了。
bust_spinlocks(1);
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
printk(KERN_EMERG "Kernel panic - not syncing: %s\n",buf);
bust_spinlocks(0);
crash_kexec(NULL);
#ifdef CONFIG_SMP
smp_send_stop();//要求别的cpu执行halt,实际上就是停止了别的cpu,而本cpu即导致panic的cpu最终将死循环。
#endif
atomic_notifier_call_chain(&panic_notifier_list, 0, buf);//处理panic通知链,做好善后工作。
if (!panic_blink)
panic_blink = no_blink;
if (panic_timeout > 0) { //重启延时如果不为0则倒计时重启。
printk(KERN_EMERG "Rebooting in %d seconds..",panic_timeout);
for (i = 0; i < panic_timeout*1000; ) { //只能用这种方式来倒计时了,进入panic意味着出现严重错误,因此最好呆在panic里面,不要触及外面的任何东西,timer这时可能已经不好用 了,因此就用这种方式吧,否则将导致更严重的错误。
touch_nmi_watchdog();
i += panic_blink(i);
mdelay(1);
i++;
}
emergency_restart(); //重新启动
}
local_irq_enable(); // 打开了中断,因此在下面的死循环里还是可以响应中断的,但是如果是在中断上下文中引起panic,即in_interrupt()为真的情况引 起 panic的话,虽然硬件中断可以响应但是软中断就不可响应了,因为软中断执行时in_interrupt()必须为假才行。
for (i = 0;;) {
touch_softlockup_watchdog();
i += panic_blink(i);
mdelay(1);
i++;
}
}
由 上述panic可以看出,panic并没有什么大不了的,就是禁抢占,善后,禁止了抢占实际上就禁止了调度,因为禁止抢占的情况下除非自己放弃cpu才能 调度,但是我们看看那个死循环根本没有放弃cpu的意思。发生panic无非就两种情形,一个是在中断上下文,另一个是在非中断上下文。在中断上下文中 panic的话,此时的in_interrupt()一定返回真,如果此硬件中断触发了一个软中断,那么这个软中断是永远不会被执行的,因为导致 panic的中断永远不会调用irq_exit来使得in_interrupt()返回假。这就解释了为何在中断中panic的系统ping不通,而在非 中断上下文中panic虽然内核死掉了但是还是可以从外部ping通的。想想ping的执行,ping依靠的是icmp协议,没有进程上下文,它是ip层 的内容,而ip层是不区分进程的,于是icmp的处理只在软中断上下文进行,既然硬件中断中panic了,根据上面的论述,软中断就没有机会执行了,于是 就ping不通了;而非中断的panic由于软中断可以执行,所以可以ping通,但是内核还是不能恢复了,因为已经关闭可抢占,无法调度了,于是执行流 出去中断后便会一直执行那个死循环,没有机会执行别的。
这就是panic,从字面意义上就是“惊慌”的意思,如果一个人惊慌了,那么他的行为无非两种,一是失控地四处乱跑,而是在原地惊呆,内核是绝对不会选择 前者的,如果一个人四处乱窜的话,一定会有人帮他镇静,比如警察或者他的家人,要么就是医生,最终还是他要呆在一个地方静止,内核当然是一步到位了,直接 不允许乱窜,人在乱窜的时候十有八九会导致事故,比如推翻了桌子,而桌子上有一只很贵的花瓶,内核也考虑到了,为了免得次生破坏,那么直接呆在原地不要动 了。因此你看不见任何出去panic的路子。当然会有一些提示,用来提示发生了严重错误,内核panic,类似于拨打了120电话,在一些平台上就是键盘 灯不停地闪烁。那么在代码中怎么体现呢?可以肯定的是,键盘闪烁一定是死循环中调用的函数的行为,而这个死循环如此之短使得把它揪出来很容易,看看 panic_blink函数,它是一个函数指针,不同的平台指向的函数不同,在大多数x86的pc上就是i8042_panic_blink:
static long i8042_panic_blink(long count)
{
long delay = 0;
static long last_blink;
static char led;
if (!i8042_blink_frequency)
return 0;
if (count - last_blink < i8042_blink_frequency)
return 0;
led ^= 0x01 | 0x04;
while (i8042_read_status() & I8042_STR_IBF)
DELAY;
i8042_write_data(0xed); /* set leds */
DELAY;
while (i8042_read_status() & I8042_STR_IBF)
DELAY;
DELAY;
i8042_write_data(led);
DELAY;
last_blink = count;
return delay;
}
这 个函数还用说吗?只要知道它的作用就完美了,硬件相关的东西如果你没有那个硬件的手册或者是你根本就不需要做那个硬件的相关的东西就没有必要浪费时间去研 究,只要知道接口的意义就可以了。在那个死循环里还有一个重要的调用涉及到一个nmi的看门狗,nmi在内核挂起从而需要调试的时候很有用,nmi是 non maskable interrupt的意思,就是不可屏蔽中断,试想当内核挂起,键盘不再响应,甚至中断都不再执行或者无法输出堆栈信息的时候,nmi实际上就有用 了,nmi中断无论如何都要发生,从而nmi的中断处理函数无论如何都要执行,那么就可以在这个中断处理中输出调试信息或进行转储,它可能会调用die函 数,然后引发一个panic,最终输出信息或转储,至于具体代码就不说了。记住,如果发生oops,那么还有一招可以用,就是Alt+Sysrq+X组合 键,X为具体命令,请查手册,比如X为m就是导出内存信息,但是如果组合键都不响应了,那么请看看你的内核是否彻底关中断锁住了,还有就是看看组合键是否 在软中断上下文被处理而导致panic的是在硬件中断上下文,起码在2.6内核引入input_dev后,这种情况成为了可能,人们为了追求和谐与统一可 以把一切键盘码的处理都放到软中断,但是却导致了组合键在oops中无法响应的问题
以上谈了panic,下面就看看BUG_ON,这也是一个panic点,但是却不是一定要panic,只有在平台不提供bug验证代码的时候才会调用 panic,因为无法处理这个哪怕小小的bug,那么就可能引发大大的崩溃,而何时引发以及危害如何是不确定的,因此还不如直接panic,停止内核,将 以下的事情交给调试人员。看看代码:
#ifndef HAVE_ARCH_BUG_ON
#define BUG_ON(condition) do { if (unlikely((condition)!=0)) BUG(); } while(0)
#endif
以上视为平台没有提供BUG_ON,以下为平台提供了BUG_ON
#ifndef HAVE_ARCH_BUG
#define BUG()
#endif
对于没有提供BUG_ON的平台,内核的定义为在满足条件的时候调用BUG,而BUG定义如下:
#ifndef HAVE_ARCH_BUG
#define BUG() do { \
printk("BUG: failure at %s:%d/%s()!\n", __FILE__, __LINE__, __FUNCTION__); \
panic("BUG!"); \
} while (0)
#endif
可以看出最终调用了panic,内核就此game over,而对于平台的BUG_ON,这里以i386为例:(2.6.17内核)
#define BUG() __asm__ __volatile__("ud2\n")
ud2 在x86上是无效指令,执行之会引发错误从而输出堆栈或转储,但是看出问题了吗?仅仅一条无效指令导致堆栈回溯,然后呢?然后当然就恢复正常了,这难道是 希望的行为吗?出错信息显然导到终端,但是是否所有人都会在意终端的输出呢?更好的做法应该是一旦BUG()被调用,立马挂起内核,像panic那样,但 是不善后。实际上内核中的任何哪怕非常小的bug都可能导致严重的后果,因此不要忽略任何bug,并且不要放过它。于是在2.6.20以及以后的内核的 BUG宏的定义就改变了:
#define BUG() \
do { \
asm volatile("ud2"); \
for(;;) ; \
} while(0)
但是还是有问题,就是for循环在抢占打开的情况下可以被抢占,不过这个问题看似没有那么严重,毕竟严重错误的时候内核就直接panic了,而BUG只是一些不希望的事情确实发生了时候才调用的,最起码它停掉了一条执行流,该条执行流将永远堵在for循环里,万劫不复!
stolen from:http://blog.csdn.net/zhang_shuai_2011/article/details/6706735
many thanks!