linux内核调试技术_第1页
linux内核调试技术_第2页
linux内核调试技术_第3页
linux内核调试技术_第4页
linux内核调试技术_第5页
已阅读5页,还剩33页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、linux内核调试技术关键词:linux 内核 调试技术调试技术对于任何一位内核代码的编写者来说,最急迫的问题之一就是如何完成调试。由于内核是一个不与特 定进程相关的功能集合,所以内核代码无法轻易地放在调试器中执行,而且也很难跟踪。同样,要想 复现内核代码中的错误也是相当困难的,因为这种错误可能导致整个系统崩溃,这样也就破坏了可以 用来跟踪它们的现场。本章将介绍在这种令人痛苦的环境下监视内核代码并跟踪错误的技术。4.1通过打印调试最普通的调试技术就是监视,即在应用程序编程中,在一些适当的地点调用printf显示监视信息。调试内核代码的时候,则可以用printk来完成相同的工作。4.1.1 pr

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

3、intk(KERN_DEBUG "Here I am: %s:%in", _ _FILE_ _, _ _LINE_ _);printk(KERN_CRIT "I'm trashed; giving up on %pn", ptr);在头文件<linux/kernel.h> 中定义了 8种可用的日志级别字符串KERN_EMERG用于紧急事件消息,它们一般是系统崩溃之前提示的消息。KERN_ALERT用于需要立即采取动作的情况。KERN_CRIT临界状态,通常涉及严重的硬件或软件操作失败。KERN_ERR用于报告错误状态;设备驱动程序会经

4、常使用KERN_ERR来报告来自硬件的问题。KERN_WARNING对可能岀现问题的情况进行警告,这类情况通常不会对系统造成严重问题。KERN_NOTICE有必要进行提示的正常情形。许多与安全相关的状况用这个级别进行汇报。KERN_INFO提示性信息。很多驱动程序在启动的时候,以这个级别打印岀它们找到的硬件信息。KERN_DEBUG0到7,数值越小,优先级用于调试信息。每个字符串(以宏的形式展开)代表一个尖括号中的整数。整数值的范围从就越高。没有指定优先级的printk语句默认采用的级别是DEFAULT_MESSAGE_LOGLEVEL ,这个宏在文件kernel/printk.c中指定为一个

5、整数值。在Linux的开发过程中,这个默认的级别值已经有过好几次变化,所以我们建议读者始终指定一个明确的级别。根据日志级别,内核可能会把消息打印到当前控制台上,这个控制台可以是一个字符模式的终端、一个串口打印机或是一个并口打印机。如果优先级小于console_loglevel这个整数值的话,消息才能显示出来。如果系统同时运行了klogd 和syslogd,则无论console_loglevel为何值,内核消息都将追加到/var/log/messages 中(否则的话,除此之外的处理方式就依赖于对syslogd的设置)。如果klogd没有运行,这些消息就不会传递到用户空间,这种情况下,就只好查看

6、/proc/kmsg 了。,而且还可以通过 sys_syslog变量 console_loglevel 的初始值是 DEFAULT_CONSOLE_LOGLEVEL系统调用进行修改。调用klogd时可以指定-c开关选项来修改这个变量,klogd的man手册页对此有详细说明。注意,要修改它的当前值,必须先杀掉klogd,再加-c选项重新启动它。此外,还可以编写程序来改变控制台日志级别。读者可以在0' Reilly的FTP站点提供的源文件 miscprogs/setlevel.c里找到这样的一段程序。新优先级被指定为一个1到8之间的整数值。如果值被设为1,则只有级别为 0 (KERN_EM

7、ERG )的消息才能到达控制台;如果设为8,则包括调试信息在内的所有消息都能显示出来。如果在控制台上工作,而且常常遇到内核错误(参见本章后面的“调试系统故障” 一节)的话,就有 必要降低日志级别,因为出错处理代码会把console_loglevel增为它的最大数值,导致随后的所有消息都显示在控制台上。如果需要查看调试信息,就有必要提高日志级别;这在远程调试内核,并且在 交互会话未使用文本控制台的情况下,是很有帮助的。从2.1.31这个版本起,可以通过文本文件/proc/sys/kernel/printk来读取和修改控制台的日志级别。这个文件容纳了 4个整数值。读者可能会对前面两个感兴趣:控制台

