Linux设备驱动程序(四)——调试技术1

简介: Linux设备驱动程序(四)——调试技术1

前言

由于内核是一个不与特定进程相关的功能集合,所以内核代码无法轻易地放在调试器中执行,而且也很难跟踪跟踪,本章节将介绍监视内核代码并跟踪错误的技术。


一、内核中的调试技术

我们列出用来开发的内核应当激活的配置选项,除了特别指出外,所有的这些选项都在内核配置工具的“kernel hacking” 菜单中。注意:并非所有体系架构都支持其中的某些选项

  • CONFIG_DEBUG_KERNEL
  • 这个选项只是使其他调试选项可用。它应当打开,但它本身不会打开所有的调试功能。
  • CONFIG_DEBUG_SLAB

这是一个非常重要的选项,它打开内核内存分配函数中的多个类型的检查,打开该检查后,就可以检测许多内存溢出及忘记初始化的错误,被分配内存的每一个字节在递交给调用者之前都设成 0xa5,而在释放后被设成 0x6b如果在自己驱动程序输出中,或者在 oops 信息中看到上述 "slab"字符,则可以轻松判断问题所在。 在打开该调试选项后,内核还会在每个已分配内存对象的前面和后面设置一些特殊的防护值;这样,当这些防护值发生变化时,

  • 内核就可以知道有些代码超出了内存的正常访问范围。
  • CONFIG_DEBUG_PAGEALLOC
  • 在释放时,全部内存页从内核地址空间中移出。该选项将大大降低运行速度,但可以快速定位特定的内存损坏的所在位置。
  • CONFIG_DEBUG_SPINLOCK
  • 打开该选项,内核将捕获对未初始化自旋锁的操作,也会捕获诸如两次解开同一锁的操作等其他错误
  • CONFIG_DEBUG_SPINLOCK_SLEEP
  • 该选项将检查拥有自旋锁时的休眠企图。实际上,如果调用可能引起休眠的函数这个选项也会生效,即使该函数可能不会导致真正的休眠。
  • CONFIG_INIT_DEBUG
  • 标记为 __init (或者 __initdata) 的符号将会在系统初始化或者模块装载之后被丢弃。该选项可用来检查初始化完成之后对用于初始化的内存空间的访向企图
  • CONFIG_DEBUG_INFO
  • 这个选项使得内核在建立时包含完整的调试信息,如果你想使用 gdb 调试内核,你将需要这些信息。如果你打算使用 gdb,你还要激活 CONFIG FRAME POINTER。
  • CONFIG_MAGIC_SYSRQ
  • 打开 “SysRq 魔法(magic SysRq)” 按键。我们将在本章后面的“系统挂起”一节中讲述该按键。
  • CONFIG_DEBUG_STACKOVERFLOW
  • CONFIG_DEBUG_STACK_USAGE
  • 这些选项可帮助跟踪内核栈的溢出问题。栈溢出的确切信号是不包含任何合理的反向跟踪信息的 oops 清单。第一个选项将在内核中增加明确的溢出检查;而第二个选项将让内核监视栈的使用,并通过 SysRq 按键输出一些统计信息
  • CONFIG_KALLSYMS
  • 该选项出现在“General setup/Standard features (一般设置/标准功能)”菜单中将在内核中包含符号信息;该选项默认是打开的。该符号信息用于调试上下文;没有此符号,oops 清单只能给出十六进制的内核反向跟踪信息,这通常没有多少用处。
  • CONFIG_IKCONFIG
  • CONFIG_IKCONFIG_PROC
  • 这些选项(在“Generl setup菜单)使得完整的内核配置状态被建立到内核中,可以通过 /proc 来使其可用,大部分内核开发者知道他们使用的哪个配置,并不需要这些选项(会使得内核更大)。但是如果你试着调试由其他人建立的内核中的问题它们可能有用
  • CONFIG_ACPI_DEBUG
  • 该选项出现在“Power management/ACPI(电源管理/ACPI)”菜单中。该选项将打开ACPI(Advanced Configuration and Power Interface,高级配置和电源接口)中的详细调试信息。如果怀疑自己所遇到的问题和ACPI相关,则可使用该选项。
  • CONFIG_DEBUG_DRIVER
  • 在“Device drivers(设备驱动程序)”菜单中。该选项打开驱动程序核心中的调试信息,它可以帮助跟踪底层支持代码中的问题。
  • CONFIG_SCSI_CONSTANTS
  • 该选项出现在“Device drivers/SCSI device support (设备驱动程序/SCSI设备支持)”菜单中,它将打开详细的 SCSI 错误消息。如果读者要编写 SCSI驱动程序则可使用该选项。

