以下内容主要学习自debug hacks一书。
有关内核调试方面的技巧,我更多的是将其映射为应用程序的调试技巧。我觉得大部分都是线程间同步的问题。
1.对于NULL指针的访问。
这个问题一般来讲是比较容易确定的,一般来讲就是未申请空间,或者申请空间失败了但是没有检查错误。这个可以通过backtrace和源码确定发生错误的位置。
这里介绍两个gdb的新命令。info symbol和maintenance translate-address,这两个命令可以把某个地址值转化为符号表中的信息,或者和符号表中某个符号的差值,这个功能在调试时可以大大地提高效率。
2.链表破坏。
这个是由于线程间同步问题导致的错误。内核中采用的确定方法是,将被删除过的节点或者链表项的前后指针指向两个非法的地址,但不是null,所以当出现段错误时,发现非法访问时由这两个非法地址所引起的,那么就可以确定是由于访问已经被删除的链表项或者空链表导致的问题。解决方法是不管访问还是修改,都需要先获得该数据结构的锁。
3.从代码发现可能的错误
书中讲了一个有关释放inode的例子,inode是高速缓存中的文件节点,如果内存不够时,就会释放某些不使用的inode。由于线程间同步问题,在一个函数中,对某个数据结构的操作过程中拿到锁之后,中途释放了该锁,其他等待线程对该数据结构操作之后,原线程又拿锁再一次对这个数据结构进行操作。这时候就可能会导致一定的问题。
这种错误在应用程序中,应该是出现了段错误,对于这个数据结构的访问出现了问题,那么使用gdb对这个地址进行监视,或者前后有函数的地方设置断点,打印堆栈,确定多线程之间的顺序。
解决这种问题,中途不释放锁,那么就不会出现差错。
4.内核停止响应(死循环)
映射为应用程序的死循环技巧的话,在上一篇文章中我已经讲过了,定位加复现加单步调试。而在内核中更多的是查看内核转储,因为此时不可能使用crash来调试,当内核转储文件没什么效果时,就该使用大招,加打印。其实加打印是最简单直接,但是繁琐的调试方法。
重点:
认真询问故障发生时的情况
在自己的环境中复现
实验各种各样的条件。
5.内核停止响应(自旋锁)
类似于应用程序中的死锁,进程A需要获取进程B的锁,进程B想要获取进程A的锁,导致了死锁的发生。
停止响应时,首先应该获取到当前的dump信息,然后分析dump信息,当前进程正在做什么。确定是在拿锁,然后在拿锁这个函数上设置断点,打印堆栈,确定多线程的流程。
6.内核停止响应(自旋锁2)
还是查看堆栈,发现有两个相同的函数调用,这两个函数中都存在拿锁的操作。解决过程一般结合源码和dump文件进行分析。根据堆栈追踪源码。
这里的技巧是根据堆栈查看源码,发现问题的原因。
7.内核停止响应(信号量)
其实这个问题还是由于线程间同步的问题,这个问题是由于读写信号量的问题导致的。可以查看堆栈信息,如果在堆栈中可以发现两个互斥的操作,那么问题也就解决了,但是如果不够详细,那么需要重新运行,设置断点,打印bt信息。
8.实时进程停止响应
这里获取到的经验还是通过堆栈查看问题原因所在,还有一个是通过查看进程占用的时间。ps -t。
9.运行缓慢的故障
当一个进程运行需要很长时间时,可以通过strace进行跟踪,查看在哪个函数中调用的时间最长,当然strace仅限于系统调用的时间。
10.cpu负载过高
这一条和前两条,8,9,10这三条的重点我觉得在于查看一个进程在各个函数锁停留的时间。这里学习了一个很重要的工具oprofile,可以解析每个函数的被调用次数,运行总时间,调用关系等等。如果仅仅是关心应用层函数,也就是自己所写的函数,可以使用gprof来检测。
总结:
上面10条中,大部分都是先查看堆栈,然后复现问题,加断点,打印堆栈,查看冲突所在。
有6条是属于线程间同步的问题,234567这六条。
第1条属于简单地查看堆栈即可确定错误。
最后的三条需要使用oprofile来确定函数或者进程的运行时间各方面的情况。
所以在解决问题时,一般流程,收集信息,分析dump文件,复现bug,查明原因,修复bug,测试。
这里学到的手段,查看堆栈,查看运行情况。