8、的当前日志级别和默认日志级别。例如,在最近的这些内核版本中,可以通过简单地输入下面的命令使所有的内核消息得到显示:# echo 8 > /proc/sys/kernel/printk不过,如果仍在 2.0版本下的话,就需要使用 setlevel这样的工具了。现在大家应该清楚为什么在hello.c范例中使用1这些标记了,它们用来确保这些消息能在控制台上显示出来。对于控制台日志策略,Linux考虑到了某些灵活性,也就是说,可以发送消息到一个指定的虚拟控制台(假如控制台是文本屏幕的话)。默认情况下,“控制台”就是当前地虚拟终端。可以在任何一个控制 台设备上调用ioctl (TIOCLINUX)

9、,来指定接收消息的虚拟终端。下面的setconsole 程序,可选择专门用来接收内核消息的控制台;这个程序必须由超级用户运行,在misc-progs目录里可以找到它。下面是程序的代码:int main(int argc, char *argv)char bytes2 = 11,0;/* 11 is the TIOCLINUX cmd number */if (argc=2) bytes1 = atoi(argv1); /* the chosen console */else fprintf(stderr, "%s: need a single argn",argvO); e

10、xit(1);if (ioctl(STDIN_FILENO, TIOCLINUX, bytes)<0) /* use stdin */fprintf(stderr,"%s: ioctl(stdin, TIOCLINUX): %sn",argv0, strerror(errno);exit(1);exit(O);setconsole 使用了特殊的ioctl命令:TIOCLINUX,这个命令可以完成一些特定的Linux功能。使用TIOCLINUX时,需要传给它一个指向字节数组的指针参数。数组的第一个字节指定所请求子命令的 数字,接下去的字节所具有的功能则由这个子命令决定。

11、在setconsole中,使用的子命令是11,后面那个字节(存于 bytes1中)标识虚拟控制台。关于TIOCLINUX 的详尽描述可以在内核源码中的drivers/char/tty_io.c文件得至叽4.1.2消息如何被记录printk函数将消息写到一个长度为LOG_BUF_LEN (定义在kernel/printk.c 中)字节的循环缓冲区中,然后唤醒任何正在等待消息的进程,即那些睡眠在syslog系统调用上的进程,或者读取/proc/kmesg的进程。这两个访问日志引擎的接口几乎是等价的,不过请注意,对/proc/kmesg 进行读操作时,日志缓冲区中被读取的数据就不再保留,而syslo

12、g系统调用却能随意地返回日志数据,并保留这些数据以便其它进程也能使用。一般而言,读/proc文件要容易些,这使它成为klogd的默认方法。手工读取内核消息时,在停止klogd之后,可以发现/proc文件很象一个FIFO,读进程会阻塞在里面以等待更多的数据。显然,如果已经有klogd或其它的进程正在读取相同的数据,就不能采用这种方法进行消息读取,因为会与这些进程发生竞争。如果循环缓冲区填满了,printk就绕回缓冲区的开始处填写新数据,覆盖最陈旧的数据,于是记录进程就会丢失最早的数据。但与使用循环缓冲区所带来的好处相比,这个问题可以忽略不计。例如,循环 缓冲区可以使系统在没有记录进程的情况下照样

13、运行,同时覆盖那些不再会有人去读的旧数据,从而 使内存的浪费减到最少。Linux消息处理方法的另一个特点是,可以在任何地方调用printk,甚至在中断处理函数里也可以调用,而且对数据量的大小没有限制。而这个方法的唯一缺点就是可能丢失某些 数据。klogd运行时,会读取内核消息并将它们分发到syslogd,syslogd随后查看/etc/syslog.conf ,找出处理这些数据的方法。syslogd根据设施和优先级对消息进行区分;这两者的允许值均定义在<sys/s yslog.h中。内核消息由 LOG_KERN 设施记录,并以 printk中使用的优先级记录(例如, printk 中使用

14、的 KERN_ERR对应于syslogd中的LOG_ERR )。如果没有运行 klogd,数据将保留在循环 缓冲区中,直到某个进程读取或缓冲区溢岀为止。如果想避免因为来自驱动程序的大量监视信息而扰乱系统日志,则可以为klogd指定-f (file)选项,指示klogd将消息保存到某个特定的文件,或者修改/etc/syslog.conf来适应自己的需求。另一种可能的办法是采取强硬措施:杀掉klogd,而将消息详细地打印到空闲的虚拟终端上。*注:例如,使用下面的命令可设置10号终端用于消息的显示:setlevel 8setconsole 10或者在一个未使用的 xterm 上执行cat /proc

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

16、tf)调用。可以通过在宏名字中删减或增加一个字母,打开或关闭每一条打印语句。编译前修改CFLAGS变量,则可以一次关闭所有消息。同样的打印语句既可以用在内核态也可以用在用户态,因此,关于这些额外的信息,驱动和测试程序 可以用同样的方法来进行管理。下面这些来自scull.h的代码,就实现了这些功能。#ifdef SCULL_DEBUG# ifdef _ _KERNEL_ _/* This one if debugging is on, and kernel space */# define PDEBUG(fmt, args.) printk( KERN_DEBUG "scull: &q

