《Linux内核设计与实现》第五周读书笔记——第十一章
第18章 调试
调试工作艰难是内核级开发区别于用户级开发的一个显著特点,相比于用户级开发,内核调试的难度确实要艰苦得多。更可怕的是,它带来的风险比用户级别更高,内核的一个错误往往立刻就能让系统崩溃。
驾驭内核调试的能力(当然,最终是为了能够成功地开发内核)很大程度上取决于经验和对整个操作系统的把握。没错,玉树临风可能会对别的事情有帮助,但是调试内核的关键还是在于你对内核的深刻理解,然而我们必须找到可以开始着手的地方所以,在这―章里我们从调试内核的一种可能步骤开始。
18.1 准备开始
一个bug
一个藏匿bug的内核版本
知道这个bug最早出现在哪个内核版本中。
相关内核代码的知识和运气
想要成功进行调试:
让这些错误重现
抽象出问题
从代码中搜索
18.2 内核中的bug
内核中的bug多种多样,它们的产生可以有无数的原因,同时它们的表象也变化多端,从明白无误的错误代码(比如,没有把正确的值存放在恰当的位置)到同步时发生的错误(比如共享变量锁定不当)再到错误地管理硬件(比如,给错误的控制寄存器发送错误的指令)。从降低所有程序的运行性能到毁坏数据再到使得系统处于死锁状态,都可能是bug发作时的症状。
18.3 通过打印来调试
18.3.1 健壮性
它是一个弹性极佳的函数,这一点相当重要。printk()之所以这么有用,就在于它随时都能被调用。printk()函数的健壮躯壳下也难免会有漏洞,在系统启动过程中.终端还没有初始化之前在某些地方不能使用它。不过说实在的,如果终端没有初始化,你又能输出到什么地方去呢?这―般不是一个什么问题。除非你要调试的是启动过程最开始的那些步骤(比如说在负责执行硬件体系结构相关的初始化动作的函数中)下进行这样的调试挑战性很强没有任何打印函数能用,确实让问题更加棘手。
健壮性即在任何时候,任何地方都能调用它。
在中断上下文和进程上下文中被调用
在任何持有锁时被调用
在多处理器上同时被调用,并且不必使用锁。
printk()有用之处在于它随时都能被调用。
注意:该函数在终端没有初始化之前,某些地方不能用它。这种情况,需要用early_printk()代替,它在启动初期就可以在终端上打印,两者功能完全相同,但early_printk()缺少可移植性。
18.3.2 日志等级
printk()和printf()在使用上最主要的区别就是前者可以指定一个日志级别。
内核根据这个级别来判断是否在终端上打印消息。
内核把级别比某个特定值低的所有消息显示在终端上。
18.3.3 记录缓冲区
内核消息都被保存在一个LOG_BUF_LEN大小的环形队列中。该缓冲区大小可以在编译时通过设置CONFIG_LOG_BUF_SHIFT进行调整。在单处理器的系统上其默认值是16KB。换句话说,就是内核在同一时间只能保存16KB的内核消息。如果消息队列已经达到最大值,那么如果再有printk()调用时,新消息将覆盖队列中的老消息。这个记录缓冲区之所以称为环形是因为它的读写都是按照环形队列方式进行操作的。
使用环形队列有许多好处。由于同时读写环形缓冲区时,其同步问题很容易解决,所以即使在中断上下文中也可以方便地使用printfk()。此外,它使记录维护起来也更容易。如果有大量的消息同时产生,新消息只需覆盖掉旧消息即可。在某个问题引发大量消息的时候。记录只会覆盖掉它本身,而不会因为失控而消耗掉大量内存。而环形缓冲区的唯一缺点——可能会丢失消息但是与简单性和健壮性的好处相比这点代价是值得的。
18.3.4 syslogd和klogd
用户空间的守护进程klogd从记录缓冲区中获取内核消息,再通过syslogd守护进程将他们保存在系统日志文件中。
(1)klogd
- 既可以从/proc/kmsg文件中,也可以通过syslog()系统调用读取这些消息。默认选择读取/proc方式实现。
- 两种方法klogd都会阻塞,直到有新的内核消息可供读出,唤醒之后将消息传给syslogd。
- 启动时,可以通过-c标志来改变终端的记录等级。
(2)syslogd
- 将它接收到的所有消息添加到一个文件中,该文件默认是/var/log/messages。
- 也可以通过/etc/syslog.conf配置文件重新指定。
18.3.5 从printf()到printk()的转换
18.4 oops
oops是内核告知用户有不幸发生的最常用的方式,oops包括:
- 输出错误信息
- 重点:输出寄存器中保存的信息(寄存器上下文)并输出可供跟踪的回溯线索(显示了导致错误发生的函数调用链)
发送完oops后,内核会处于一种不稳定状态,如果oops在idle进程(pid=0)或init进程(pid=1)时发生,系统将陷入混乱,若是其他进程,内核会杀死该进程并尝试继续执行。
ksymoops命令:将回溯线索中的地址转化为有意义的符号名称,必须提供编译内核时产生的System.map,ksymoops会自行解析,得到解码版:
ksymoops saved_oops.txt
kallsyms特性:内核引入kallsyms特性,通过定义CONFIG_KALLSYMS配置选项启用,存放着内核镜像中相应函数地址的符号名称,内核可以打印解码好的跟踪线索。不再需要System.map或ksymoops了。
18.4.1 ksymoops
回溯线索中的地址需要转化成有意义的符号名称才方便使用,这需要调用ksymoops命令。并且还必须提供编译内核时产生的System.map。如果使用的是模块,还需要一些模块信息。
然后该程序就会吐出解码版的oops。如果ksymoops无法找到默认位置上的信息,或者想提供不同信息,该程序可以接受许多参数。ksymoops的使用手册上提供了许多说明信息,使用之前最好先行查阅。ksymoops一般会随Linux发行版本提供。
18.4.2 kallsyms
谢天谢地,现在已经无须使用ksymoops工具了。使这是一个了不起的工作。
这样做会使内核变大一些,因为从函数的地址到符号名称的映射必须永久地驻久地驻留在内核所映射的内存地址上。然而,不管是在开发的过程中还是在部署的过程中,占用这些内存都是值得的。
配置选项CONFIG_KALLSYMS_ALL 表示不仅存放函数名称,还存放所有的符号名称。
18.5 内核调试配置选项
在编译的时候,为了方便调试和测试内核代码,内核提供了许多配置选项。在内核配置编辑器的内核开发菜单。这些选项中,它们都依赖于CONFIG_DEBUG_KERNEL。当开发内核的时候,作为一种练习,不妨打开所有这些选项。
有些选项确实有用,这些选项确实能完成不少调试工作。
18.6 引发bug并打印信息
一些内核调用可以用来方便标记bug方便标记bug提供断言并输出信息。最常用的两个是BUG()和些声明BUG_ON()。当被调用的时候,它们会引发oops,导致栈的回溯和错误信息的打印。大部分体系结构把BUG()和BUG_ON()定义成某种会导致oops跟硬件的体系结构是相关的非法操作,这样自然会产生需要的oops。可以把这些调用当做断言使用,想要断言某种情况不该发生。
18.7 神奇的系统请求键
神奇的系统请求键是另外一根救命稻草,该功能可以通过定义CONFIG_MAGIC_SYSRQ配置选项来启用。
当该功能被启用的时候,无论内核处于什么状态,都可以通过特殊的组合键跟内核进行通信。这种功能可以让你在面对一台奄奄一息的系统时能完成一些有用的工作。除了配置选项以外,还要通过一个sysctl用来标记该特性的开或关。
需要启用它时使用如下命令:echo 1> /proc/sys/kernel/sysrq
18.8 内核调试器的传奇
1.配置选项
为了方便调试和测试内核代码,内核提供了许多配置选项。它们都在内核配置编辑器的内核开发菜单项中,都依赖于CONFIG_DEBUG_KERNEL。
- slab layer debugging slab层调试选项
- high-memory debugging 高端内存调试选项
- I/O mapping debugging I/O映射调试选项
- spin-lock debugging 自旋锁调试选项
- stack-overflow debugging 栈溢出检查选项
- sleep-inside-spinlock checking 自旋锁内睡眠选项
2.原子操作
指那些能够不分隔执行的东西;在执行时不能中断否则就是完不成的代码。
18.8.1 gdb
18.8.2 kgdb
kgdb是一个补丁,它可以让我们在远端主机上通过串口利用gdb的所有功能对内核进行调试。这需要两台计算机:第一台运行带有kgdb补丁的内核,第二台通过串行线使用gdb对第一台进行调试。通过kgdb的所有功能都能使用:读取或修改变量值,设置断点,设置关注变量,单步执行等。某些版本的gdb甚至允许执行函数。设置kgdb和连接串行线比较麻烦,但是一旦做完了,调试就变得很简单了。
18.9 探测系统
如果对内核调试有丰富的经验的话,那么你会掌握一些诀窍来帮助你更进一步地探测系统从而找到想要的答案。内核调试很有挑战性,即使是一点小的暗示或者技巧都能给你很大的帮助我们最好把它们联系起来。
18.9.1 用UID作为选择条件
假设为了加入一个激动人心的新特性,你重写了fork()系统调用。除非第一次的尝试就完美无缺,否则系统调试就是―场噩梦。如fork()系统调用不正常的话,压根就不用指望整个系统还能正常工作。当然,和任何时候一样,希望总是存在的,一般情况下,只要保留原有的算法而把你的新算法加入到其他位置上,基本就能保证安全:可以利用把用户id作为选择条件来实现这种功能,通过这种选择条件,可以安排到底执行哪种算法。
18.9.2 使用条件变量
如果代码与进程无关,或者希望有一个针对所有情况都能使用的机制来控制某个特性,可以使用条件变量。这比使用UID还来得简单,只需要创建一个全局变量作为一个条件选择开关。如果该变量为零,就使用一个分支上的代码。如果它不为零,就选择另外一个分支。可以通过某种接口提供对这个变量的操控,也可以直接通过调试器进行操控。
18.9.3 使用统计量
需要掌握某个特定事件的发生规律的时候,通过创建统计量,并提供某种机制访问其统计结果。
注意:这种实现不是SMP安全的
18.9.4 重复频率限制
18.10 用二分查找法找出引发罪恶的变更
18.11 使用Git进行二分搜索
Git源码管理工具提供了一个有用的二分搜索机制。如果你使用Git来控制Linux源码树的副本,那么Git将自动运行二分搜索进程。此外,Git会在修订版本中进行二分搜索,这样可以找到具体哪次提交的代码引发了bug。很多Git相关的任务比较繁杂,但使用Git进行二分搜索并不那么的困难。
18.12 当所有的努力都失败时:社区
- Linux内核邮件列表(LKML)