第18章读书笔记

18章 调试

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

驾驭内核调试的能力(当然,最终是为了能够成功地开发内核)很大程度上取决于经验和对整个操作系统的把握。没错,玉树临风可能会对别的事情有帮助,但是调试内核的关键还是在于你对内核的深刻理解,然而我们必须找到可以开始着手的地方所以,在这―章里我们从调试内核的一种可能步骤开始。

18.1 准备开始

内核调试往往是一个令人挠头不已的漫长过程。幸运的是,在这些费劲的问题中也有不少比较简单而且容易消灭的小bug,运气好你可能面对的是些简单的小bug。开始做―些调查之前,不会清楚到底面对的是什么现在,需要的只是:

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

18.2 内核中的bug

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

18.3 通过打印来调试

18.3.1 健壮性

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

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

18.3.2 日志等级

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

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

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

18.3.3 记录缓冲区

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

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

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。如果使用的是模块,还需要一些模块信息。

然后该程序就会吐出解码版的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 内核调试器的传奇

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

18.11 使用Git进行二分搜索

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

时间: 2024-10-30 06:20:50

第18章读书笔记的相关文章

《内核设计与实现》第一章读书笔记

<内核设计与实现>第一章读书笔记 第一章:Linux内核简介 1.1 Unix的历史 Unix强大的特点 A.简洁,几百个系统调用,明确的设计目的 B.文件对待所有东西 C.移植性强(C语言) D.进程创建快,使用fork()系统调用. E.进程间通信元语,进程间通信机制 1.2 linux的简介 Linux系统的基础是内核.C库.工具库和系统的基本工具. 1.3 操作系统和内核简介 内核:管理者,操作系统的核心 通常一个内核由负责响应中断的中断服务程序和网络.进程间通信等系统服务共同组成.

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

<Linux内核设计与实现>Chapter 2 读书笔记 一.获取内核源码 1.使用Git 我们曾经在以前的学习中使用过Git方法 $ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git 更新分支到Linux的最新分支 $ git pull 可以获取并随时保持与内核官方的代码树一致 2.安装内核源代码 压缩形式为bzip2 $ tar xvjf linux-x.y.z.tar.bz2 压缩

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

<Linux内核设计与实现>Chapter 5 读书笔记 在现代操作系统中,内核提供了用户进程与内核进行交互的一组接口,这些接口的作用是: 使应用程序受限地访问硬件设备 提供创建新进程与已有进程进行通信的机制 提供申请操作系统其他资源的能力 一.与内核通信 1.系统调用的作用 系统调用在用户空间进程和硬件设备之间添加了一个中间层,作用是: 为用户空间提供了一种硬件抽象接口: 系统调用保证了系统的稳定和安全,即可以避免应用程序不正确地使用硬件设备,窃取其他进程的资源: 每个进程都运行在虚拟系统中

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

<Linux内核设计与实现>Chapter 3 读书笔记 进程管理是所有操作系统的心脏所在. 一.进程 1.进程就是处于执行期的程序以及它所包含的资源的总称. 2.线程是在进程中活动的对象. 3.进程提供两种虚拟机制:虚拟处理器和虚拟内存. 4.内核调度的对象是线程,而不是进程. 二.进程描述符及任务结构 内核把进程的列表存放在叫做任务队列的双向循环链表中.链表中的每一项都是类型为task_struct的进程描述符结构,该结构定义在<linux/sched.h>文件中. 1.分配进

《Linux内核分析》之第三章读书笔记

进程管理 进程是处于执行期的程序以及相关的资源的总称,也称作任务.执行线程,简称线程,是在进程中活动的对象. 可以两个或两个以上的进程执行同一个程序 也可以两个或两个以上并存的进程共享许多资源 内核调度的对象是线程,而不是进程. 进程描述符及任务结构 内核把进程的列表存放在任务列表(task list)的双向循环链表中. 链表中每一项都是类型为task_struct的进程描述符的结构. 进程描述符中包含的数据能完整地描述一个正在执行的程序: 打开的文件 进程的地址空间 挂起的信号 进程的状态 m

《Linux内核分析》之第四章读书笔记

4.1多任务 多任务操作系统:同时并发地交互执行多个进程的操作系统 多任务操作系统会使多个进程处于堵塞或者睡眠状态.这些任务尽管位于内存,但是并不处于可运行状态.这些进程利用内核堵塞自己,直到某一事件发生. 多任务系统可以划分为两类:非抢占式和抢占式. 抢占:强制挂起. 时间片:分配给每个可运行进程的处理器时间段. 4.2 linux的进程调度 4.3策略 策略决定调度程序在何时让什么程序运行. 4.3.1I/O消耗型和处理器消耗型的进程 I/O消耗型:进程的大部分时间用来提交I/O请求或是等待

linux及安全《Linux内核设计与实现》第一章——20135227黄晓妍

<linux内核设计与实现>第一章 第一章Linux内核简介: 1.3操作系统和内核简介 操作系统:系统包含了操作系统和所有运行在它之上的应用程序.操作系统是指整个在系统中负责完成最基本功能和系统管理的那些部分.这些部分包括内核.设备驱动程序.启动应到程序.命令行shell或者其他种类的用户界面.基本的文件管理系统工具. 内核:如果说用户界面是操作系统的外在表像,那么内核就是操作系统的内在核心. 内核空间:系统态和保护起来的内存空间. 内核的组成: 1.中断服务程序(响应中断) 2.调度程序(

《LINUX内核设计的艺术》第一章从开机家电到执行main函数之前的过程 学习笔记之一

从开机加电到实行main函数之前的过程 分为三步,目的是实现从启动盘加载操作系统程序,完成实现main函数的准备工作 启动BLOS,准备是模式下的中断向量表和中断服务程序 从启动盘加载操作系统程序到内存.加载操作系统程序就是靠第一步实现的 为实现32位的main函数做过度工作 1.1启动blos,准备实模式下的中断向量表和中断服务程序 由blos来加载软件操作系统的任务 1.1.1         BLOS的启动原理 0XFFFF0 由硬件来启动,CPU硬件设计逻辑设计为加电瞬间就强行将CS的值

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

1. 设备类型 linux中主要由3种类型的设备,分别是: 设备类型 代表设备 特点 访问方式 块设备 硬盘,光盘 随机访问设备中的内容 一般都是把设备挂载为文件系统后再访问 字符设备 键盘,打印机 只能顺序访问(一个一个字符或者一个一个字节) 一般不挂载,直接和设备交互 网络设备 网卡 打破了Unix "所有东西都是文件" 的设计原则 通过套接字API来访问 除了以上3种典型的设备之外,还有"伪设备",即一些虚拟的设备,仅提供访问内核功能而已,没有物理设备与之关联

Linux内核设计与实现 第五章

1. 什么是系统调用 系统调用就是用户程序和硬件设备之间的桥梁. 用户程序在需要的时候,通过系统调用来使用硬件设备. 系统调用的存在意义: 1)用户程序通过系统调用来使用硬件,而不用关心具体的硬件设备,这样大大简化了用户程序的开发. 比如:用户程序通过write()系统调用就可以将数据写入文件,而不必关心文件是在磁盘上还是软盘上,或者其他存储上. 2)系统调用使得用户程序有更好的可移植性. 只要操作系统提供的系统调用接口相同,用户程序就可在不用修改的情况下,从一个系统迁移到另一个操作系统. 3)