17、uot; fmt,# args)# else/* This one for user space */# define PDEBUG(fmt, args.) fprintf(stderr, fmt, # args)# endif#else# define PDEBUG(fmt, args.) /* not debugging: nothing */#endif#undef PDEBUGG#define PDEBUGG(fmt, args.) /* nothing: it's a placeholder */符号PDEBUG依赖于是否定义了 SCULL_DEBUG,它能根据代码所运行的环

18、境选择合适的方式显示 信息:内核态运行时使用 printk系统调用;用户态下则使用libc调用fprintf,向标准错误设备进行输出 符号PDEBUGG则什么也不做;它可以用来将打印语句注释掉,而不必把它们完全删除。为了进一步简化这个过程,可以在 Makefile加上下面几行:# Comment/uncomment the following line to disable/enable debuggingDEBUG = y# Add your debugging flag (or not) to CFLAGSifeq ($(DEBUG),y)DEBFLAGS = -O -g -DSCULL_

19、DEBUG # "-O" is needed to expand inlineselseDEBFLAGS = -02endifCFLAGS += $(DEBFLAGS)本节所给出的宏依赖于 gcc对ANSI C预编译器的扩展,这种扩展支持了带可变数目参数的宏。对gcc的这种依赖并不是什么问题,因为内核对 gcc特性的依赖更强。此外, Makefile依赖于GNU的 make版本;基于同样的道理,这也不是什么问题。如果读者熟悉 C预编译器,可以将上面的定义进行扩展,实现“调试级别”的概念,这需要定义一组 不同的级别,并为每个级别赋一个整数(或位掩码),用以决定各个级别消息的详

20、细程度。但是每一个驱动程序都会有自身的功能和监视需求。良好的编程技术在于选择灵活性和效率的最佳折 衷点,对读者来说,我们无法预知最合适的点在哪里。记住,预处理程序的条件语句(以及代码中的 常量表达式)只在编译时执行,要再次打开或关闭消息必须重新编译。另一种方法就是使用C条件语句,它在运行时执行,因此可以在程序运行期间打开或关闭消息。这是个很好的功能,但每次代码执 行时系统都要进行额外的处理,甚至在消息关闭后仍然会影响性能。有时这种性能损失是无法接受的。在很多情况下,本节提到的这些宏都已被证实是很有用的,仅有的缺点是每次开启和关闭消息显示时 都要重新编译模块。4.2通过查询调试上一节讲述了pri

