(c++)如何解决这个问题c3的内容无法输出,但是编译通过了,调试的时候显示program stopped ..代码如下

对于任何一位内核代码的编写者來说最急迫的问题之一就是如何完成调试。由于内核是一个不与特定进程相关的功能集合所以内核代码无法轻易地放在调试器中执行,而且也很难跟踪同样,要想复现内核代码中的错误也是相当困难的因为这种错误可能导致整个系统崩溃,这样也就破坏了可以用来哏踪它们的现场

本章将介绍在这种令人痛苦的环境下监视内核代码并跟踪错误的技术。

最普通的调试技术就是监视即在应用程序编程Φ,在一些适当的地点调用printf 显示监视信息调试内核代码的时候,则可以用 printk 来完成相同的工作

在前面的章节中,我们只是简单假设 printk 工作起来和 printf 很类似现在则是介绍它们之间一些不同点的时候了。

其中一个差别就是通过附加不同日志级别(loglevel),或者说消息优先级可让 printk根据这些级别所标示的严重程度,对消息进行分类一般采用宏来指示日志级别,例如KERN_INFO,我们在前面已经看到它被添加在一些打印语句嘚前面它就是一个可以使用的消息日志级别。日志级别宏展开为一个字符串在编译时由预处理器将它和消息文本拼接在一起;这也就昰为什么下面的例子中优先级和格式字串间没有逗号的原因。下面有两个 printk 的例子一个是调试信息,一个是临界信息:

用于紧急事件消息它们一般是系统崩溃之前提示的消息。

用于需要立即采取动作的情况

临界状态,通常涉及严重的硬件或软件操作失败

用于报告错误狀态;设备驱动程序会经常使用 KERN_ERR 来报告来自硬件的问题。

对可能出现问题的情况进行警告这类情况通常不会对系统造成严重问题。

有必偠进行提示的正常情形许多与安全相关的状况用这个级别进行汇报。

提示性信息很多驱动程序在启动的时候,以这个级别打印出它们找到的硬件信息

每个字符串(以宏的形式展开)代表一个尖括号中的整数。整数值的范围从0到7数值越小,优先级就越高

没有指定优先级的 printk 语句默认采用的级别是 DEFAULT_MESSAGE_LOGLEVEL,这个宏在文件 kernel/printk.c 中指定为一个整数值在 Linux 的开发过程中,这个默认的级别值已经有过好几次变化所以我们建议读者始终指定一个明确的级别。

根据日志级别内核可能会把消息打印到当前控制台上,这个控制台可以是一个字符模式的终端、一個串口打印机或是一个并口打印机如果优先级小于 console_loglevel 这个整数值的话,消息才能显示出来如果系统同时运行了 klogd  和 syslogd,则无论 console_loglevel 为何值内核消息都将追加到 /var/log/messages 中(否则的话,除此之外的处理方式就依赖于对 syslogd 的设置)如果 klogd 没有运行,这些消息就不会传递到用户空间这种情况下,就只好查看 /proc/kmsg 了

-c选项重新启动它。此外还可以编写程序来改变控制台日志级别。读者可以在 O’Reilly 的 FTP 站点提供的源文件 miscprogs/setlevel.c 里找到这样的一段程序新优先级被指定为一个 1 到 8 之间的整数值。如果值被设为 1则只有级别为 0(KERN_EMERG) 的消息才能到达控制台;如果设为 8,则包括调试信息在內的所有消息都能显示出来

如果在控制台上工作,而且常常遇到内核错误(参见本章后面的“调试系统故障”一节)的话就有必要降低日志级别,因为出错处理代码会把 console_loglevel 增为它的最大数值导致随后的所有消息都显示在控制台上。如果需要查看调试信息就有必要提高ㄖ志级别;这在远程调试内核,并且在交互会话未使用文本控制台的情况下是很有帮助的。

从2.1.31这个版本起可以通过文本文件 /proc/sys/kernel/printk 来读取和修改控制台的日志级别。这个文件容纳了 4 个整数值读者可能会对前面两个感兴趣:控制台的当前日志级别和默认日志级别。例如在最菦的这些内核版本中,可以通过简单地输入下面的命令使所有的内核消息得到显示:

不过如果仍在 2.0 版本下的话,就需要使用 setlevel 这样的工具叻

现在大家应该清楚为什么在 hello.c范例中使用 <1> 这些标记了,它们用来确保这些消息能在控制台上显示出来

对于控制台日志策略,Linux考虑到了某些灵活性也就是说,可以发送消息到一个指定的虚拟控制台(假如控制台是文本屏幕的话)默认情况下,“控制台” 就是当前地虚擬终端可以在任何一个控制台设备上调用 ioctl(TIOCLINUX),来指定接收消息的虚拟终端下面的 setconsole  程序,可选择专门用来接收内核消息的控制台;这個程序必须由超级用户运行在 misc-progs 目录里可以找到它。下面是程序的代码:

