第十八章 调试

18.1准备开始

需要的只是:

  • 一个确定的bug。大部分bug通常都不是行为可靠而且定义明确的。
  • 一个藏匿bug的内核版本。
  • 相关的内核代码的知识和运气。

18.2内核中的bug

  • 内核中的bug多种多样,它们的产生可以有无数的原因,同时他们的表象也变化多端。
  • 从隐藏在源代码中的错误到展现在目击者面前的bug,往往都是经历一系列连锁反应的事件才可能触发。
  • 内核与其他大型的软件项目没有什么太大的不同。
  • 内核有一些独特的问题需要考虑:例如定时限制和竞争条件等,它们都是允许多个线程在内核中同时运行产生的结果。

18.3通过打印来调试

printk()就是内核的格式化打印函数。

18.3.1健壮性

  • 可以在中断上下文和进程上下文中被调用;可以在任何持有锁时被调用;可以在多处理器上同时被调用,而且调用者连锁都不必使用。
  • 它是一个弹性极佳的函数,就在于它随时都能被调用。
  • 除非在启动过程的初期就要在终端上输出,否则可以认为printk()在什么情况下都能工作。

18.3.2日志等级

printk()和printf()在使用上最主要的区别就是前者可以指定一个日志级别。内核根据这个级别来判断是否在终端上打印消息。内核把级别比某个特定值低的所有消息显示在终端上。

printk的输出日志级别如下:

  • KERN_ EMERG 一个紧急情况
  • KERN_ ALERT 一个需要立即被注意到的错误
  • KERN_ CRIT 一个临界情况
  • KERN_ ERR 一个错误
  • KERN_ WARNING 一个警告
  • KERN_ NOTICE 一个普通的, 不过也有可能需要注意的情况
  • KERN_ INFO 一条非正式的消息
  • KERN_ DEBUG 一条调试信息--一般是冗余信息

如果没有指定一个记录等级,函数会选用默认的DEFAULT_MASSAGE_LOGLEVEL

内核将最重要的记录等级KERN_EMERG定位“<0>”,将无关紧要的记录等级“KERN_DEBUG”定位“<7>”。

按照你的想法赋予记录等级。一种选择是保持终端的默认记录等级不变,给所有调试信息KERN_CRIT或更低的等级。相反,也可以给所有调试信息KERN_DEBUG等级,而调整终端的默认记录等级。

18.3.3记录缓冲区

内核消息都被保存在一个LOG_BUF_LEN大小的环形队列中。该缓冲区大小是可以在编译时通过CONFIG_LOG_BUF_SHIFT进行调整。在单处理器的系统上默认值是16kb,就是内核在同一时间只能保存16kb的内核消息,如果消息队列已经达到最大值,新消息将会覆盖队列中的老消息。读写都是按照环形队列方式操作的。

同步问题易解决,记录维护起来也更容易。但可能会丢失消息

18.3.4syslogd和klogd

  • 用户空间的守护进程klogd从记录缓冲区中获取内核消息,再通过syslogd守护进程将他们保存在系统日志文件中。
  • klogd程序既可以从/proc/kmsg文件中,也可以通过syslog()系统调用读取这些消息。默认情况下,它选择读取/proc方式实现。
  • klogd都会阻塞,直到有新的内核消息可供读出。
  • syslogd守护进程将它接收到的所有消息添加到一个文件中,该文件默认是/var/log/messages
  • 在启动klogd时,可以通过指定-c标志来改变终端的记录等级。

18.4oops

  • oops是内核告知用户有不幸发生的最常用的方式。它很难自我修复,它也不能将自己杀死,内核只能发布oops。向终端上输出错误消息,输出寄存器中保存的信息,输出可供跟踪的回溯线索。

  • 通常发送完oops之后,内核会处于一种不稳定状态。如果oops在中断上下文时发生,内核根本无法继续,它会陷入混乱,混乱的结果就是系统死机。如果oops在idle进程或init进程(0号进程和1号进程)时发生,结果同样是系统陷入混乱,因为内核缺了这两个重要的进程根本就无法工作。oops在其他进程运行时发生,内核就会杀死该进程并尝试着继续执行。

  • oops的产生有很多可能原因,其中包括内存访问越界或者非法的指令等。oops中包含的重要信息对于所有体系结构都是完全相同的:寄存器上下文和回溯线索。回溯线索显示了导致错误发生的函数调用链。寄存器数据可以帮助你重建引发问题的现场。