CONFIG_INPUT_EVBUG

  • 该选项可在“Device drivers/Input device support(设备驱动程序/输入设备支持)中找到,
  • 它会打开对输人事件的详细记录。如果读者要针对输入设备编写驱动程序,则可使用该选项。注意该选项会导致的安全问题:它会记录你键入的任何东西包括密码。
  • CONFIG_PROFILING
  • 这个选项位于"Profiling support"之下,剖析通常用在系统性能调整,但是在追踪一些内核挂起和相关问题上也有用

在我们讲解不同的内核问题跟踪方法时,将再次遇到上述选项。

二、通过打印调试

调试内核代码的时候,可以用printk 来完成相同的工作。

1、printk

相对于 printf,printk 的不同之处:差别之一就是,通过附加不同日志级别 (logevel),或者说消息优先级,可让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> 中定义了八种可用的日志级别字符串,下面以严重程度的降序来列出这些级别:

  • 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 中被指定为一个整数。在 2.6.10 内核中,DEFAULT_MESSAGE_LOGLEVEL 就是 KERN_WARNING

根据日志级别,内核可能会把消息打印到当前控制台上,这个控制台可以是一个字符模式的终端、一个串口打印机或是一个并口打印机。当优先级小于 console_loglevel 这个整数变量的值,消息才能显示出来,而且每次输出一行(如果不以newline字符结尾,则不会输出)。如果系统同时运行了 klogd 和 syslogd,则无论 console_loglevel 为何值,内核消息都将追加到 /var/log/messages 中(否则按照syslogd 的配置进行处理)如果 klogd 没有运行,这些消息就不会传递到用户空间,这种情况下,只能查看 /proc/kmsg 文件(使用 dmesg 命令可以轻松做到)如果使用 klogd,则应该了解它不会保存连续相同的信息行,它只会保存连续相同的第一行,并在最后打印这一行的重复次数。

变量 console_loglevel 的初始值是 DEFAULT_CONSOLE_LOGLEVEL,而且还可以通过sys_syslog 系统调用进行修改调用 klogd 时可以指定 -c 开关项来修改这个变量。注意,要修改其当前值,必须先杀掉 klogd,然后再用新的 -c 选项重新启动它此外,还可以编写程序来改变控制台的日志级别。新优先级被指定为一个1~8 之间的整数值。如果值被设为1,则只有级别为0(KERN_EMERG)的消息才能到达控制台;如果被设为8,则包括调试信息在内的所有消息都能显示出来。

我们也可以通过对文本文件 /procsys/kernel/printk 的访问来读取和修改控制台的日志级别。这个文件包含了4个整数值,分别是:当前的日志级别、未明确指定日志级别时的默认消息级别、最小允许的日志级别以及引导时的默认日志级别。向该文件中写人单个整数值,将会把当前日志级别修改为这个值。例如,可以简单地输入下面的命令使所有的内核消息显示到控制台上:

echo 8 > /proc/sys/kernel/printk

2、重定向控制台消息

对于控制台日志策略,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 文件中得到。

3、消息如何被记录

printk 函数将消息写到一个长度为 __LOG_BUP_LEN 字节的循环缓冲区中(我们可在配置内核时为__LOG_BUP_LEN 指定 4 KB-1MB 之间的值)。然后,该函数会唤醒任何正在等待消息的进程即那些睡眠在 syslog 系统调用上的进程或者正在读取 /proc/kmsg 的进程。这两个访问日志引擎的接口几乎是等价的,不过请注意,对 /proc/kmsg 进行读操作时,日志缓冲区中被读取的数据就不再保留,而 syslog 系统调用却能通过选项返回日志数据并保留这些数据,以便其他进程也能使用一般而言,读 /proc 文件要容易些,这也是 klogd 的默认方法。dmesg 命令可在不刷新缓冲区的情况下获得缓冲区的内容;实际上,该命令将缓冲区的整个内容返回到 sdout,而无论该缓冲区是否已经被读取。

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

如果循环缓冲区填满了,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 来满足自己的需求。

4、开启及关闭消息

下面给出了一个调用 printk 的编码方法,它可个别或全局地开关 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)

预处理条件语句(以及代码中的常量表达式)只在编译时执行,所以要再次打开或关闭消息就必须重新编译。另一种方法就是使用 C 条件语句,它在运行时执行,因此可以在程序运行期间打开或关闭消息这是个很好的功能,但每次代码执行时系统都要进行额外的处理其至在禁用消息后仍然会影响性能,而有时这种性能损失是无法接受的。

5、速度限制

有时会一不小心利用 printk 产生了上千条消息,从而让日志信息充满控制台,更可能使系统日志文件溢出。如果使用某个慢速控制台设备(比如串口),过高的消息输出速度会导致系统变慢,甚至使系统无法正常响应。

在许多情况下,最好的办法是设置一个标志,表示“我已经就此声明过了”,并在该标志被设置时不再打印任何信息。但在某些情况下,仍然有理由偶尔发出一条“该设备仍停止工作”这样的消息。内核为这种情况提供了一个有用的函数:

int printk_ratelimit(void);

在打印一条可能被重复的信息之前,应调用上面这个函数。如果该函数返回一个非零值则可以继续并打印我们的消息,否则就应该跳过。这样,典型的调用应如下所示:

if (printk_ratelimit())
  printk(KERN_NOTICE "The printer is still on fire\n");

printk ratelimit 通过跟踪发送到控制台的消息数量工作。如果输出的速度超过一个阈值,printk ratelimit 将返回零,从而避免发送重复消息。

我们可通过修改 /proc/sys/kernel/printk ratelimit(在重新打开消息之前应该等待的秒数)以及 /proc/sys/kernel/printk ratelimit burst(在进行速度限制之前可以接受的消息数)来定制 printk ratelimit 的行为

6、打印设备编号

有时当从一个驱动程序打印消息时,我们会希望打印与硬件关联的设备编号。内核提供了一对辅助宏(在 <linux/kdev t.h> 中定义):

int print_dev_t(char *buffer, dev_t dev);
char *format_dev_t(char *buffer, dev_t dev);

这两个宏均将设备编号打印到给定的缓冲区,其唯一的区别是 print_dev_t 返回的是打印的字符数,而format_dev_t 返回的是缓冲区,这样,它的返回值可直接作为调用 printk 时的参数使用不能忘记只有在结尾处存在newline(新行)字符时,printk才将消息刷新到控制台。传入上述宏的缓冲区必须足够保存一个设备编号。因为在未来的内核版本中,使用64位设备编号的可能性非常明显,因此,该缓冲区的大小应该至少有20字节长

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
29天前
|
运维 Oracle 容灾
Oracle dataguard 容灾技术实战(笔记),教你一种更清晰的Linux运维架构
Oracle dataguard 容灾技术实战(笔记),教你一种更清晰的Linux运维架构
|
8天前
|
Linux 芯片
Linux 驱动开发基础知识——查询方式的按键驱动程序_编写框架(十三)
Linux 驱动开发基础知识——查询方式的按键驱动程序_编写框架(十三)
9 2
|
9天前
|
NoSQL Linux C++
Linux C/C++ gdb调试正在运行的程序
Linux C/C++ gdb调试正在运行的程序
|
9天前
|
NoSQL Linux C++
Linux C/C++ gdb调试core文件
Linux C/C++ gdb调试core文件
|
9天前
|
NoSQL Linux C++
Linux C/C++ gdb调试
Linux C/C++ gdb调试
|
9天前
|
运维 NoSQL Ubuntu
深入理解Linux中的"crash"命令:内核崩溃的调试利器
`crash`是Linux内核崩溃调试工具,用于分析内核崩溃转储文件,提供GDB-like的交互式CLI。通过加载`vmcore`文件和内核映像,管理员可以查看系统状态、调用栈、内存布局等。安装`crash`可使用包管理器,如`apt-get`或`yum/dnf`。尽管有学习曲线且依赖转储文件,但`crash`在系统故障排查中极其重要。
|
9天前
|
负载均衡 Ubuntu Linux
Linux命令探秘:bond2team与网络绑定技术
Linux的`bond2team`是网络绑定和团队技术工具,用于组合多个网络接口以提升带宽、容错性和负载均衡。通过安装`ifenslave-2.6`,在`/etc/sysconfig/network-scripts/`或`/etc/network/interfaces`配置文件中设定接口绑定模式,如`activebackup`。它支持负载均衡、容错和热备等多种工作模式,确保网络高可用性和性能。在配置前务必备份,并重启服务使配置生效。
|
11天前
|
传感器 物联网 Linux
物联网设备的操作系统之争:Linux vs RTOS
【6月更文挑战第4天】在遥远的数码星球,物联网城中的Linux先生与RTOS小姐展开激烈角逐,分别在操作系统领域各显神通。Linux先生以其开源、兼容性强、功能丰富占据服务器、桌面及嵌入式设备市场,适合处理复杂任务和需要强大计算能力的设备。而RTOS小姐以实时性、高效响应和低资源占用见长,适用于资源有限、强调实时性的物联网设备。设备制造商在两者间抉择,引发物联网设备操作系统的选择大战。通过Python与FreeRTOS示例,展现了两者在智能家居和生产线控制等场景的应用。在物联网世界,Linux与RTOS共同推动设备智能化,为生活带来更多便捷。
62 3
|
29天前
|
运维 Linux
CentOS系统openssh-9,你会的还只有初级Linux运维工程师的技术吗
CentOS系统openssh-9,你会的还只有初级Linux运维工程师的技术吗
|
1月前
|
网络协议 Shell Linux
LabVIEW 在NI Linux实时设备上访问Shell
LabVIEW 在NI Linux实时设备上访问Shell
19 0