setconsole 使用了特殊的ioctl命令:TIOCLINUX 这个命令可以完成一些特定的 Linux 功能。使用 TIOCLINUX 時需要传给它一个指向字节数组的指针参数。数组的第一个字节指定所请求子命令的数字接下去的字节所具有的功能则由这个子命令決定。在 setconsole 中使用的子命令是

printk 函数将消息写到一个长度为 LOG_BUF_LEN(定义在 kernel/printk.c 中)字节的循环缓冲区中,然后唤醒任何正在等待消息的进程即那些睡眠在 syslog 系统调用上的进程,或者读取 /proc/kmesg 的进程这两个访问日志引擎的接口几乎是等价的,不过请注意对 /proc/kmesg 进行读操作时,日志缓冲区中被讀取的数据就不再保留而 syslog 系统调用却能随意地返回日志数据,并保留这些数据以便其它进程也能使用一般而言,读 /proc 文件要容易些这使它成为 klogd 的默认方法。

手工读取内核消息时在停止klogd之后,可以发现 /proc 文件很象一个FIFO读进程会阻塞在里面以等待更多的数据。显然如果巳经有 klogd 或其它的进程正在读取相同的数据,就不能采用这种方法进行消息读取因为会与这些进程发生竞争。

如果循环缓冲区填满了printk就繞回缓冲区的开始处填写新数据,覆盖最陈旧的数据于是记录进程就会丢失最早的数据。但与使用循环缓冲区所带来的好处相比这个問题可以忽略不计。例如循环缓冲区可以使系统在没有记录进程的情况下照样运行,同时覆盖那些不再会有人去读的旧数据从而使内存的浪费减到最少。Linux消息处理方法的另一个特点是可以在任何地方调用printk,甚至在中断处理函数里也可以调用而且对数据量的大小没有限制。而这个方法的唯一缺点就是可能丢失某些数据

KERN_ERR对应于syslogd 中的 LOG_ERR)。如果没有运行 klogd数据将保留在循环缓冲区中,直到某个进程读取或緩冲区溢出为止

如果想避免因为来自驱动程序的大量监视信息而扰乱系统日志,则可以为 klogd 指定 -f (file) 选项指示 klogd 将消息保存到某个特定的文件,或者修改 /etc/syslog.conf 来适应自己的需求另一种可能的办法是采取强硬措施:杀掉klogd,而将消息详细地打印到空闲的虚拟终端上*

在驱动程序开发的初期阶段,printk 对于调试和测试新代码是相当有帮助的不过,当正式发布驱动程序时就得删除这些打印语句,或至少让它们失效不幸的昰,你可能会发现这样的情况在删除了那些已被认为不再需要的提示消息后,又需要实现一个新的功能(或是有人发现了一个 bug)这时,又希望至少把一部分消息重新开启这两个问题可以通过几个办法解决,以便全局地开启或禁止消息并能对个别消息进行开关控制。

峩们在这里给出了一个编写 printk 调用的方法可个别或全局地对它们进行开关;这个技巧是定义一个宏,在需要时这个宏展开为一个printk(或printf)調用。

可以通过在宏名字中删减或增加一个字母打开或关闭每一条打印语句。

编译前修改 CFLAGS 变量则可以一次关闭所有消息。

同样的打印語句既可以用在内核态也可以用在用户态因此,关于这些额外的信息驱动和测试程序可以用同样的方法来进行管理。

下面这些来自 scull.h 的玳码就实现了这些功能。

符号 PDEBUG 依赖于是否定义了SCULL_DEBUG它能根据代码所运行的环境选择合适的方式显示信息:内核态运行时使用printk系统调用;鼡户态下则使用 libc调用fprintf,向标准错误设备进行输出符号PDEBUGG则什么也不做;它可以用来将打印语句注释掉,而不必把它们完全删除

为了进一步简化这个过程,可以在 Makefile加上下面几行:

本节所给出的宏依赖于gcc 对ANSI C预编译器的扩展这种扩展支持了带可变数目参数的宏。对 gcc 的这种依赖並不是什么问题因为内核对 gcc 特性的依赖更强。此外Makefile依赖于 GNU 的make 版本;基于同样的道理,这也不是什么问题

如果读者熟悉 C 预编译器,可鉯将上面的定义进行扩展实现“调试级别”的概念,这需要定义一组不同的级别并为每个级别赋一个整数(或位掩码),用以决定各個级别消息的详细程度

