第18章 调试

20135307张嘉琪


18.1 准备开始

18.2 内核中的bug

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

18.3 通过打印来调试

18.3.1 健壮性

  • 健壮性是printk()函数最容易让人们接受的一个特质。任何时候,任何地方都能调用它,内核中的printk()比比皆是。它是一个弹性极佳的函数,这一点相当重要。printk()之所以这么有用,就在于它随时都能被调用。printk()函数的健壮躯壳下也难免会有漏洞,在系统启动过程中.终端还没有初始化之前在某些地方不能使用它。不过说实在的,如果终端没有初始化,你又能输出到什么地方去呢?这―般不是一个什么问题。除非你要调试的是启动过程最开始的那些步骤(比如说在负责执行硬件体系结构相关的初始化动作的函数中)下进行这样的调试挑战性很强没有任何打印函数能用,确实让问题更加棘手。

18.3.2 日志等级

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

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

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

18.3.3 记录缓冲区

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

  • 使用环形队列有许多好处。由于同时读写环形缓冲区时,其同步问题很容易解决,所以即使在中断上下文中也可以方便地使用printfk()。此外,它使记录维护起来也更容易。如果有大量的消息同时产生,新消息只需覆盖掉旧消息即可。在某个问题引发大量消息的时候。记录只会覆盖掉它本身,而不会因为失控而消耗掉大量内存。而环形缓冲区的唯一缺点——可能会丢失消息但是与简单性和健壮性的好处相比这点代价是值得的。

18.3.4 syslogd和klogd

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

18.3.5 从printf()到printk()的转换

18.4 oops

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

  • oops中包含的重要信息对于所有体系结构都是完全相同的:寄存器上下文和回溯线索。

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

18.4.1 ksymoops

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

18.4.2 kallsyms

  • 配置选项CONFIG_KALLSYMS_ALL 表示不仅存放函数名称,还存放所有的符号名称。

18.5 内核调试配置选项

  • 在编译的时候,为了方便调试和测试内核代码,内核提供了许多配置选项。在内核配置编辑器的内核开发菜单。这些选项中,它们都依赖于CONFIGDEBUGKERNEL。

18.6 引发bug并打印信息

  • 一些内核调用可以用来方便标记bug方便标记bug提供断言并输出信息。最常用的两个是BUG()和些声明BUG_ON()。当被调用的时候,它们会引发oops,导致栈的回溯和错误信息的打印。大部分体系结构把BUG()和BUG_ON()定义成某种会导致oops跟硬件的体系结构是相关的非法操作,这样自然会产生需要的oops。可以把这些调用当做断言使用,想要断言某种情况不该发生。

  • 18.7 神奇的系统请求键

  • 系统请求键功能可以通过定义CONFIGMAGICSYSRQ配置选项来启用。

  • 当该功能被启用的时候,无论内核处于什么状态,都可以通过特殊的组合键跟内核进行通信。这种功能可以让你在面对一台奄奄一息的系统时能完成一些有用的工作。除了配置选项以外,还要通过一个sysctl用来标记该特性的开或关。

  • 需要启用它时使用如下命令:echo 1> /proc/sys/kernel/sysrq

18.8 内核调试器的传奇

  • 很多内核开发者一直以来都希望能拥有一个用于内核的调试器.不幸的是,Linus不愿意在它的内核源代码树中加入一个调试器。他认为调试器会误导开发者,从而致引入不良的修正,没有人能对他的逻辑提出异议从真正理解代码出发,确实更能保证修正的正确性。然而,许多内核开发者们还是希望有一个官方发布的、用于内核的调试器。因为这个要求看起来不会马上被满足,所以许多补丁应运而生了,它们为标准内核附加上了内核调试的支持,虽然这都是―些不被官方认可的附加补丁,但它们确实功能完善,十分强大。在我们深入这些解决方案之前,先看看标准的调试器gdb能够给我们一些什么帮助是―个不错的选择。

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 使用统计量

  • 有些时候你需要掌握某个特定事件的发生规律。有些时候需要比较多个事件并从中得出规律。通过创建统计量并提供某种机制访问其统计结果,很容易就能满足这种需求。举个例子,假设我们希望得到foo和bar的发生频率,那么在某个文件中,当然最好是在定义该事件的那个文件里定义两个全局变量。

18.9.4 重复频率限制

18.10 用二分查找法找出引发罪恶的变更

18.11 使用Git进行二分搜索

  • Git源码管理工具提供了一个有用的二分搜索机制。如果你使用Git来控制Linux源码树的副本,那么Git将自动运行二分搜索进程。此外,Git会在修订版本中进行二分搜索,这样可以找到具体哪次提交的代码引发了bug。很多Git相关的任务比较繁杂,但使用Git进行二分搜索并不那么的困难。

18.12 当所有的努力都失败时:社区

  • 或许你已经做完了所有你能想到的尝试,你在键盘上呕心沥血了几个小时。实际上,可能是无数日子,答案依旧没有眷顾你。此时,如果bug是在Linux内核的主流部分中,你可以在内核开发社区中寻求其他开发者的帮助你应该向内核邮件列表发送一份电子邮件,对bug进行完整而又简洁地描述,你的发现可能会对找到最终的答案起到帮助。

18.13 小结

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