21、ntk是如何工作的以及如何使用它,但还没谈到它的缺点。由于syslogd会一直保持对其输岀文件的同步刷新,每打印一行都会引起一次磁盘操作,因此大量使 用printk会严重降低系统性能。从syslogd的角度来看,这样的处理是正确的。它试图把每件事情都记录到磁盘上,以防系统万一崩溃时,最后的记录信息能反应崩溃前的状况;然而,因处理调试信息 而使系统性能减慢,是大家所不希望的。这个问题可以通过在/etc/syslogd.conf中日志文件的名字前面,前缀一个减号符解决。*注:这个减号是个 特殊"标记,避免syslogd在每次岀现新信息时都去刷新磁盘文件,这些内容记述在syslog.con

22、f(5)中,这个手册页很值得一读。修改配置文件带来的问题在于,在完成调试之后改动将依旧保留;即使在一般的系统操作中,当希望尽快把信息刷新到磁盘时,也是如此。如果不愿作这种持久性修改的话,另一个选择是运行一个非klogd程序(如前面介绍的cat /proc/kmesg ),但这样并不能为通常的系统操作提供一个合适的环境。多数情况中,获取相关信息的最好方法是在需要的时候才去查询系统信息,而不是持续不断地产生数据。实际上,每个 Unix系统都提供了很多工具,用于获取系统信息,如:ps、netstat、vmstat等等。驱动程序开发人员对系统进行查询时,可以采用两种主要的技术:在/proc文件系统中创

23、建文件,或者使用驱动程序的ioctl方法。/proc方式的另一个选择是使用devfs,不过用于信息查找时,/proc更为简单一些。4.2.1使用/proc文件系统/proc文件系统是一种特殊的、由程序创建的文件系统,内核使用它向外界输岀信息。/proc下面的每个文件都绑定于一个内核函数,这个函数在文件被读取时,动态地生成文件的“内容”。我们已经见到过这类文件的一些输岀情况,例如,/proc/modules 列岀的是当前载入模块的列表。Linux系统对/proc的使用很频繁。现代 Linux系统中的很多工具都是通过/proc来获取它们的信息,例如ps、top和uptime。有些设备驱动程序也通过

24、 /proc输出信息,你的驱动程序当然也可以这么 做。因为/proc文件系统是动态的,所以驱动程序模块可以在任何时候添加或删除其中的文件项。特征完全的/proc文件项相当复杂;在所有的这些特征当中,有一点要指岀的是,这些/proc文件不仅可以用于读出数据,也可以用于写入数据。不过,大多数时候,/proc文件项是只读文件。本节将只涉及简单的只读情形。如果有兴趣实现更为复杂的事情,读者可以先在这里了解基础知识,然后参考 内核源码来建立完整的认识。所有使用/proc的模块必须包含 <linux/proc_fs.h>,通过这个头文件定义正确的函数。为创建一个只读/proc文件,驱动程序必须

25、实现一个函数,用于在文件读取时生成数据。当某个进程 读这个文件时(使用read系统调用),请求会通过两个不同接口的其中之一发送到驱动程序模块,使用哪个接口取决于注册情况。我们先把注册放到本节后面,先直接讲述读接口。无论采用哪个接口,在这两种情况下,内核都会分配一页内存(也就是PAGE_SIZE 个字节),驱动程序向这片内存写入将返回给用户空间的数据。推荐的接口是read_proc,不过还有一个名为get_info的老一点的接口int (*read_proc)(char *page, char *start, off_t offset, int count, int *eof, void *da

26、ta);参数表中的page指针指向将写入数据的缓冲区;start被函数用来说明有意义的数据写在页面的什么 位置(对此后面还将进一步谈到);offset和count这两个参数与在 read实现中的用法相同。eof 参数指向一个整型数,当没有数据可返回时,驱动程序必须设置这个参数;data参数是一个驱动程序特有的数据指针,可用于内部记录。*注:纵览全书,我们还会发现这样的一些指针;它们表示了这类处理中有关的 对象”,与C+中的同类处理有些相似。这个函数可以在2.4内核中使用,如果使用我们的 sysdep.h头文件,那么在2.2内核中也可以用这个 函数。int (*get_info)(char *p

27、age, char *start, off_t offset, int count);get_info是一个用来读取/proc文件的较老接口。所有的参数与 read_proc中的对应参数用法相同。 缺少的是报告到达文件尾的指针和由 data指针带来的面向对象风格。这个函数可以用在所有我们感兴 趣的内核版本中(尽管在它 2.0版本的实现中有一个额外未用的参数) 。这两个函数的返回值都是实际放入页面缓冲区的数据的字节数,这一点与read函数对其它类型文件的处理相同。另外还有*eof和*start这两个输出值。eof只是一个简单的标记,而start的用法就有 点复杂了。对于/proc文件系统的用户扩

