内核调试(二)
记录缓冲区
内核消息都被保存在一个LOG_BUF_LEN大小的环形队列中。该缓冲区大小可以在编译时通过CONFIG_LOG_BUF_SHIFT进行调整。在单处理器的系统上其默认值是16K字节。换句话说,就是内核在同一时间只能保存16K字节的内核消息。如果它已经放满而又接收到了新的消息,旧消息就会被覆盖。这个记录缓冲区之所以称为环形是因为它的读写都是按照环形队列方式进行操作的。
使用环形队列有许多好处。由于读写环形队列时同步问题非常容易解决,所以即使在中断上下文中也可以方便的使用printk()。此外,它使记录维护起来也更容易。如果有大量的消息同时产生,新消息只需覆盖掉旧消息即可。在某个问题引发大量消息的时候,记录只会覆盖掉它本身,而不会因为失控而消耗掉大量内存。而环形缓冲区的唯一缺点——可能会丢失消息——带来的损失和在简单性和健壮性上的好处相比,完全可以忽略不计。
syslogd和klogd
在标准的Linux系统上,用户空间的守护进程klogd从记录缓冲区中获取内核消息,再通过syslogd守护进程将它们保存在系统日志文件中。klogd程序既可以从/proc/kmsg文件中也可以通过syslog()系统调用读取这些消息。默认情况下,它选择读取/proc方式实现。不管是那种方法,直到有新的内核消息可供读出,klogd都会阻塞。在被唤醒之后,它会读取出新的内核消息并进行处理。默认方式下,处理例程就是把消息传给syslogd守护进程。
syslogd守护进程把它接收到的所有消息加进一个文件中,该文件默认是/var/log/messages。你也可以通过/etc/syslog.conf配置文件重新指定。
在载入klogd的时候,可以通过指定-c标志来改变终端的记录等级。
printk()和内核开发时需要留意的一点
当你刚开始开发内核代码的时候,往往会把printk()敲成printf()。这很正常,你没法抗拒多年来在用户级程序中使用printf()的习惯。好在这种错误不会持续很长时间,反复出现的链接错误很快就会让在心烦意乱中开始培养新的习惯。
终于有一天,在编写用户级程序的时候,你敲printf()的时候不小心敲成了printk()。恭喜你,你成为一个真正的内核黑客的时刻终于到来了。
Oopsoops是内核告知用户有不幸发生的最常用的方式。由于内核是整个系统的管理者,所以它不能采取像在用户空间出现运行错误时使用的那些简单手段,因为它很难自行修复,它也不能将自己杀死。内核只能发布oops。这个过程包括向终端上输出错误消息,输出寄存器中保存的信息并输出可供跟踪的回溯线索。内核中出现的故障很难处理,所以内核往往要经历严峻的考验才能发送出oops和靠它自己完成的一些清理工作。通常,发送完oops之后,内核会处于一种不稳定状态。举例来说,oops发生的时候内核可能正在处理非常重要的数据。它可能持有一把锁或正在和硬件设备交互。内核必须适当的从当前的上下文环境中退出并尝试恢复对系统的控制。多数时候,这种尝试都会失败。因为如果oops在中断上下文时发生,内核根本无法继续,它会陷入混乱。混乱的结果就是系统死机。如果oops在idle进程(pid为0)或init进程(pid为1)时发生,结果同样是系统陷入混乱,因为内核缺了这两个重要的进程根本就没法工作。不过,要是oops在其它进程运行时发生,内核就会杀死该进程并尝试着继续执行。
oops的产生有很多可能原因,其中包括内存访问越界或者非法的指令等。作为一个内核开发者,你将会经常处理(毫无疑问,也将导致)oops。
紧接着的是一个oops的实例,它是在一台PPC机器上的tulip网卡的定时器处理函数运行时发生的:
Oops: Execption in kernel mode, sig : 4
Unable to handle kernel NULL pointer derefernece at virtual address 00000001
NIP C013A7F0 LR:C013A7F0 SP:C0685E00 REGS: c0905d10 TRAP 0700
Not tainted
MSR : 0089037 EE:1 PR: 0 FP:0 ME: 1 IR/DR : 11
TASK = c0712530[0] ‘swapper’ last syscall :120
GPR00:C013A7C0 C0295E00 C0231530 0000002F 00000001 C0380CB8 C0291B80 C02D0000
GPR08:000012A0 00000000 00000000 C0292AA0 4020A088 00000000 00000000 00000000
GPR16:00000000 0000000000000000 00000000 00000000 00000000 00000000 00000000
GPR24:00000000 00000005 00000000 00001032 C3F7C000 00000032 FFFFFFFF C3F7C1C0
Call trace:
[c013ab30] tulip_timer+0x128/0x1c4
[c0020744] run_timer_softirq+0x10c/0x164
[c001b864] do_softirq+0x88/0x104
[c0007e80] timer_interrupt+0x284/0x298
[c00033c4] ret_from_except+0x0/0x34
[c0007b84] default_idel+0x20/0x60
[c0007bf8] cpu_idle+0x34/0x38
[c0003ae8] rest_init+0x24/0x34
使用PC的读者可能对这么多的寄存器感到惊奇(居然有32个之多)。你可能对X86系统更熟悉一些,在这种系统上,oops会简单一点。但是,oops中包含的重要的信息对于所有的体系结构都是完全相同的:寄存器上下文和回溯线索。
回溯线索显示了导致错误发生的函数调用链。这样我们就可以观察究竟发生了什么:机器处于空闲状态,正在执行idle循环,由cpu_idle()反复调用default_idle()。此时定时器中断产生了,它引起了对定时器的处理。tulip_timer()这个定时器处理函数被调用,而就是它引用了空指针。你甚至可以通过偏移量(像0x128/0x1c4这些出现在函数左侧的数字)找出导致问题的语句。
寄存器上下文信息可能同样有用,尽管使用起来不那么方便。如果你有函数的汇编代码,这些寄存器数据可以帮助你重建引发问题的现场。在寄存器中发现一个本不应该出现的数值可能会在黑暗中给你带来第一丝光明。在上面的例子中,我们可以查看是哪个寄存器包含了NULL(一个所有位都为零的数值)进而找出是函数的哪个变量的值不正常。一般在这种情况下问题往往是竞争引起的——本例中,是定时器和这块网卡驱动的其它部分之间的竞争。调试一个竞争条件往往很有挑战性。
ksymoops前面列举的oops可以说是一个经过解码的oops,因为内存地址都已经被转换成了它们对应的函数。下面是其未解码版本:
NIP C013A7F0 LR:C013A7F0 SP:C0685E00 REGS: c0905d10 TRAP 0700
Not tainted
MSR : 0089037 EE:1 PR: 0 FP:0 ME: 1 IR/DR : 11
TASK = c0712530[0] ‘swapper’ last syscall :120
GPR00:C013A7C0 C0295E00 C0231530 0000002F 00000001 C0380CB8 C0291B80 C02D0000
GPR08:000012A0 00000000 00000000 C0292AA0 4020A088 00000000 00000000 00000000
GPR16:00000000 0000000000000000 00000000 00000000 00000000 00000000 00000000
GPR24:00000000 00000005 00000000 00001032 C3F7C000 00000032 FFFFFFFF C3F7C1C0
call trace: [c013ab30] [c0020744] [c001b864] [c0007e80] [c00033c4] [c0007b84] [c0007bf8] [c0003ae8]
回溯线索中的地址需要被转化成有意义的符号名称才方便使用。这需要调用ksymoops命令,并且还必须提供编译内核时产生的System.map。如果你使用的是模块,你还需要一些模块信息。ksymoops通常会自行解析这些信息,所以一般你可以这样调用它:
ksymoops saved_oops.txt
然后该程序就会吐出解码版的oops。如果ksymoops无法找到默认位置上的信息,或者你想提供不同信息,该程序可以接受许多参数。它的使用手册上提供了许多说明信息,使用之前你最好先行查阅。
ksymoops一般会随你得到的Linux发行提供。
Kallsyms
谢天谢地,现在已经无须使用ksymoops工具了,这是一个了不起的工作。因为尽管开发者使用它的时候一般很少出现问题,但是最终用户常常会错误地匹配System.map文件或错误地对oops进行解码。
开发版的2.5内核引入了kallsyms特性,它可以通过定义CONFIG_KALLSYMS配置选项启用。该选项可以载入内核镜像对应的内存地址的符号的名称,所以内核可以打印解码好的跟踪线索。相应的,解码oops也不再需要System.map或者ksymoops工具了。硬币总有另一面,这样做会使内核变大一些,因为地址对应的符号名称必须始终驻留在内核所在的内存上。占用这些内存是值得的,至少在开发过程中如此。