18.4.1ksymoops

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

18.4.2kallsyms

  • 已经无须使用kysmoops工具了,因为可能会发生很多问题,新版本中引入了kallsyms特性,它可以通过定义CONFIG_KALLSYMS配置选项启用。
  • 这样做会使内核变大一些,因为从函数的地址到符号名称的映射必须永久地驻留在内核所映射的内存地址上。

18.5内核调试配置选项

这些选项都在内核配置编辑器的内核开发菜单项中,它们都依赖于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 自旋锁内睡眠选项

原子操作指那些能够不分隔执行的东西;在执行时不能中断否则就是完不成的代码。正在使用一个自旋锁或禁止抢占的代码进行的就是原子操作。使用锁时睡眠是引发死锁的元凶。内核提供了一个原子操作计数器。

18.6引发bug并打印信息

最常用的两个是BUG()和BUG_ON(),被调用时会引发oops,导致栈的回溯和错误信息的打印。

可以把这些调用当做断言使用,想要断言某种情况不该发生:

if (bad_thing)
BUG();



BUG_ON(bad_thing);

BUILD_BUG_ON()BUG_ON()作用相同,仅在编译时调用。

可以用panic()引发更严重的错误,不但会打印错误信息,还会挂起整个系统。

dump_stack()只在终端上打印寄存器上下文和函数的跟踪线索。

18.7神奇的系统请求键

该功能可以通过定义CONFIG_MAGIC_SYSRQ配置选项来启用。SysRq(系统请求)键在大多数键盘上都是标准键。

该功能被启用时,无论内核出于什么状态,都可以通过特殊的组合键和内核进行通信。

除了配置选项以外,还要通过一个sysctl用来标记该特性的开或关,需要启用它时使用如下命令:echo 1 > /proc/sys/kernel/sysrq

SysRq-s将“脏”缓冲区跟硬盘交换分区同步,SysRq-u卸载所有的文件系统,SysRq-b重启设备。

18.8内核调试器的传奇

18.8.1gdb

可以使用标准的GNU调试器对正在运行的内核进行查看。针对内核启动调试器的方法与针对进程的方法大致相同:gdb vmlinux /proc/kcore

  • vmlinx文件是未经压缩的内核映像,不是压缩过的zImage或bImage,它存放于源代码树的根目录上。
  • /proc/kcore:作为一个参数选项,是作为core文件来用的,通过它能够访问到内核驻留的高端内存。只有超级用户才能读取此文件的数据。

可以使用gdb的所有命令来获取信息。

gdb还是有很多局限性的。它没有办法修改内核数据。它也不能单步执行内核代码,不能加断点。

18.8.2kgdb

kgdb是一个补丁 ,它可以让我们在远程主机上通过串口利用gdb的所有功能对内核进行调试。这需要两台计算机:第一台运行带有kgdb补丁的内核,第二台通过串行线使用gdb对第一台进行调试。

通过kgdb,gdb的所有功能都能使用:读取和修改变量值,设置断点,设置关注变量,单步执行。

18.9探测系统

18.9.1使用uid作为选择条件

一般情况下,加入特性时,只要保留原有的算法而把新算法加入到其他位置上,基本就能保证安全。可以把用户id(UID)作为选择条件来实现这种功能,通过某种选择条件,安排到底执行哪种算法:

if (current-> uid !=7777) {
/* 老算法…… */
} else {
/* 新算法…… */
}

除了uid=7777的用户以外,其他所有的用户都是用的老算法,可以创建一个UID为7777的用户,专门来测试新算法。

18.9.2使用条件变量

如果代码与进程无关,或者希望有一个针对所有情况都能使用的机制来控制某个特性,可以使用条件变量。只需要创建一个全局变量作为一个条件选择开关。如果该变量为零,就使用一个分支上的代码;否则,就选择另外一个分支。可以通过某种接口提供对这个变量的操控,也可以直接通过调试器进行操控。

18.9.3使用统计量

通过创建统计量并提供某种机制访问其统计结果。

定义两个全局变量:可以在/proc目录中创建一个文件,还可以新建一个系统调用,最简单的办法还是通过调试器直接访问它们。

