`

内核模块调试方法

阅读更多

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

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

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

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

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

printk(KERN_DEBUG "Here I am: %s:%i\n", _ _FILE_ _, _ _LINE_ _);
printk(KERN_CRIT "I'm trashed; giving up on %p\n", ptr);

在头文件 <linux/kernel.h> 中定义了 8 种可用的日志级别字符串。

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

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

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

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

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

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

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

KERN_DEBUG
用于调试信息。

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

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

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

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

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

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

# echo 8 > /proc/sys/kernel/printk

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

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

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

int main(int argc, char **argv)
{
  char bytes[2] = {11,0}; /* 11 is the TIOCLINUX cmd number */

  if (argc==2) bytes[1] = atoi(argv[1]); /* the chosen console */
  else {
      fprintf(stderr, "%s: need a single arg\n",argv[0]); exit(1);
  }
  if (ioctl(STDIN_FILENO, TIOCLINUX, bytes)<0) {    /* use stdin */
      fprintf(stderr,"%s: ioctl(stdin, TIOCLINUX): %s\n",
              argv[0], strerror(errno));
      exit(1);
  }
  exit(0);
}

setconsole 使用了特殊的ioctl命令:TIOCLINUX ,这个命令可以完成一些特定的 Linux 功能。使用 TIOCLINUX 时,需要传给它一个指向字节数组的指针参数。数组的第一个字节指定所请求子命令的数字,接下去的字节所具有的功能则由这个子命令决定。在 setconsole 中,使用的子命令是 11,后面那个字节(存于bytes[1]中)标识虚拟控制台。关于 TIOCLINUX 的详尽描述可以在内核源码中的 drivers/char/tty_io.c 文件得到。

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

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

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

klogd 运行时,会读取内核消息并将它们分发到 syslogd,syslogd 随后查看 /etc/syslog.conf ,找出处理这些数据的方法。syslogd 根据设施和优先级对消息进行区分;这两者的允许值均定义在 <sys/syslog.h> 中。内核消息由 LOG_KERN 设施记录,并以 printk 中使用的优先级记录(例如,printk 中使用的 KERN_ERR对应于syslogd 中的 LOG_ERR)。如果没有运行 klogd,数据将保留在循环缓冲区中,直到某个进程读取或缓冲区溢出为止。

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

注: 例如,使用下面的命令可设置 10 号终端用于消息的显示:
setlevel 8
setconsole 10

或者在一个未使用的 xterm 上执行cat /proc/kmesg来显示消息。

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

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

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

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

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

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

#undef PDEBUG             /* undef it, just in case */
#ifdef SCULL_DEBUG
#  ifdef _ _KERNEL_ _
   /* This one if debugging is on, and kernel space */
#    define PDEBUG(fmt, args...) printk( KERN_DEBUG "scull: " 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,它能根据代码所运行的环境选择合适的方式显示信息:内核态运行时使用printk系统调用;用户态下则使用 libc调用fprintf,向标准错误设备进行输出。符号PDEBUGG则什么也不做;它可以用来将打印语句注释掉,而不必把它们完全删除。

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

# Comment/uncomment the following line to disable/enable debugging
DEBUG = y

# Add your debugging flag (or not) to CFLAGS
ifeq ($(DEBUG),y)
DEBFLAGS = -O -g -DSCULL_DEBUG # "-O" is needed to expand inlines
else
DEBFLAGS = -O2
endif

CFLAGS += $(DEBFLAGS)

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

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

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

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

4.2  通过查询调试
上一节讲述了 printk 是如何工作的以及如何使用它,但还没谈到它的缺点。

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

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


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

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

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

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

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

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

所有使用 /proc 的模块必须包含 <linux/proc_fs.h>,通过这个头文件定义正确的函数。

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

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

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

int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data);

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

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

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

int (*get_info)(char *page, char **start, off_t offset, int count);  

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”,可以在 /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 && 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 %li\n",
                     i, d->qset, d->quantum, d->size);
      for (; d && len <= limit; d = d->next) { /* scan the list */
          len += sprintf(buf+len, "  item at %p, qset at %p\n", d,
                                  d->data);
          if (d->data && !d->next) /* dump only the last item
                                                  - save space */
              for (j = 0; j < d->qset; j++) {
                  if (d->data[j])
                      len += sprintf(buf+len,"    % 4i: %8p\n",
                                                  j,d->data[j]);
              }
      }
      up(&scull_devices[ i].sem);
  }
  *eof = 1;
  return len;
}

这是一个相当典型的 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 功能。

create_proc_read_entry("scullmem",
                     0    /* default mode */,
                     NULL /* parent dir */,
                     scull_read_procmem,
                     NULL /* client data */);

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

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

当然,在模块卸载时,/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内核的头文件下进行编译时,考虑下面 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:   scull_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.low_ino);
}

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

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

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

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

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

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

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

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

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

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

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

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

例如,下面的屏幕信息显示了 strace ls /dev > /dev/scull0 命令的最后几行:

[...]
open("/dev", O_RDONLY|O_NONBLOCK)     = 4
fcntl(4, F_SETFD, FD_CLOEXEC)         = 0
brk(0x8055000)                        = 0x8055000
lseek(4, 0, SEEK_CUR)                 = 0
getdents(4, /* 70 entries */, 3933)   = 1260
[...]
getdents(4, /* 0 entries */, 3933)    = 0
close(4)                              = 0
fstat(1, {st_mode=S_IFCHR|0664, st_rdev=makedev(253, 0), ...}) = 0
ioctl(1, TCGETS, 0xbffffa5c)          = -1 ENOTTY (Inappropriate ioctl
                                                   for device)
write(1, "MAKEDEV\natibm\naudio\naudio1\na"..., 4096) = 4000
write(1, "d2\nsdd3\nsdd4\nsdd5\nsdd6\nsdd7"..., 96) = 96
write(1, "4\nsde5\nsde6\nsde7\nsde8\nsde9\n"..., 3325) = 3325
close(1)                              = 0
_exit(0)                              = ?

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

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

[...]
open("/dev/scull0", O_RDONLY)           = 4
fstat(4, {st_mode=S_IFCHR|0664, st_rdev=makedev(253, 0), ...}) = 0
read(4, "MAKEDEV\natibm\naudio\naudio1\na"..., 16384) = 4000
read(4, "d2\nsdd3\nsdd4\nsdd5\nsdd6\nsdd7"..., 16384) = 3421
read(4, "", 16384)                      = 0
fstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(3, 7), ...}) = 0
ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0
write(1, "   7421 /dev/scull0\n", 20)   = 20
close(4)                                = 0
_exit(0)                                = ?

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

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

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

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

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

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

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

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

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

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

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

Unable to handle kernel NULL pointer dereference at virtual address 00000000
printing eip:

c48370c3
*pde = 00000000
Oops: 0002
CPU:    0
EIP:    0010:[<c48370c3>]
EFLAGS: 00010286
eax: ffffffea   ebx: c2281a20   ecx: c48370c0   edx: c2281a40
esi: 4000c000   edi: 4000c000   ebp: c38adf8c   esp: c38adf8c
ds: 0018   es: 0018   ss: 0018
Process ls (pid: 23171, stackpage=c38ad000)
Stack: 0000010e c01356e6 c2281a20 4000c000 0000010e c2281a40 c38ac000 \
          0000010e
     4000c000 bffffc1c 00000000 00000000 c38adfc4 c010b860 00000001 \
          4000c000
     0000010e 0000010e 4000c000 bffffc1c 00000004 0000002b 0000002b \
          00000004
Call 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)
{
  /* make a simple fault by dereferencing a NULL pointer */
  *(int *)0 = 0;
  return 0;
}

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

char faulty_buf[1024];

ssize_t faulty_read (struct file *filp, char *buf, size_t count,
                   loff_t *pos)
{
  int ret, ret2;
  char stack_buf[4];

  printk(KERN_DEBUG "read: buf %p, count %li\n", 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: retry\n");
  /* For 2.2 and 2.4, let's try a buffer overflow  */
  sprintf(stack_buf, "1234567\n");
  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:[<00000000>]
[...]
Call Trace: [<c010b860>]
Code:  Bad EIP value.

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

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

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

Unable to handle kernel NULL pointer dereference at virtual address \
   00000000
printing eip:
c48370c3
*pde = 00000000
Oops: 0002
CPU:    0
EIP:    0010:[faulty:faulty_write+3/576]
EFLAGS: 00010286
eax: ffffffea   ebx: c2c55ae0   ecx: c48370c0   edx: c2c55b00
esi: 0804d038   edi: 0804d038   ebp: c2337f8c   esp: c2337f8c
ds: 0018   es: 0018   ss: 0018
Process cat (pid: 23413, stackpage=c2337000)
Stack: 00000001 c01356e6 c2c55ae0 0804d038 00000001 c2c55b00 c2336000 \
          00000001
     0804d038 bffffbd4 00000000 00000000 bffffbd4 c010b860 00000001 \
          0804d038
     00000001 00000001 0804d038 bffffbd4 00000004 0000002b 0000002b \
          00000004
Call Trace: [sys_write+214/256] [system_call+52/56]  
Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00  

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 就会拒绝解析符号。假如一个符号被解析在系统日志中,那么就有理由确信它已被正确解析了。

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

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

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

System.map 文件这个映射文件必须与 oops 发生时正在运行的内核相一致。默认为 /usr/src/linux/System.map。
模块列表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; c48370c3 <[faulty]faulty_write+3/20>   <=====
Trace; c01356e6 <sys_write+d6/100>
Trace; c010b860 <system_call+34/38>
Code;  c48370c3 <[faulty]faulty_write+3/20>
00000000 <_EIP>:
Code;  c48370c3 <[faulty]faulty_write+3/20>   <=====
 0:   c7 05 00 00 00    movl   $0x0,0x0   <=====
Code;  c48370c8 <[faulty]faulty_write+8/20>
 5:   00 00 00 00 00
Code;  c48370cd <[faulty]faulty_write+d/20>
 a:   31 c0             xorl   %eax,%eax
Code;  c48370cf <[faulty]faulty_write+f/20>
 c:   89 ec             movl   %ebp,%esp
Code;  c48370d1 <[faulty]faulty_write+11/20>
 e:   5d                popl   %ebp
Code;  c48370d2 <[faulty]faulty_write+12/20>
 f:   c3                ret    
Code;  c48370d3 <[faulty]faulty_write+13/20>
10:   8d b6 00 00 00    leal   0x0(%esi),%esi
Code;  c48370d8 <[faulty]faulty_write+18/20>
15:   00

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

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

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

Unable to handle kernel NULL pointer dereference
tsk->mm->context = 0000000000000734
tsk->mm->pgd = fffff80003499000
            \/ ____ \/
            "@'/ .. \`@"
           /_| \_ _/ |_\
              \_ _ _/
ls(16740): Oops
TSTATE: 0000004400009601 TPC: 0000000001000128 TNPC: 0000000000457fbc \
Y: 00800000
g0: 000000007002ea88 g1: 0000000000000004 g2: 0000000070029fb0 \
g3: 0000000000000018
g4: fffff80000000000 g5: 0000000000000001 g6: fffff8000119c000 \
g7: 0000000000000001
o0: 0000000000000000 o1: 000000007001a000 o2: 0000000000000178 \
o3: fffff8001224f168
o4: 0000000001000120 o5: 0000000000000000 sp: fffff8000119f621 \
ret_pc: 0000000000457fb4
l0: fffff800122376c0 l1: ffffffffffffffea l2: 000000000002c400 \
l3: 000000000002c400
l4: 0000000000000000 l5: 0000000000000000 l6: 0000000000019c00 \
l7: 0000000070028cbc
i0: fffff8001224f140 i1: 000000007001a000 i2: 0000000000000178 \
i3: 000000000002c400
i4: 000000000002c400 i5: 000000000002c000 i6: fffff8000119f6e1 \
i7: 0000000000410114
Caller[0000000000410114]
Caller[000000007007cba4]
Instruction DUMP: 01000000 90102000 81c3e008 <c0202000> \
30680005 01000000 01000000 01000000 01000000

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

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

>>TPC; 0000000001000128 <[faulty].text.start+88/a0>   <=====
>>O7;  0000000000457fb4 <sys_write+114/160>
>>I7;  0000000000410114 <linux_sparc_syscall+34/40>
Trace; 0000000000410114 <linux_sparc_syscall+34/40>
Trace; 000000007007cba4 <END_OF_CODE+6f07c40d/????>
Code;  000000000100011c <[faulty].text.start+7c/a0>
0000000000000000 <_TPC>:
Code;  000000000100011c <[faulty].text.start+7c/a0>
 0:   01 00 00 00       nop
Code;  0000000001000120 <[faulty].text.start+80/a0>
 4:   90 10 20 00       clr  %o0     ! 0 <_TPC>
Code;  0000000001000124 <[faulty].text.start+84/a0>
 8:   81 c3 e0 08       retl
Code;  0000000001000128 <[faulty].text.start+88/a0>   <=====
 c:   c0 20 20 00       clr  [ %g0 ]   <=====
Code;  000000000100012c <[faulty].text.start+8c/a0>
10:   30 68 00 05       b,a   %xcc, 24 <_TPC+0x24> \
                      0000000001000140 <[faulty]faulty_write+0/20>
Code;  0000000001000130 <[faulty].text.start+90/a0>
14:   01 00 00 00       nop
Code;  0000000001000134 <[faulty].text.start+94/a0>
18:   01 00 00 00       nop
Code;  0000000001000138 <[faulty].text.start+98/a0>
1c:   01 00 00 00       nop
Code;  000000000100013c <[faulty].text.start+9c/a0>
20:   01 00 00 00       nop

要打印出上面显示的反汇编代码,我们就必须告知 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 sparc:v9)。

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

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

4.4.2  系统挂起
尽 管内核代码中的大多数错误仅会导致一个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 组合键。连同这两个键一起按下的第三个键,会执行许多有用动作中的其中一种,这些动作如下:

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

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

s
对所有磁盘进行紧急同步。

colo

分享到:
评论

相关推荐

    编译Linux内核及调试内核模块

    编译Linux内核及调试内核模块 菜鸟必备! 欢迎下载!

    linux内核模块加载顺序

    你是否曾经疑问过我们编写的内核模块是什么时候,如何加载到内核的,本文将为你揭开迷惑。

    linux内核调试方法总结

    三 内核调试配置选项 1 内核配置 2 调试原子操作 四 引发bug并打印信息 1 BUG()和BUG_ON() 2 dump_stack() 五 printk() 1 printk函数的健壮性 2 printk函数脆弱之处 3 LOG等级 4 记录缓冲区 5 syslogd/klogd 6 dmesg...

    使用cmake创建linux内核模块,多种arch适用(可在vscode中使用)

    按照cmake的编写代码习惯即可,cmake会自动生成对应...1.在json文件中根据需求调整设置; 2.设置后,在vscode左下角选择工具链类型; 3.点击cmake的build工具图标,生成.ko文件; 4.清理l临时文件,在源码目录执行make clean;

    使用kgdb调试linux内核及内核模块

    使用kgdb调试linux内核及内核模块

    基于内核模块的包过滤防火墙源码+系统说明文档+数据.zip

    【资源说明】 1、该资源包括项目的全部源码,下载可以直接使用! 2、本项目适合作为计算机、数学、电子信息等专业的课程设计、期末大作业和毕设项目,作为参考...基于内核模块的包过滤防火墙源码+系统说明文档+数据.zip

    内核模块编程与调试介绍

    内核模块编程与调试 主要是调试部分,可以借鉴 大家看看

    Windows x64内核模块-HelloWorld

    Windows内核安全与驱动开发书的第一个示例程序,在新版本开发环境中调试通过,使用Visual Studio 2017加上WDK 1803环境编译。在调试过程中,由于x64驱动程序禁止使用嵌入汇编,采用Shellcode方式进行软件中断调用,...

    信息安全科技创新课程大作业-基于内核模块的包过滤防火墙c源码+项目说明.zip

    信息安全科技创新课程大作业-基于内核模块的包过滤防火墙c源码+项目说明.zip # VersaGuard-Firewall NIS3302信息安全科技创新课程大作业——基于内核模块的包过滤防火墙 ## Usage 内核模块、CLI、GUI都在`bin`目录...

    Linux实验课设报告

    6、Linux内核模块编程:内核模块编程、卸载模块编程、参数模块编程 8、Linux内存管理:编写模块程序、编译、插入模块、查看打印信息 8、Linux设备驱动:编写一个简单的字符设备驱动、查看设备号、编写测试程序 使用...

    Linux内核模块管理

     内核模块路径  查看已加载的内核  加载与卸载内核模块  修改内核参数  Linux内核采用的是模块化技术,这样的设计使得系统内核可以保持小化,同时确保了内核的可扩展性与可维护性,模块化设计允许我们...

    Cortex-M3内核的Fault调试模块设计-V1.doc

    本文经过对Cortex-M3内核的异常处理模型的分析,设计了简单的Fault调试模块。将此调试模块添加到开发者的工程之中,当发生Fault时,调试模块可以通过UART输出Fault的具体信息以及发生Fault之前处理器的工作环境。...

    Cortex-M3内核的Fault调试模块设计-带代码

    本文经过对Cortex-M3内核的异常处理模型的分析,设计了简单的Fault调试模块。将此调试模块添加到开发者的工程之中,当发生Fault时,调试模块可以通过UART输出Fault的具体信息以及发生Fault之前处理器的工作环境。...

    cpp-Androidloadble内核模块它主要用于在受控系统仿真器上进行反转和调试

    Android loadble内核模块 - 它主要用于在受控系统/仿真器上进行反转和调试。

    使用KGDB调试内核模块

    KGDB can provide the source level debug for your kernel module, this is doc is a step by step example to setup KGDB.

    linux内核调试分析指南

    linux内核调试分析指南 linux内核调试分析指南--上篇 本文档已经转到下面的网址,位于zh-kernel.org的文档停止更新,请访问新网址 一些前言 作者前言 知识从哪里来 为什么撰写本文档 为什么需要汇编级调试 ***第一...

    用BDI2000调试Linux内核和模块

    用BDI2000调试Linux内核和模块 BDI2000是性价比较高的JTAG调试器,通过装载不同的firmware就可以支持ARM、MIPS、XSCALE等多种嵌入式处理器。我所用的是 mips版本的bdiGDB,也就是能够仿真成为gdbserver,配合gdb进行...

    嵌入式linux学习资料之使用UML调试Linux内核和模块--千锋培训

    文档介绍了概述,一、构建UML内核调试环境,1.构建UML内核树,1)下载一份内核源码包,2)解压之后cd到源码根目录下,3)配置内核并编译,4)编译内核模块,2.准备运行UML的根文件系统和交换文件系统

    Linux内核按需动态装载和卸掉模块

    单核结构在添加新模块时,一种方法是重新调整设置,所以非常费时。可以使用 insmod 和 rmmod 命令来装载和卸掉 Linux 模块, 内核自己也可以调用内核驻留程序(Kerneld) 来按需要装载和卸掉模块。

Global site tag (gtag.js) - Google Analytics