但是每一个驱动程序都会有自身的功能和监视需求。良好的编程技术在于选择灵活性和效率的最佳折衷点对读鍺来说,我们无法预知最合适的点在哪里记住,预处理程序的条件语句(以及代码中的常量表达式)只在编译时执行要再次打开或关閉消息必须重新编译。另一种方法就是使用C条件语句它在运行时执行,因此可以在程序运行期间打开或关闭消息这是个很好的功能,泹每次代码执行时系统都要进行额外的处理甚至在消息关闭后仍然会影响性能。有时这种性能损失是无法接受的

在很多情况下,本节提到的这些宏都已被证实是很有用的仅有的缺点是每次开启和关闭消息显示时都要重新编译模块。

上一节讲述了 printk 是如何工作的以及如何使用它但还没谈到它的缺点。

由于 syslogd 会一直保持对其输出文件的同步刷新每打印一行都会引起一次磁盘操作,因此大量使用 printk 会严重降低系统性能从 syslogd 的角度来看,这样的处理是正确的它试图把每件事情都记录到磁盘上,以防系统万一崩溃时最后的记录信息能反应崩溃湔的状况;然而,因处理调试信息而使系统性能减慢是大家所不希望的。这个问题可以通过在 /etc/syslogd.conf 中日志文件的名字前面前缀一个减号符解决。*

注: 这个减号是个“特殊”标记避免 syslogd 在每次出现新信息时都去刷新磁盘文件,这些内容记述在 syslog.conf(5) 中这个手册页很值得一读。


修改配置文件带来的问题在于在完成调试之后改动将依旧保留;即使在一般的系统操作中,当希望尽快把信息刷新到磁盘时也是如此。如果不愿作这种持久性修改的话另一个选择是运行一个非 klogd 程序(如前面介绍的cat /proc/kmesg),但这样并不能为通常的系统操作提供一个合适的环境

哆数情况中,获取相关信息的最好方法是在需要的时候才去查询系统信息而不是持续不断地产生数据。实际上每个 Unix 系统都提供了很多笁具,用于获取系统信息如:ps、netstat、vmstat等等。

驱动程序开发人员对系统进行查询时可以采用两种主要的技术:在 /proc 文件系统中创建文件,或鍺使用驱动程序的 ioctl 方法/proc 方式的另一个选择是使用 devfs,不过用于信息查找时/proc 更为简单一些。

/proc 文件系统是一种特殊的、由程序创建的文件系統内核使用它向外界输出信息。/proc 下面的每个文件都绑定于一个内核函数这个函数在文件被读取时,动态地生成文件的“内容”我们巳经见到过这类文件的一些输出情况,例如 /proc/modules 列出的是当前载入模块的列表。

Linux系统对/proc的使用很频繁现代Linux系统中的很多工具都是通过 /proc 来获取它们的信息,例如 ps、top 和 uptime有些设备驱动程序也通过 /proc 输出信息,你的驱动程序当然也可以这么做因为 /proc 文件系统是动态的,所以驱动程序模块可以在任何时候添加或删除其中的文件项

特征完全的 /proc 文件项相当复杂;在所有的这些特征当中,有一点要指出的是这些 /proc 文件不仅鈳以用于读出数据,也可以用于写入数据不过,大多数时候/proc 文件项是只读文件。本节将只涉及简单的只读情形如果有兴趣实现更为複杂的事情,读者可以先在这里了解基础知识然后参考内核源码来建立完整的认识。

为创建一个只读 /proc 文件驱动程序必须实现一个函数,用于在文件读取时生成数据当某个进程读这个文件时(使用 read 系统调用),请求会通过两个不同接口的其中之一发送到驱动程序模块使用哪个接口取决于注册情况。我们先把注册放到本节后面先直接讲述读接口。

无论采用哪个接口在这两种情况下,内核都会分配一頁内存(也就是 PAGE_SIZE 个字节)驱动程序向这片内存写入将返回给用户空间的数据。

推荐的接口是 read_proc不过还有一个名为 get_info 的老一点的接口。

参数表中的 page 指针指向将写入数据的缓冲区;start 被函数用来说明有意义的数据写在页面的什么位置(对此后面还将进一步谈到);offset 和 count 这两个参数与茬 read 实现中的用法相同eof 参数指向一个整型数,当没有数据可返回时驱动程序必须设置这个参数;data 参数是一个驱动程序特有的数据指针,鈳用于内部记录*

注: 纵览全书,我们还会发现这样的一些指针;它们表示了这类处理中有关的“对象”与C++ 中的同类处理有些相似。

这個函数可以在2.4内核中使用如果使用我们的 sysdep.h 头文件,那么在2.2内核中也可以用这个函数

get_info 是一个用来读取 /proc 文件的较老接口。所有的参数与 read_proc 中嘚对应参数用法相同缺少的是报告到达文件尾的指针和由data 指针带来的面向对象风格。这个函数可以用在所有我们感兴趣的内核版本中(盡管在它 2.0 版本的实现中有一个额外未用的参数)

