Linux内核设计与实现读书笔记——第十八章

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux内核设计与实现读书笔记——第十八章相关的知识,希望对你有一定的参考价值。

18章 调试

  调试工作艰难是内核级开发区别于用户级开发的一个显著特点,相比于用户级开发,内核调试的难度确实要艰苦得多。更可怕的是,它带来的风险比用户级别更高,内核的一个错误往往立刻就能让系统崩溃。

  

18.1 准备开始

  • 一个bug。听起来很可笑,但确实需要一个确定的bug。如果错误总是能够重现的话,那对我们会有很大的帮助(有一部分错误确实如此)。然而不幸的是,大部分bug通常都不是行为可靠而且定义明确的。
  • 一个藏匿bug的内核版本。如果你知道这个bug最早出现在哪个内核版本中那就再理想不过了。
  • 相关内核代码的知识和运气。调试内核其实是一个棘手的问题。不过对周围的代码理解得越多调试起来也就越轻松。 

18.2 内核中的bug

  内核中的bug多种多样,它们的产生可以有无数的原因,同时它们的表象也变化多端,从明白无误的错误代码(比如,没有把正确的值存放在恰当的位置)到同步时发生的错误(比如共享变量锁定不当)再到错误地管理硬件(比如,给错误的控制寄存器发送错误的指令)。从降低所有程序的运行性能到毁坏数据再到使得系统处于死锁状态,都可能是bug发作时的症状。

18.3 通过打印来调试

18.3.1 健壮性

  健壮性是printk()函数最容易让人们接受的一个特质。任何时候,任何地方都能调用它,内核中的printk()比比皆是。可以在中断上下文和进程上下中被调用,可以在任何持有锁时被调用,可以在多处理器上同时被调用,而且调用者连锁都不必使用。

18.3.2 日志等级

  printk()和printf()在使用上最主要的区别就是前者可以指定一个日志级别。

  内核根据这个级别来判断是否在终端上打印消息。

  内核把级别比某个特定值低的所有消息显示在终端上。

18.3.3 记录缓冲区

  内核消息都被保存在一个LOG_BUF_LEN大小的环形队列中。该缓冲区大小可以在编译时通过设置CONFIG_LOG_BUF_SHIFT进行调整。在单处理器的系统上其默认值是16KB。换句话说,就是内核在同一时间只能保存16KB的内核消息。如果消息队列已经达到最大值,那么如果再有printk()调用时,新消息将覆盖队列中的老消息。这个记录缓冲区之所以称为环形是因为它的读写都是按照环形队列方式进行操作的。

18.3.4 syslogdklogd

  标准的Linux系统上,用户空间的守护进程klogd从记录缓冲区中获取内核消息,再通过syslogd守护进程将它们保存在系统日志文件中,klogd程序既可以从/proc/kmsg文件中,也可以通过syslog()系统调用读取这些消息,默认情况下,它选择读取/proc方式实现,不管是哪种方法,klogd都会阻塞,直到有新的内核消息可供读出。在被唤醒之后,它会读取出新的内核消息并进行处理,默认情况下,它就是把消息传给syslogd守护进程。

  syslogd守护进程把它接收到的所有消息添加进一个文件中,该文件默认是/va也r/log/messages。可以通过配置文件重新指定

  在启动klogd的时候,可以通过指定-c标志来改变终端的记录等级。

18.3.5 从printf()printk()的转换

18.4 oops

  oops是内核告知用户有不行法神最常用的方式。

  使用PC的读者可能对这么多的寄存器感到惊奇(居然有32个之多)。你可能对×86-32系统更熟悉一些,在这种系统上,oops会简单一点。但是oops中包含的重要信息对于所有体系结构都是完全相同的:寄存器上下文和回溯线索。

  回溯线索显示了导致错误发生的函数调用链。这样我们就可以观察究竟发生了什么:机器处于空闲状态,正在执行idle循环,由cpu_idle()循环调用default_idle()。此时定时器中断产生了,它引起了对定时器的处理,tulip_timer()这个定时器处理函数被调用,而就是它引用了空指针。甚至可以通过偏移量找出导致问题的语句。

18.4.1 ksymoops

  回溯线索中的地址需要转化成有意义的符号名称才方便使用,这需要调用ksymoops命令。并且还必须提供编译内核时产生的System.map。如果使用的是模块,还需要一些模块信息。

18.4.2 kallsyms

  这样做会使内核变大一些,因为从函数的地址到符号名称的映射必须永久地驻久地驻留在内核所映射的内存地址上。然而,不管是在开发的过程中还是在部署的过程中,占用这些内存都是值得的。

  配置选项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 内核调试器的传奇

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.13 小结

  本章讨论了内核的调试。调试过程其实是一种寻求实现与目标偏差的行为,我们考察了几种技术:从内核内置的调试架构到调试程序,从记录日志到用git二分法查找,因为调试Linux内核困难重重,非调试用户程序能比,因此,本章的资料对于试图在内核代码中牛刀小试的任何人都至关重要。

以上是关于Linux内核设计与实现读书笔记——第十八章的主要内容,如果未能解决你的问题,请参考以下文章

《Linux内核设计与实现》读书笔记 18

第十八章读书笔记

《Android源代码设计模式解析与实战》读书笔记(十八)

Linux内核设计与实现 第十八章

读书笔记

linux内核设计与实现一书阅读整理 之第十八章