十八章 调试
一、内核调试概述
1.需要面对的
一个确定的bug
一个藏匿bug的内核版本
相关的内核代码的知识和运气
2.艰难的调试工作
重现bug很困难:大部分bug通常都不是行为可靠而且定义明确的。
确定bug最初出现的内核版本很困难:内核版本更新快,难以确定bug从哪个版本开始出现的。
3.内核中的bug
产生原因众多
表象变化多
从隐藏在源代码中的错误到展现在目击者面前的bug,往往都是经历一系列连锁反应的事件才可能触发。
内核调试事实上与其它大型的软件项目没有什么太大的不同。
内核有一些独特的问题需要考虑:例如定时限制和竞争条件等,都是允许多个线程在内核中同时运行产生的结果。
二、通过打印来调试
printk():内核的格式化打印函数
1.健壮性
弹性极佳的函数:任何时候、任何地方都能调用它
可以在中断上下文和进程上下文中被调用
可以在任何持有锁时被调用
可以在多处理器上同时被调用
除非再启动过程中的初期就要在终端上输出
2.日志等级
printk()和printf()在使用上最主要的区别就是前者可以指定一个日志级别。内核通过这个级别来判断是否在终端上打印消息。
内核把级别比某个特定值低的所有消息显示在终端上。
3.记录缓冲区
内核消息保存在一个LOG_BUF_LEN大小的环形队列中,读写都是按照环形队列方式操作的。
大小是可以在编译时通过CONFIG_LOG_BUF_SHIFT进行调整。
在单处理器的系统上默认值是16kb,即内核在同一时间只能保存16kb的内核消息,再多的话新消息就会覆盖老消息。
4.syslogd和klogd
这是两个用户空间的守护进程,klogd从记录缓冲区中获取内核消息,再通过syslogd守护进程将他们保存在系统日志文件中。
klogd:
既可以从/proc/kmsg文件中,也可以通过syslog()系统调用读取这些消息。
默认是/proc方式。
两种情况klogd都会阻塞,知道有新的内核消息可供读出,唤醒之后默认处理是将消息传给syslogd。
启动时可以通过-c标志来改变终端的记录等级
syslogd:
将它接收到的所有消息添加到一个文件中,默认是/var/log/messages。
三、oops
oops是内核告知用户有不幸发生的最常用的方式。
内核很难自我修复,也不能将自己杀死,只能发布oops,过程:
向终端上输出错误消息
输出寄存器中保存的信息
输出可供跟踪的回溯线索
通常发送完oops之后,内核会处于一种不稳定状态。
oops发生的时机:
中断上下文:内核无法继续,会陷入混乱,导致系统死机
在idle进程或init进程(0号进程和1号进程):内核无法继续,会陷入混乱,导致系统死机
在其他进程运行时,内核会杀死该进程并尝试着继续执行
四、探测系统
1.使用uid作为选择条件
一般情况下,加入特性时,只要保留原有的算法而把新算法加入到其他位置上,基本就能保证安全。
可以把用户id(UID)作为选择条件来实现这种功能,通过某种选择条件,安排到底执行哪种算法。
2.使用条件变量
如果代码与进程无关,或者希望有一个针对所有情况都能使用的机制来控制某个特性,可以使用条件变量。
只需要创建一个全局变量作为一个条件选择开关:如果该变量为0,就使用某一个分支上的代码;否则,选择另外一个分支。
操控方式:某种接口,或者调试器。
3.使用统计量
这种方法常用于使用者需要掌握某个特定事件的发生规律的时候。
方法是创建统计量,并提供某种机制访问其统计结果。