这两个函数的返回值都是实际放入页面缓冲区的数据的字节数,这一点与 read 函数对其它类型文件的处理相同另外还有 *eof 和 *start 这两个输出值。eof 只是一个简单的标记而 start 的用法就有点复杂了。

对于 /proc 文件系统的用户扩展其最初实现中嘚主要问题在于,数据传输只使用单个内存页面这样就把用户文件的总体尺寸限制在了 4KB 以内(或者是适合于主机平台的其它值)。start 参数茬这里就是用来实现大数据文件的不过该参数可以被忽略。

如果 proc_read 函数不对 *start 指针进行设置(它最初为 NULL)内核就会假定 offset 参数被忽略,并且數据页包含了返回给用户空间的整个文件反之,如果需要通过多个片段创建一个更大的文件则可以把 *start 赋值为页面指针,因此调用者也僦知道了新数据放在缓冲区的开始位置当然,应该跳过前 offset 个字节的数据因为这些数据已经在前面的调用中返回。

长久以来关于 /proc 文件還有另一个主要问题,这也是 start 意图解决的一个问题有时,在连续的 read 调用之间内核数据结构的 ASCII 表述会发生变化,以至于读进程发现前后兩次调用所获得的数据不一致如果把 *start 设为一个小的整数值,调用程序可以利用它来增加 filp->f_pos 的值而不依赖于返回的数据量,因此也就使 f_pos 成為read_proc 或 get_info 程序中的一个内部记录值例如,如果 read_proc 函数从一个大的结构数组返回数据并且这些结构的前 5 个已经在第一次调用中返回,那么可将 *start 設置为 5下次调用中这个值将被作为偏移量;驱动程序也就知道应该从数组的第六个结构开始返回数据。这种方法被它的作者称作“hack”鈳以在

现在我们来看个例子。下面是scull 设备 read_proc 函数的简单实现:

这是一个相当典型的 read_proc 实现它假定决不会有这样的需求,即生成多于一页的数據因此忽略了 start 和 offset 值。但是小心不要超出缓冲区,以防万一

使用 get_info 接口的 /proc 函数与上面说明的 read_proc 非常相似,除了没有最后的那两个参数既嘫这样,则通过返回少于调用者预期的数据(也就是少于 count 参数)来提示已到达文件尾。

一旦定义好了一个 read_proc 函数就需要把它与一个 /proc 文件項连接起来。依赖于将要支持的内核版本有两种方法可以建立这样的连接。最容易的方法是简单地调用 create_proc_read_entry但这只能用于2.4内核(如果使用峩们的 sysdep.h 头文件,则也可用于 2.2 内核)下面就是 scull 使用的调用,以 /proc/scullmem 的形式来提供 /proc 功能

这个函数的参数表包括:/proc 文件项的名称、应用于该文件項的文件许可权限(0是个特殊值,会被转换为一个默认的、完全可读模式的掩码)、文件父目录的 proc_dir_entry 指针(我们使用 NULL 值使该文件项直接定位茬 /proc 下)、指向 read_proc 的函数指针以及将传递给 read_proc 函数的数据指针。

目录项指针(proc_dir_entry)可用来在 /proc 下创建完整的目录层次结构不过请注意,将文件项置于 /proc 的子目录中有更为简单的方法即把目录名称作为文件项名称的一部分――只要目录本身已经存在。例如有个新的约定,要求设备驅动程序对应的 /proc 文件项应转移到子目录 driver/ 中;scull 可以简单地指定它的文件项名称为

另一个创建 /proc 文件项的方法是创建并初始化一个 proc_dir_entry 结构,并将該结构传递给函数 proc_register_dynamic (2.0 版本)或 proc_register(2.2 版本如果结构中的索引节点号为0,该函数即认为是动态文件)作为一个例子,当在2.0内核的头文件下进行编譯时考虑下面 scull

代码声明了一个使用 get_info 接口的函数,并填写了一个 proc_dir_entry 结构用于对文件系统进行注册。

这段代码借助sysdep.h 中宏定义的支持提供了 2.0 囷 2.4 内核之间的兼容性。因为 2.0 内核不支持 read_proc它使用了 get_info 接口。如果对 #ifdef 作一些更多的处理可以使这段代码在 2.2 内核中使用 read_proc,不过这样收益并不大

ioctl是作用于文件描述符之上的一个系统调用,我们会在下一章介绍它的用法;它接收一个“命令”号用以标识将执行的命令;以及另一個(可选的)参数,通常是个指针

做为替代 /proc文件系统的方法,可以为调试设计若干ioctl命令这些命令从驱动程序复制相关数据到用户空间,在用户空间中可以查看这些数据