28、展,其最初实现中的主要问题在于,数据传输只使用单个内存页面。这样就把用户文件的总体尺寸限制在了4KB以内(或者是适合于主机平台的其它值)。start参数在这里就是用来实现大数据文件的,不过该参数可以被忽略。如果proc_read函数不对*start指针进行设置(它最初为 NULL ),内核就会假定 offset参数被忽略, 并且数据页包含了返回给用户空间的整个文件。反之,如果需要通过多个片段创建一个更大的文件, 则可以把*start赋值为页面指针,因此调用者也就知道了新数据放在缓冲区的开始位置。当然,应该 跳过前offset个字节的数据,因为这些数据已经在前面的调用中返回。长久以来,关于/pr

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

30、返回数据。这种方法被它的作者称作“hack ”,可以在/fs/proc/generic.c 中看到。现在我们来看个例子。下面是scull设备read_proc函数的简单实现:int scull_read_procmem(char *buf, char *start, off_t offset,int count, int *eof, void *data)int i, j, len = 0;int limit = count - 80; /* Don't print more than this */for (i = 0; i < scull_nr_devs &&

31、 len <= limit; i+) Scull_Dev *d = & scull_devices i;if (down_interruptible(&d->sem)return -ERESTARTSYS;len += sprintf(buf+len,"nDevice %i: qset %i, q %i, sz %lin",i, d->qset, d->quantum, d->size);for (; d && len <= limit; d = d->next) /* scan the list *

32、/ len += sprintf(buf+len, " item at %p, qset at %pn", d, d->data);if (d->data && !d->next) /* dump only the last item-save space */ for (j = 0; j < d->qset; j+) if (d->dataj)len += sprintf(buf+len," % 4i: %8pn", j,d->dataj);up(&scull_devices i.sem

33、);*eof = 1;return len;这是一个相当典型的read_proc实现。它假定决不会有这样的需求,即生成多于一页的数据,因此忽略了 start和offset值。但是,小心不要超出缓冲区,以防万一。使用get_info接口的/proc函数与上面说明的read_proc非常相似,除了没有最后的那两个参数。既然这样,则通过返回少于调用者预期的数据(也就是少于count参数),来提示已到达文件尾。一旦定义好了一个read_proc函数,就需要把它与一个/proc文件项连接起来。依赖于将要支持的内核版本,有两种方法可以建立这样的连接。最容易的方法是简单地调用create_proc_read

34、_entry ,但这只能用于2.4内核(如果使用我们的sysdep.h头文件,则也可用于2.2内核)。下面就是scull使用的调用,以/proc/scullmem 的形式来提供/proc功能。create_proc_read_entry("scullmem",0/* default mode */,NULL /* parent dir */,scull_read_procmem,NULL /* client data */);这个函数的参数表包括:/proc文件项的名称、应用于该文件项的文件许可权限(0是个特殊值,会被转换为一个默认的、完全可读模式的掩码)、文件父目录的 p

35、roc_dir_entry 指针(我们使用 NULL值使该文件项直接定位在/proc下)、指向read_proc的函数指针,以及将传递给read_proc函数的数据指针。目录项指针(proc_dir_entry )可用来在/proc下创建完整的目录层次结构。不过请注意,将文件项置 于/proc的子目录中有更为简单的方法,即把目录名称作为文件项名称的一部分一一只要目录本身已 经存在。例如,有个新的约定,要求设备驱动程序对应的/proc文件项应转移到子目录driver/中;scull可以简单地指定它的文件项名称为driver/scullmem,从而把它的/proc文件放到这个子目录中。当然,在模块

36、卸载时,/proc中的文件项也应被删除。remove_proc_entry 就是用来撤消 create_proc_read_entry 所做工作的函数。remove_proc_entry("scullmem", NULL /* parent dir */);另一个创建/proc文件项的方法是,创建并初始化一个proc_dir_entry结构,并将该结构传递给函数proc_register_dynamic (2.0版本)或proc_register (2.2版本,如果结构中的索引节点号为 0,该函 数即认为是动态文件)。作为一个例子,当在 2.0内核的头文件下进行编译时,考虑

37、下面scull所使用的这些代码:static int scull_get_info(char *buf, char *start, off_t offset,int len, int unused)int eof = 0;return scull_read_procmem (buf, start, offset, len, &eof, NULL);struct proc_dir_entry scull_proc_entry = namelen:8,name:"scullmem",mode:S_IFREG | S_IRUGO,nlink:1,get_info:scul

38、l_get_info,;static void scull_create_proc()proc_register_dynamic (&proc_root, & scull_proc_entry);static void scull_remove_proc()proc_unregister(&proc_root, scull_proc_entry .Io w_ino);代码声明了一个使用get_info接口的函数,并填写了一个 proc_dir_entry 结构,用于对文件系统进行注册。这段代码借助sysdep.h中宏定义的支持,提供了 2.0和2.4内核之间的兼容性。因

39、为2.0内核不支 持read_proc,它使用了 get_info接口。如果对 #ifdef作一些更多的处理,可以使这段代码在2.2内核中使用read_proc,不过这样收益并不大。4.2.2 ioctl 方法ioctl是作用于文件描述符之上的一个系统调用,我们会在下一章介绍它的用法;它接收一个“命令”号,用以标识将执行的命令;以及另一个(可选的)参数,通常是个指针。做为替代/proc文件系统的方法,可以为调试设计若干ioctl命令。这些命令从驱动程序复制相关数据到用户空间,在用户空间中可以查看这些数据。使用ioctl获取信息比起/proc来要困难一些,因为需要另一个程序调用ioctl并显示结

40、果。这个程序是必须编写并编译的,而且要和测试模块配合一致。从另一方面来说,相对实现/proc文件所需的工作,驱动程序的编码则更为容易些。有时ioctl是获取信息的最好方法,因为它比起读 /proc要快得多。如果在数据写到屏幕之前要完成 某些处理工作,以二进制获取数据要比读取文本文件有效得多。此外,ioctl并不要求把数据分割成不超过一个内存页面的片断。ioctl方法的一个优点是,在结束调试之后,用来取得信息的这些命令仍可以保留在驱动程序中。/proc文件对任何查看这个目录的人都是可见的(很多人可能会纳闷“这些奇怪的文件是用来做什么的”),然而与/proc文件不同,未公开的ioctl命令通常都不

41、会被注意到。此外,万一驱动程序有什么异常,这 些命令仍然可以用来调试。唯一的缺点就是模块会稍微大一些。4.3通过监视调试有时,通过监视用户空间中应用程序的运行情况,可以捕捉到一些小问题。监视程序同样也有助于确认驱动程序工作是否正常。例如,看到scull的read实现如何响应不同数据量的read请求后,我们就可以判断它是否工作正常。有许多方法可监视用户空间程序的工作情况。可以用调试器一步步跟踪它的函数,插入打印语句,或者在strace状态下运行程序。在检查内核代码时,最后一项技术最值得关注,我们将在此对它进行讨 论。strace命令是一个功能非常强大的工具,它可以显示程序所调用的所有系统调用。它

42、不仅可以显示调 用,而且还能显示调用参数,以及用符号方式表示的返回值。当系统调用失败时,错误的符号值(如ENOMEM )和对应的字符串(如 Out of memory )都能被显示出来。strace有许多命令行选项;最 为有用的是-t,用来显示调用发生的时间;-T,显示调用所花费的时间; -e,限定被跟踪的调用类型; -o,将输岀重定向到一个文件中。默认情况下,strace将跟踪信息打印到 stderr上。strace从内核中接收信息。这意味着一个程序无论是否按调试方式编译(用gcc的-g选项)或是被去掉了符号信息都可以被跟踪。与调试器可以连接到一个运行进程并控制它一样,strace也可以跟踪

43、一个正在运行的进程。跟踪信息通常用于生成错误报告,然后发给应用开发人员,但是它对内核编程人员来说也同样非常有用。我们已经看到驱动程序是如何通过响应系统调用得到执行的;strace允许我们检查每次调用中输入和输岀数据的一致性。例如,下面的屏幕信息显示了strace ls /dev > /dev/scullO 命令的最后几行:open("/dev", O_RDONLY|O_NONBLOCK)= 4fcntl(4, F_SETFD, FD_CLOEXEC)= 0brk(0x8055000)= 0x8055000lseek(4, 0, SEEK_CUR)= 0getdents

44、(4, /* 70 entries */, 3933)= 1260getdents(4, /* 0 entries */, 3933)= 0close(4)= 0fstat(1, st_mode=S_IFCHR|0664, st_rdev=makedev(253, 0), .) = 0ioctl(1, TCGETS, 0xbffffa5c)= -1 ENOTTY (Inappropriate ioctlfor device)write(1, "MAKEDEVnatibmnaudionaudio1na"., 4096) = 4000write(1, "d2nsdd3

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

46、退岀。另一个例子,让我们来对scull设备进行读操作(使用 WC命令):open("/dev/scullO", O_RDONLY)= 4fstat(4, st_mode=S_IFCHR|0664, st_rdev=makedev(253, 0), .) = 0read(4, "MAKEDEVnatibmnaudionaudio1na"., 16384) = 4000read(4, "d2nsdd3nsdd4nsdd5nsdd6nsdd7"., 16384) = 3421read(4, "", 16384)= 0fs

47、tat(1, st_mode=S_IFCHR|0600, st_rdev=makedev(3, 7), .) = 0ioctl(1, TCGETS, B38400 opost isig icanon echo .) = 0write(1, "7421 /dev/scull0n", 20)=20close(4)= 0_exit(0)= ?正如所料,read每次只能读取4000个字节,但数据总量与前面例子中写入的数量是相同的。与上面 的写跟踪相对比,请读者注意本例中重试工作是如何组织的。为了快速读取数据,wc已被优化了,因而它绕过了标准库,试图通过一次系统调用读取更多的数据。可

48、以从跟踪的read行中看到 wc每次均试图读取16KB数据。Linux行家可以在strace的输出中发现很多有用信息。如果觉得这些符号过于拖累的话,则可以仅限 于监视文件方法(open,read等)是如何工作的。就个人观点而言,我们发现strace对于查找系统调用运行时的细微错误最为有用。通常应用或演示程序中的perror调用在用于调试时信息还不够详细,而strace能够确切查明系统调用的哪个参数引发了错误,这一点对调试是大有帮助的。4.4调试系统故障即使采用了所有这些监视和调试技术,有时驱动程序中依然会有错误,这样的驱动程序在执行时就会产生系统故障。在岀现这种情况时,获取尽可能多的信息对解决

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

50、过,当内核行为异常时,会在控制台上打印岀提示信息。下一节将解释如何解码并使用这 些消息。尽管它们对于初学者来说相当晦涩,不过处理器在岀错时转储岀的这些数据包含了许多值得 关注的信息,通常足以查明程序错误,而无需额外的测试。4.4.1 oops 消息大部分错误都在于NULL指针的使用或其他不正确的指针值的使用上。这些错误通常会导致一个oops消息。由处理器使用的地址都是虚拟地址,而且通过一个复杂的称为页表(见第13章中的“页表” 一节)的结构映射为物理地址。当引用一个非法指针时,页面映射机制就不能将地址映射到物理地址,此时 处理器就会向操作系统发岀一个“页面失效”的信号。如果地址非法,内核就无法

51、“换页”到并不存 在的地址上;如果此时处理器处于超级用户模式,系统就会产生一个“oops ”。值得注意的是,2.0版本之后引入的第一个增强是,当向用户空间移动数据或者移岀时,无效地址错误会被自动处理。Linus选择了让硬件来捕捉错误的内存引用,所以正常情况(地址都正确时)就可以更 有效地得到处理。oops显示发生错误时处理器的状态,包括CPU寄存器的内容、页描述符表的位置,以及其它看上去无法理解的信息。这些消息由失效处理函数(arch/*/kernel/traps.c )中的printk语句产生,就象前面“printk” 一节所介绍的那样分发出来。让我们看看这样一个消息。当我们在一台运行2.4

52、内核的PC机上使用一个 NULL指针时,就会导致下面这些信息显示出来。这里最为相关的信息就是指令指针(EIP),即出错指令的地址。Unable to handle kernel NULL pointer dereference at virtual address 00000000printing eip:C48370C3*pde = 00000000Oops: 0002CPU:0EIP:0010:<c48370c3>EFLAGS: 00010286eax: ffffffea ebx: c2281a20 ecx: c48370c0 edx: c2281a40esi: 4000c00

53、0 edi: 4000c000 ebp: c38adf8c esp: c38adf8cds: 0018 es: 0018 ss: 0018Process Is (pid: 23171, stackpage=c38ad000)Stack: 0000010e c01356e6 c2281a20 4000c000 0000010e c2281a40 c38ac000 0000010e4000c000 bffffc1c 00000000 00000000 c38adfc4 c010b860 00000001 4000c0000000010e 0000010e 4000c000 bffffc1c 000

54、00004 0000002b 0000002b 00000004Call Trace: <c01356e6> <c010b860>Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00这个消息是通过对faulty模块的一个设备进行写操作而产生的,faulty这个模块专为演示出错而编写。faulty.c中write方法的实现很简单:ssize_t faulty_write (struct file *filp, const char *buf, size_t count, loff_t *pos)

55、/* make a simple fault by dereferencing a NULL pointer */*(int *)0 = 0;return 0;正如读者所见,我们这使用了一个NULL指针。因为0决不会是个合法的指针值,所以错误发生,内核进入上面的 oops消息状态。这个调用进程接着就被杀掉了。在read实现中,faulty模块还有更多有意思的错误状态。char faulty_buf1024;ssize_t faulty_read (struct file *filp, char *buf, size_t count,loff_t *pos)int ret, ret2;char

56、stack_buf4;printk(KERN_DEBUG "read: buf %p, count %lin", buf, (long)count);/* the next line oopses with 2.0, but not with 2.2 and later */ret = copy_to_user(buf, faulty_buf, count);if (!ret) return count; /* we survived */printk(KERN_DEBUG "didn't fail: retryn");/* For 2.2 an

57、d 2.4, let's try a buffer overflow */sprintf(stack_buf, "1234567n");if (count > 8) count = 8; /* copy 8 bytes to the user */ret2 = copy_to_user(buf, stack_buf, count);if (!ret2) return count;return ret2;这段程序首先从一个全局缓冲区读取数据,但并不检查数据的长度,然后通过对一个局部缓冲区进行写入操作,制造一次缓冲区溢岀。第一个操作仅在2.0内核会导致oops的发生,因为后期版本能自动地处理用户拷贝函数。缓冲区溢出则会在所有版本的内核中造成oops ;然而,由于return指令把指令指针带到了不知道的地方,所以这种错误很难跟踪,所能获得的仅是如下的信息:EIP:0010:<000

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论