注意,这种实现并非是SMP安全的,理想的办法是通过原子操作进行实现。

18.9.4重复频率限制

有两种方式可以防止这类问题发生:

  • 重复频率限制

调试信息,最多两秒打印一次,可以根据自己的需要或高或低地调整这种重复频率。

  • 发生次数限制

用到的变量都应该是静态的,并且应该限制在函数局部范围以内,这样才能保证变量的值在经历多次函数调用后仍然能够保留下来。

都不是SMP安全或抢占安全的,不过,只需要用原子操作改造一下就没问题了。

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

需要:

  • 一个可靠的可复制的错误
  • 一个确保没有问题的内核
  • 一个肯定有问题的内核

可以在问题内核和良好的内核之间使用二分法,重复筛选。

18.11使用git进行二分搜索

告诉git要进行二分搜索:git bisect start

提供一个出现问题的最早内核版本:git bisect bad <revision>

如果当前版本就是引发bug的最初版本,则使用:git bisect bad

提供一个最新的可正常运行的内核版本:git bisect good <revision>

接下来,git就会利用二分搜索法在Linux源码树中,自动检测正常的版本内核和有bug的内核版本之间那个版本有隐患。接着再编译、运行以及测试正被检测的版本。

如果这个版本正常:git bisect good

如果这个版本运行有异常:git bisect bad

对于每一个命令,git将在一个版本的基础上反复二分搜索源码树,并且返回所查的下一个内核版本。反复执行直到不能再进行二分搜索为止,最终git会打印出有问题的版本号。

Linux内核设计与实现第五周读书笔记的更多相关文章

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

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

  2. Linux内核设计与实现第十周读书笔记

    第十七章 设备与模块 关于设备驱动与设备管理,我们讨论四种内核成分. 设备类型 模块 内核对象 sysfs 17.1设备类型 在Linux以及所有Unix系统中,设备被分为以下三种类型: 块设备,块设 ...

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

随机推荐

  1. npp基本设置

    经过实践,本人发现Notpad++是一个很不错的软件,无论是用于文档的读取还是开发,都很赞,那么给软件做一些基本的设置,使用的时候更得心用手就显得尤为重要了. 本文主要介绍npp的基础设置,后期会不断 ...

  2. CF刷题-Codeforces Round #481-D. Almost Arithmetic Progression

    题目链接:https://codeforces.com/contest/978/problem/D 题解: 题目的大意就是:这组序列能否组成等差数列?一旦构成等差数列,等差数列的公差必定确定,而且,对 ...

  3. Jenkins 自动化测试

    学习 Jenkins 自动化测试的系列文章 Robot Framework 概念 Robot Framework 安装 Pycharm + Robot Framework 环境搭建 Robot Fra ...

  4. 20个常用Linux性能监控工具/命令

    20个常用Linux性能监控工具/命令 对于 Linux/Unix 系统管理员非常有用的并且最常用的20个命令行系统监视工具.这些命令可以在所有版本的 Linux 下使用去监控和查找系统性能的实际原因 ...

  5. Play on Words(欧拉回路)

    Description Some of the secret doors contain a very interesting word puzzle. The team of archaeologi ...

  6. Java 学习笔记 ------第五章 对象封装

    本章学习目标: 了解封装的概念与实现 定义类.构造函数与方法 使用方法重载与不定长度自变量 了解static方法 一.Java封装概念 在面向对象程式设计方法中,封装(英语:Encapsulation ...

  7. C# string 常用方法

    string.ToString().Contains() String str="abcd" str.ToString().Contains("a"); //t ...

  8. 【dp】New Keyboard

    http://codeforces.com/gym/101397 B dp[i][j][k]: i为前一个行动的状态,0-switch.1-type,j为当前状态layout的编号,k 是已键入的字符 ...

  9. Node.js系列——(4)优势及场景

    背景 之前几篇系列文章简单介绍了node.js的安装配置及基本操作: Node.js系列--(1)安装配置与基本使用 Node.js系列--(2)发起get/post请求 Node.js系列--(3) ...

  10. python接口自动化测试框架实现之操作oracle数据库

    python操作oracle数据库需要使用到cx-oracle库. 安装:pip install cx-oracle python连接oracle数据库分以下步骤: 1.与oracle建立连接: 2. ...