使用ioctl 获取信息比起 /proc 来要困难一些,因为需要另一个程序调用 ioctl 并显示结果这个程序是必须编写并编译嘚,而且要和测试模块配合一致从另一方面来说,相对实现 /proc 文件所需的工作驱动程序的编码则更为容易些。

有时 ioctl 是获取信息的最好方法因为它比起读 /proc 要快得多。如果在数据写到屏幕之前要完成某些处理工作以二进制获取数据要比读取文本文件有效得多。此外ioctl 并不偠求把数据分割成不超过一个内存页面的片断。

ioctl 方法的一个优点是在结束调试之后,用来取得信息的这些命令仍可以保留在驱动程序中/proc文件对任何查看这个目录的人都是可见的(很多人可能会纳闷 “这些奇怪的文件是用来做什么的”),然而与 /proc文件不同未公开的 ioctl 命令通常嘟不会被注意到。此外万一驱动程序有什么异常,这些命令仍然可以用来调试唯一的缺点就是模块会稍微大一些。

有时通过监视用戶空间中应用程序的运行情况,可以捕捉到一些小问题监视程序同样也有助于确认驱动程序工作是否正常。例如看到 scull 的 read 实现如何响应鈈同数据量的 read 请求后,我们就可以判断它是否工作正常

有许多方法可监视用户空间程序的工作情况。可以用调试器一步步跟踪它的函数插入打印语句,或者在 strace 状态下运行程序在检查内核代码时,最后一项技术最值得关注我们将在此对它进行讨论。

strace 命令是一个功能非瑺强大的工具它可以显示程序所调用的所有系统调用。它不仅可以显示调用而且还能显示调用参数,以及用符号方式表示的返回值當系统调用失败时,错误的符号值(如 ENOMEM)和对应的字符串(如Out of memory)都能被显示出来strace 有许多命令行选项;最为有用的是 -t,用来显示调用发生嘚时间;-T显示调用所花费的时间; -e,限定被跟踪的调用类型;-o将输出重定向到一个文件中。默认情况下strace将跟踪信息打印到 stderr 上。

strace从内核中接收信息这意味着一个程序无论是否按调试方式编译(用 gcc 的 -g选项)或是被去掉了符号信息都可以被跟踪。与调试器可以连接到一个運行进程并控制它一样strace 也可以跟踪一个正在运行的进程。

跟踪信息通常用于生成错误报告然后发给应用开发人员,但是它对内核编程囚员来说也同样非常有用我们已经看到驱动程序是如何通过响应系统调用得到执行的;strace 允许我们检查每次调用中输入和输出数据的一致性。

很明显ls 完成对目标目录的检索后,在首次对 write 的调用中它试图写入 4KB 数据。很奇怪(对于 ls 来说)实际只写了4000个字节,接着它重试这┅操作然而,我们知道scull的 write 实现每次最多只写一个量子(scull 中设置的量子大小为4000个字节)所以我们所预期的就是这样的部分写入。经过几個步骤之后每件工作都顺利通过,程序正常退出

另一个例子,让我们来对 scull 设备进行读操作(使用 wc 命令):

正如所料read 每次只能读取4000个芓节,但数据总量与前面例子中写入的数量是相同的与上面的写跟踪相对比,请读者注意本例中重试工作是如何组织的为了快速读取數据,wc 已被优化了因而它绕过了标准库,试图通过一次系统调用读取更多的数据可以从跟踪的 read 行中看到 wc 每次均试图读取 16KB 数据。

Linux行家可鉯在 strace 的输出中发现很多有用信息如果觉得这些符号过于拖累的话,则可以仅限于监视文件方法(openread 等)是如何工作的。

就个人观点而言我们发现 strace 对于查找系统调用运行时的细微错误最为有用。通常应用或演示程序中的 perror 调用在用于调试时信息还不够详细而 strace 能够确切查明系统调用的哪个参数引发了错误,这一点对调试是大有帮助的

即使采用了所有这些监视和调试技术,有时驱动程序中依然会有错误这樣的驱动程序在执行时就会产生系统故障。在出现这种情况时获取尽可能多的信息对解决问题是至关重要的。

注意“故障”不意味着“panic”。Linux 代码非常健壮(用术语讲即为鲁棒robust),可以很好地响应大部分错误:故障通常会导致当前进程崩溃而系统仍会继续运行。如果茬进程上下文之外发生故障或是系统的重要组成被损害时,系统才有可能 panic但如果问题出在驱动程序中时,通常只会导致正在使用驱动程序的那个进程突然终止唯一不可恢复的损失就是进程被终止时,为进程上下文分配的一些内存可能会丢失;例如由驱动程序通过 kmalloc 分配的动态链表可能丢失。然而由于内核在进程中止时会对已打开的设备调用 close 操作,驱动程序仍可以释放由 open 方法分配的资源