《Linux内核设计与实现》 第五周 读书笔记(第十八章)的更多相关文章

  1. Linux内核设计与实现第五周读书笔记

    第十八章 调试 18.1准备开始 需要的只是: 一个确定的bug.大部分bug通常都不是行为可靠而且定义明确的. 一个藏匿bug的内核版本. 相关的内核代码的知识和运气. 18.2内核中的bug 内核 ...

  2. LINUX内核设计与实现第三周读书笔记

    LINUX内核设计与实现第三周读书笔记 第一章 LINUX内核简介 1.1 Unix的历史 1969年的夏天,贝尔实验室的程序员们在一台PDR-7型机上实现了Unix这个全新的操作系统. 1973年, ...

  3. Linux内核设计与实现第六周读书笔记

    第三章 进程管理 3.1 进程 进程是处于执行期的代码.通常进程还要包含其他资源,像打开的文件.挂起的信号.内核的内部数据.处理器状态.一个或多个具有内存映射的内存地址空间及一个或多个执行线程,当然还 ...

  4. Linux内核设计与实现第八周读书笔记

    第四章 进程调度 进程在操作系统看来是程序的运行态表现形式. 4.1多任务 多任务操作系统就是能同时并发地交互执行多个进程的操作系统. 多任务操作系统会使多个进程处于堵塞或者睡眠状态.这些任务尽管位于 ...

  5. linux内核设计与实现第七周读书笔记

    第七章 链接 链接(linking)是将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储并执行.链接可以执行于编译时(compile time),也就是在源代 ...

  6. 《Linux内核设计与实现》第四周读书笔记——第五章

    <Linux内核设计与实现>第四周读书笔记--第五章 20135301张忻 估算学习时间:共1.5小时 读书:1.0 代码:0 作业:0 博客:0.5 实际学习时间:共2.0小时 读书:1 ...

  7. 《Linux内核设计与实现》Chapter 3 读书笔记

    <Linux内核设计与实现>Chapter 3 读书笔记 进程管理是所有操作系统的心脏所在. 一.进程 1.进程就是处于执行期的程序以及它所包含的资源的总称. 2.线程是在进程中活动的对象 ...

  8. 《Linux内核设计与实现》Chapter 1 读书笔记

    <Linux内核设计与实现>Chapter 1 读书笔记 一.Unix的特点 Unix从Multics中产生,是一个强大.健壮和稳定的操作系统. 特点 1.很简洁 2.在Unix系统中,所 ...

  9. 《Linux内核设计与实现》Chapter 5 读书笔记

    <Linux内核设计与实现>Chapter 5 读书笔记 在现代操作系统中,内核提供了用户进程与内核进行交互的一组接口,这些接口的作用是: 使应用程序受限地访问硬件设备 提供创建新进程与已 ...

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

    <Linux内核设计与实现>Chapter 18 读书笔记 一.准备开始 一个bug 一个藏匿bug的内核版本 知道这个bug最早出现在哪个内核版本中. 相关内核代码的知识和运气 想要成功 ...

随机推荐

  1. 用JQuery操作元素的style属性

    可以直接利用css()方法获取元素的样式属性,JQuery代码如下: 1 $("p").css("color");  //获取p元素的样式颜色 无论color属 ...

  2. SQL 加载

    首先新建表 第一张表 order_info 第二张表 user_info 登录mysql mysql -u root -p 在开始加载数据之前配置编码 查看默认编码 show variables li ...

  3. 转载 .Net多线程编程—任务Task https://www.cnblogs.com/hdwgxz/p/6258014.html

    .Net多线程编程—任务Task   1 System.Threading.Tasks.Task简介 一个Task表示一个异步操作,Task的创建和执行是独立的. 只读属性: 返回值 名称 说明 ob ...

  4. day12 Python元祖

    前戏 #元祖:元素不可被改变,不能白增加或者删除 #tuple #tu = (11,22,33,44) #tu.count(22),获取指定元素在元祖中出现的次数 #tu.index(22),获取元素 ...

  5. linux shell脚本调用java main方法 代码传值

    #!/bin/bash #description: ljdjService export PRG_HOME=/ainmc/work/toptea/dataTransfer PRG_KEYWORD=pr ...

  6. LeetCode算法题详解之两个数组的交集

    题目背景: 这个与我们高中时期学习的交集是一样的,顺便复习一下相关的数学知识有助于更好的理解. 交集的定义: 对于两个集合A和B,定义A和B的交集为C,其中C={x|x属于A且X属于B},记作A∩B. ...

  7. JS常见的小代码

    一:去掉数组里面的重复项. 比如 如下一个数组:var arr = [1,2,4,3,4,3]; 我想要得到数组 [1,2,4,3].为这样的 写一个函数去掉重复的项. var unique = fu ...

  8. ThreadLocal可能引起的内存泄露

    threadlocal里面使用了一个存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露. 最好 ...

  9. IntelliJ IDEA 常用设置 (二)

    一. 代码提示和补充功能有一个特性:区分大小写. 区分大小写的情况是这样的:比如我们在 Java 代码文件中输入 stringBuffer IntelliJ IDEA 是不会帮我们提示或是代码补充的, ...

  10. python descriptor 详解

    descriptor简介 在python中,如果一个新式类定义了__get__, __set__, __delete__方法中的一个或者多个,那么称之为descriptor.descriptor有分为 ...