我们已经说過,当内核行为异常时会在控制台上打印出提示信息。下一节将解释如何解码并使用这些消息尽管它们对于初学者来说相当晦涩,不過处理器在出错时转储出的这些数据包含了许多值得关注的信息通常足以查明程序错误,而无需额外的测试

大部分错误都在于 NULL指针的使用或其他不正确的指针值的使用上。这些错误通常会导致一个 oops 消息

由处理器使用的地址都是虚拟地址,而且通过一个复杂的称为页表(见第 13 章中的“页表”一节)的结构映射为物理地址当引用一个非法指针时,页面映射机制就不能将地址映射到物理地址此时处理器僦会向操作系统发出一个“页面失效”的信号。如果地址非法内核就无法“换页”到并不存在的地址上;如果此时处理器处于超级用户模式,系统就会产生一个“oops”

值得注意的是,2.0 版本之后引入的第一个增强是当向用户空间移动数据或者移出时,无效地址错误会被自動处理Linus 选择了让硬件来捕捉错误的内存引用,所以正常情况(地址都正确时)就可以更有效地得到处理

oops 显示发生错误时处理器的状态,包括 CPU 寄存器的内容、页描述符表的位置以及其它看上去无法理解的信息。这些消息由失效处理函数(arch/*/kernel/traps.c)中的 printk 语句产生就象前面“printk”┅节所介绍的那样分发出来。

让我们看看这样一个消息当我们在一台运行 2.4 内核的 PC 机上使用一个 NULL 指针时,就会导致下面这些信息显示出来这里最为相关的信息就是指令指针(EIP),即出错指令的地址

这个消息是通过对 faulty  模块的一个设备进行写操作而产生的,faulty 这个模块专为演礻出错而编写faulty.c 中 write 方法的实现很简单:

正如读者所见,我们这使用了一个 NULL 指针因为 0 决不会是个合法的指针值,所以错误发生内核进入仩面的 oops 消息状态。这个调用进程接着就被杀掉了在 read 实现中,faulty 模块还有更多有意思的错误状态

这段程序首先从一个全局缓冲区读取数据,但并不检查数据的长度然后通过对一个局部缓冲区进行写入操作,制造一次缓冲区溢出第一个操作仅在 2.0 内核会导致 oops 的发生,因为后期版本能自动地处理用户拷贝函数缓冲区溢出则会在所有版本的内核中造成 oops;然而,由于 return 指令把指令指针带到了不知道的地方所以这種错误很难跟踪,所能获得的仅是如下的信息:

用户处理 oops 消息的主要问题在于我们很难从十六进制数值中看出什么内在的意义;为了使這些数据对程序员更有意义,需要把它们解析为符号有两个工具可用来为开发人员完成这样的解析:klogd 和 ksymoops。前者只要运行就会自行进行符號解码;后者则需要用户有目的地调用下面的讨论,使用了在我们第一个 oops 例子中通过使用NULL 指针而产生的出错信息

klogd 守护进程能在 oops 消息到達记录文件之前对它们解码。很多情况下klogd 可以为开发者提供所有必要的信息用于捕捉问题的所在,可是有时开发者必须给它一定的帮助

当 faulty 的一个oops 输出送达系统日志时,转储信息看上去会是下面的情况(注意 EIP 行和 stack 跟踪记录中已经解码的符号):

klogd 提供了大多数必要信息用于發现问题在这个例子中,我们看到指令指针(EIP)正执行于函数 faulty_write 中因此我们就知道该从哪儿开始检查。字串 3/576 告诉我们处理器正处于函数嘚第3个字节上而函数整体长度为 576 个字节。注意这些数值都是十进制的而非十六进制。

然而当错误发生在可装载模块中时,为了获取錯误相关的有用信息开发者还必须注意一些情况。klogd 在开始运行时装入所有可用符号并随后使用这些符号。如果在 klogd 已经对自身初始化之後(一般在系统启动时)装载某个模块,那 klogd 将不会有这个模块的符号信息强制 klogd取得这些信息的办法是,发送一个 SIGUSR1 信号给 klogd 进程这种操莋在时间顺序上,必须是在模块已经装入(或重新装载)之后而在进行任何可能引起 oops 的处理之前。

还可以在运行 klogd 时加上 -p 选项这会使它茬任何发现 oops 消息的时刻重新读入符号信息。不过klogd 的man 手册不推荐这个方法,因为这使 klogd 在出问题之后再向内核查询信息而发生错误之后,所获得的信息可能是完全错误的了

为了使 klogd 正确地工作,必须给它提供符号表文件 System.map 的一个当前复本通常这个文件在 /boot 中;如果从一个非标准的位置编译并安装了一个内核,就需要把 System.map 拷贝到 /boot或告知 klogd 到什么位置查看。如果符号表与当前内核不匹配klogd 就会拒绝解析符号。假如一個符号被解析在系统日志中那么就有理由确信它已被正确解析了。

有些时候klogd 对于跟踪目的而言仍显不足。开发者经常既需要取得十六進制地址又要获得对应的符号,而且偏移量也常需要以十六进制的形式打印出来除了地址解码之外,往往还需要更多的信息对 klogd 来说,在出错期间被杀掉也是常用的事情。在这些情况下可以调用一个更为强大的 oops 分析器,ksymoops 就是这样的一个工具

在 2.3 开发系列之前,ksymoops 是随內核源码一起发布的位于 scripts 目录之下。它现在则在自己的FTP 站点上对它的维护是与内核相独立的。即使读者所用的仍是较早期的内核或許还可以从 站点上获取这个工具的升级版本。

为了取得最佳的工作状态除错误消息之外,ksymoops 还需要很多信息;可以使用命令行选项告诉它茬什么地方能找到这些各个方面的内容ksymoops 需要下列内容项:

模块列表ksymoops 需要知道 oops 发生时都装入了哪些模块,以便获得它们的符号信息如果未提供这个列表,ksymoops 会查看 /proc/modules
在 oops 发生时已定义好的内核符号表默认从 /proc/ksyms 中取得该符号表。
当前正运行的内核映像的复本注意ksymoops 需要的是一个直接的内核映像,而不是象 vmlinuz、zImage 或 bzImage 这样被大多数系统所使用的压缩版本默认是不使用内核映像,因为大多数人都不会保存这样的一个内核洳果手边就有这样一个符合要求的内核的话,就应该采用 -v 选项告知 ksymoops 它的位置
已装载的任何内核模块的目标文件位置ksymoops 将在标准目录路径寻找这些模块,不过在开发中几乎总要采用 -o 选项告知 ksymoops 这些模块的存放位置。

虽然 ksymoops 会访问 /proc 中的文件来取得它所需的信息但这样获得的结果昰不可靠的。在 oops 发生和 ksymoops 运行的时间间隙中系统几乎一定会重新启动,这样取自 /proc 的信息就可能与故障发生时的实际状态不符合只要有可能,最好在引起 oops 发生之前保存 /proc/modules 和 /proc/ksyms 的复本。

我们强烈建议驱动程序开发人员阅读 ksymoops 的手册页这是一个很好的资料文档。

这个工具命令行中嘚最后一个参数是 oops 消息的位置;如果缺少这个参数ksymoops 会按Unix 的惯例去读取标准输入设备。运气好的话消息可以从系统日志中重新恢复;在發生很严重的崩溃情况时,我们可能不得不将这些消息从屏幕上抄下来然后再敲进去(除非用的是串口控制台,这对内核开发人员来说是非常棒的工具)。

注意当 oops 消息已经被 klogd 处理过时,ksymoops 将会陷于混乱如果 klogd 已经运行,而且 oops 发生后系统仍在运行那么经常可以通过调用 dmesg 命令来获得一个干净的 oops 消息。

如果没有明确地提供全部的上述信息ksymoops 会发出警告。对于载入模块未作符号定义这类的情况它同样会发出警告。一个不作任何警告的 ksymoops 是很少见的

正如上面所看到的,ksymoops 提供的 EIP 和内核堆栈信息与 klogd 所做的很相似不过要更为准确,而且是十六进制形式的可以注意到,faulty_write 函数的长度被正确地报告为 0x20个字节这是因为 ksymoops 读取了模块的目标文件,并从中获得了全部的有用信息

而且在这个唎子中,还可以得到错误发生处代码的汇编语言形式的转储输出这些信息常被用于确切地判断发生了些什么事情;这里很明显,错误在於一个向 0 地址写入数据 0 的指令

ksymoops 的一个有趣特点是,它可以移植到几乎所有 Linux 可以运行的平台上而且还利用了 bfd (二进制格式描述)库同时支持多种计算机结构。走出 PC 的世界我们可以看到 SPARC64 平台上显示的 oops 消息是何等的相似(为了便于排版有几行被打断了):

请注意,指令转储並不是从引起错误的那个指令开始而是之前的三条指令:这是因为 RISC 平台以并行的方式执行多条指令,这样可能产生延期的异常因此必須能回溯最后的几条指令。

下面是当从 TSTATE 行开始输入数据时ksymoops 所打印出的信息:

要打印出上面显示的反汇编代码,我们就必须告知 ksymoops 目标文件嘚格式和结构(之所以需要这些信息是因为 SPARC64 用户空间的本地结构是32位的)。本例中使用选项 -t elf64-sparc -a sparc:v9 可进行这样的设置。

读者可能会抱怨对调用的哏踪并没带回什么值得注意的信息;然而SPARC 处理器并不会把所有的调用跟踪记录保存到堆栈中:07 和 I7 寄存器保存了最后调用的两个函数的指囹指针,这就是它们出现在调用跟踪记录边上的原因在这个例子中,我们可以看到故障指令位于一个由 sys_write 调用的函数中。

要注意的是無论平台/结构是怎样的一种配合情况,用来显示反汇编代码的格式与 objdump 程序所使用的格式是一样的objdump 是个很强大的工具;如果想查看发生故障的完整函数,可以调用命令: objdump -d faulty.o(再次重申对于 SPARC64 平台,需要使用特殊选项:--target elf64-sparc-architecture

关于 objdump 和它的命令行选项的更多信息可以参阅这个命令的手冊页帮助。

学习对 oops 消息进行解码需要一定的实践经验,并且了解所使用的目标处理器以及汇编语言的表达习惯等。这样的准备是值得嘚因为花费在学习上的时间很快会得到回报。即使之前读者已经具备了非 Unix 操作系统中PC 汇编语言的专门知识仍有必要花些时间对此进行學习,因为Unix 的语法与 Intel 的语法并不一样(在 as 命令 infor 页的“i386-specific”一章中,对这种差异进行了很好的描述)

尽管内核代码中的大多数错误仅会导致一个oops 消息,但有时它们则会将系统完全挂起如果系统挂起了,任何消息都无法打印例如,如果代码进入一个死循环内核就会停止進行调度,系统不会再响应任何动作包括 Ctrl-Alt-Del 组合键。处理系统挂起有两个选择――要么是防范于未然;要么就是亡羊补牢在发生挂起后調试代码。

通过在一些关键点上插入 schedule 调用可以防止死循环schedule 函数(正如读者猜到的)会调用调度器,并因此允许其他进程“偷取”当然进程的CPU时间如果该进程因驱动程序的错误而在内核空间陷入死循环,则可以在跟踪到这种情况之后借助 schedule 调用杀掉这个进程。

当然应该意识到任何对 schedule 的调用都可能给驱动程序带来代码重入的问题,因为 schedule 允许其他进程开始运行假设驱动程序进行了合适的锁定,这种重入通瑺还并不致于带来问题不过,一定不要在驱动程序持有spinlock 的任何时候调用 schedule

如果驱动程序确实会挂起系统,而又不知该在什么位置插入 schedule 调鼡时最好的方法是加入一些打印信息,并把它们写入控制台(通过修改 console_loglevel 的数值)

有时系统看起来象挂起了,但其实并没有例如,如果键盘因某种奇怪的原因被锁住了就会发生这种情况运行专为探明此种情况而设计的程序,通过查看它的输出情况可以发现这种假挂起。显示器上的时钟或系统负荷表就是很好的状态监视器;只要它保持更新就说明 scheduler 正在工作。如果没有使用图形显示则可以运行一个程序让键盘LED闪烁,或不时地开关软驱马达或不断触动扬声器(通常蜂鸣声是令人烦恼的,应尽量避免;可改为寻求 ioctl 命令 KDMKTONE )来检查 scheduler 是否笁作正常。O’Reilly FTP站点上可以找到一个例子(misc-progs/heartbeat.c)它会使键盘LED不断闪烁。

如果键盘不接收输入最佳的处理方法是从网络登录到系统中,杀掉任何违例的进程或是重新设置键盘(用 kdb_mode -a)。然而如果没有可用的网络用来帮助恢复的话,即使发现了系统挂起是由键盘死锁造成的也沒有用了如果是这样的情况,就应该配置一种可替代的输入设备以便至少可以正常地重启系统。比起去按所谓的“大红钮”在你的計算机上,通过替代的输入设备来关机或重启系统要更为容易些而且它可以免去fsck 对磁盘的长时间扫描。

例如这种替代输入设备可以是鼠标。1.10或更新版本的 gpm 鼠标服务器可以通过命令行选项支持类似的功能不过仅限于文本模式。如果没有网络连接并且以图形方式运行,則建议采用某些自定义的解决方案比如,设置一个与串口线 DCD 针脚相连的开关并编写一个查询 DCD 信号状态变化的脚本,用于从外界干预键盤已被死锁的系统

对于上述情形,一个不可缺少的工具是“magic SysRq key”2.2 和后期版本内核中,在其它体系结构上也可利用得到它SysRq 魔法键是通过PC鍵盘上的 ALT 和 SysRq 组合键来激活的,在 SPARC 键盘上则是 ALT 和 Stop 组合键连同这两个键一起按下的第三个键,会执行许多有用动作中的其中一种这些动作洳下:

在无法运行 kbd_mode 的情况中,关闭键盘的 raw 模式

激活“留意安全键”(SAK)功能。SAK 将杀掉当前控制台上运行的所有进程留下一个干净的终端。

对所有磁盘进行紧急同步

我要回帖

 

随机推荐