精通 Linux 设备驱动开发(七)

原文:zh.annas-archive.org/md5/95A00CE7D8C2703D7FF8A1341D391E8B

译者:飞龙

协议:CC BY-NC-SA 4.0

第十四章:Linux 内核调试技巧和最佳实践

大多数情况下,在开发过程中,编写代码并不是最困难的部分。困难之处在于 Linux 内核是操作系统最底层的独立软件。这使得调试 Linux 内核变得具有挑战性。然而,这得到了补偿,因为大多数情况下,我们不需要额外的工具来调试内核代码,因为大多数内核调试工具都是内核本身的一部分。我们将首先熟悉 Linux 内核发布模型,然后学习 Linux 内核发布流程和步骤。接下来,我们将关注与 Linux 内核调试相关的开发技巧(特别是通过打印进行调试),最后,我们将专注于跟踪 Linux 内核,以离线调试和学习如何利用内核 oops 结束。

本章将涵盖以下主题:

  • 了解 Linux 内核发布流程

  • Linux 内核开发技巧

  • Linux 内核跟踪和性能分析

  • Linux 内核调试技巧

技术要求

本章的先决条件如下:

了解 Linux 内核发布流程

根据 Linux 内核发布模型,始终存在三种类型的活动内核发布:主线、稳定发布和长期支持(LTS)发布。首先,子系统维护者收集和准备 bug 修复和新功能,然后提交给 Linus Torvalds,以便他将它们包含在自己的 Linux 树中,这被称为主线 Linux 树,也被称为主 Git 存储库。这是每个稳定发布的起源。

在每个新内核版本发布之前,它都会通过发布候选标签提交给社区,以便开发人员可以测试和完善所有新功能,最重要的是分享反馈。在这个周期内,Linus 将依赖反馈来决定最终版本是否准备好发布。当他确信新内核准备就绪时,他会进行(实际上是标记)最终发布,我们称这个发布为稳定,以表示它不再是发布候选:这些发布是vX.Y版本。

没有制定发布的严格时间表。然而,新的主线内核通常每 2-3 个月发布一次。稳定内核发布基于 Linus 的发布,即主线树发布。

一旦 Linus 发布了主线内核,它也会出现在 linux-stable 树中(位于git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/),在那里它成为一个分支,并且可以接收稳定发布的 bug 修复。Greg Kroah-Hartman 负责维护这个树,它也被称为稳定树,因为它用于跟踪先前发布的稳定内核。也就是说,为了将修复应用到这个树中,这个修复必须首先被合并到 Linus 的树中。因为修复必须先前进再返回,所以说这个修复是被反向移植的。一旦在主线存储库中修复了 bug,它就可以应用到仍由内核开发社区维护的先前发布的内核中。所有反向移植到稳定发布的修复必须符合一组强制接受标准,其中之一是它们必须已经存在于 Linus 的树中。

重要提示

修复 bug 的内核发布被认为是稳定的。

例如,4.9内核由 Linus 发布,然后基于此内核的稳定内核发布被编号为4.9.14.9.24.9.3等。这些发布被称为错误修复内核发布,通常在稳定内核发布树中引用它们的分支时,序列通常缩短为数字4.9.y。每个稳定内核发布树由单个内核开发人员维护,负责挑选发布所需的补丁,并执行审查/发布流程。通常只有几个错误修复内核发布,直到下一个主线内核可用,除非它被指定为长期维护内核

每个子系统和内核维护者存储库都托管在这里:git.kernel.org/pub/scm/linux/kernel/git/。在那里,我们也可以找到 Linus 或稳定树。在 Linus 树中(git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/),Linus 的树中只有一个分支,即主分支。其中的标签要么是稳定发布,要么是发布候选版本。在稳定树中(git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/),每个稳定内核发布都有一个分支(命名为*<A.B>.y*,其中*<A.B>*是 Linus 树中的发布版本),每个分支都包含其错误修复内核发布。

重要提示

有一些链接可以随时保留,以便跟踪 Linux 内核发布。第一个是www.kernel.org/,您可以从这里下载内核存档,然后是www.kernel.org/category/releases.html,您可以访问最新的 LTS 内核发布及其支持时间表。您还可以参考此链接patchwork.kernel.org/,从这里您可以按子系统基础跟踪内核补丁提交。

现在我们熟悉了 Linux 内核发布模型,我们可以深入一些开发提示和最佳实践,这有助于巩固和利用其他内核开发人员的经验。

Linux 内核开发提示

最佳的 Linux 内核开发实践是受现有内核代码的启发。这样,您肯定可以学到良好的实践。也就是说,我们不会重复造轮子。我们将专注于本章所需的内容,即调试。最常用的调试方法涉及日志记录和打印。为了利用这种经过时间考验的调试技术,Linux 内核提供了适当的日志记录 API,并公开了内核消息缓冲区以存储日志。虽然这似乎很明显,我们将专注于内核日志记录 API,并学习如何管理消息缓冲区,无论是从内核代码内部还是从用户空间。

消息打印

消息打印和记录是开发的固有部分,无论我们是在内核空间还是用户空间。在内核中,printk()函数长期以来一直是事实上的内核消息打印函数。它类似于 C 库中的printf(),但具有日志级别的概念。

如果您查看实际驱动程序代码的示例,您会注意到它的用法如下:

printk(<LOG_LEVEL> "printf like formatted message\n");

在这里,<LOG_LEVEL>include/linux/kern_levels.h中定义的八个不同日志级别之一,并指定错误消息的严重程度。您还应该注意,日志级别和格式字符串之间没有逗号(因为预处理器连接了这两个字符串)。

内核日志级别

Linux 内核使用级别的概念来确定消息的严重程度。共有八个级别,每个级别都定义为一个字符串,它们的描述如下:

  • KERN_EMERG,定义为"0"。它用于紧急消息,意味着系统即将崩溃或不稳定(无法使用)。

  • KERN_ALERT,定义为"1",意味着发生了严重的事情,必须立即采取行动。

  • KERN_CRIT,定义为"2",意味着发生了严重的条件,例如严重的硬件/软件故障。

  • KERN_ERR,定义为"3",在错误情况下使用,通常由驱动程序用于指示与硬件的困难或与子系统的交互失败。

  • KERN_WARNING,定义为"4",用作警告,本身并不严重,但可能表示问题。

  • KERN_NOTICE,定义为"5",意味着没有严重问题,但仍然值得注意。这经常用于报告安全事件。

  • KERN_INFO,定义为"6",用于信息消息,例如驱动程序初始化时的启动信息。

  • KERN_DEBUG,定义为"7",用于调试目的,仅在启用DEBUG内核选项时才有效。否则,它的内容将被简单地忽略。

如果您在消息中未指定日志级别,则默认为DEFAULT_MESSAGE_LOGLEVEL(通常为"4" = KERN_WARNING),可以通过CONFIG_DEFAULT_MESSAGE_LOGLEVEL内核配置选项进行设置。

因此,对于新的驱动程序,建议您使用更方便的打印 API,这些 API 在其名称中嵌入了日志级别。这些打印助手是pr_emergpr_alertpr_critpr_errpr_warningpr_warnpr_noticepr_infopr_debugpr_dbg。除了比等效的printk()调用更简洁之外,它们还可以通过pr_fmt()宏使用格式字符串的通用定义;例如,在源文件的顶部(在任何#include指令之前)定义这个:

#define pr_fmt(fmt) "%s:%s: " fmt, KBUILD_MODNAME, __func__

这将在该文件中的每个pr_*()消息前缀模块和生成消息的函数名称。如果内核是使用DEBUG编译的,则pr_develpr_debug将被替换为printk(KERN_DEBUG …),否则它们将被替换为空语句。

pr_*()系列宏应该在核心代码中使用。对于设备驱动程序,您应该使用与设备相关的辅助程序,这些辅助程序还接受相关设备结构作为参数。它们还以标准形式打印相关设备的名称,确保始终可以将消息与生成它的设备关联起来:

dev_emerg(const struct device *dev, const char *fmt, ...);
dev_alert(const struct device *dev, const char *fmt, ...);
dev_crit(const struct device *dev, const char *fmt, ...);
dev_err(const struct device *dev, const char *fmt, ...);
dev_warn(const struct device *dev, const char *fmt, ...);
dev_notice(const struct device *dev, const char *fmt, ...);
dev_info(const struct device *dev, const char *fmt, ...);
dev_dbg(const struct device *dev, const char *fmt, ...);

虽然日志级别的概念被内核用来确定消息的重要性,但它也用于决定是否应立即将此消息呈现给用户,通过将其打印到当前控制台(其中控制台也可以是串行线或甚至打印机,而不是xterm)。

为了决定,内核将消息的日志级别与console_loglevel内核变量进行比较,如果消息的日志级别重要性更高(即较低的值)比console_loglevel,则消息将被打印到当前控制台。由于默认内核日志级别通常为"4",这就是为什么您在控制台上看不到pr_info()pr_notice()甚至pr_warn()消息的原因,因为它们具有更高或相等的值(这意味着优先级更低)比默认值。

要确定系统上的当前console_loglevel,您只需输入以下内容:

$ cat /proc/sys/kernel/printk
4    4    1    7

第一个整数(4)是当前控制台日志级别,第二个数字(4)是默认值,第三个数字(1)是可以设置的最小控制台日志级别,第四个数字(7)是启动时的默认控制台日志级别。

要更改当前的console_loglevel,只需写入同一文件,即/proc/sys/kernel/printk。因此,为了将所有消息打印到控制台,执行以下简单命令:

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

每条内核消息都会出现在您的控制台上。然后您将看到以下内容:

# cat /proc/sys/kernel/printk
8    4    1    7

改变控制台日志级别的另一种方法是使用带有-n参数的dmesg

# dmesg -n 5

通过上述命令,console_loglevel被设置为打印KERN_WARNING4)或更严重的消息。您还可以在引导时使用loglevel引导参数指定console_loglevel(有关更多详细信息,请参阅Documentation/kernel-parameters.txt)。

重要说明

还有KERN_CONTpr_cont,它们有点特殊,因为它们不指定紧急级别,而是指示继续的消息。它们只应该在早期引导期间由核心/架构代码使用(否则,继续的行不是 SMP 安全的)。当要打印的消息行的一部分取决于计算结果时,这可能是有用的,如下面的示例所示:

[…]
pr_warn("your last operation was ");
if (success)
   pr_cont("successful\n");
else
   pr_cont("NOT successful\n");

您应该记住,只有最终的打印语句才有尾随的\n字符。

内核日志缓冲区

无论它们是否立即在控制台上打印,每个内核消息都会被记录在一个缓冲区中。这个内核消息缓冲区是一个固定大小的循环缓冲区,这意味着如果缓冲区填满,它会环绕并且您可能会丢失一条消息。因此,增加缓冲区大小可能会有所帮助。为了改变内核消息缓冲区大小,您可以调整LOG_BUF_SHIFT选项,其值用于左移 1 以获得最终大小,即内核日志缓冲区大小(例如,16 => 1<<16 => 64KB17 => 1 << 17 => 128KB)。也就是说,这是一个在编译时定义的静态大小。这个大小也可以通过内核引导参数定义,通过使用log_buf_len参数,换句话说,log_buf_len=1M(只接受 2 的幂值)。

添加时间信息

有时,向打印的消息添加时间信息是有用的,这样您就可以看到特定事件发生的时间。内核包括一个用于执行此操作的功能,称为printk times,通过CONFIG_PRINTK_TIME选项启用。在配置内核时,此选项可以在内核调试菜单中找到。一旦启用,此时间信息将作为每条日志消息的前缀。

$ dmesg
[…]
[    1.260037] loop: module loaded
[    1.260194] libphy: Fixed MDIO Bus: probed
[    1.260195] tun: Universal TUN/TAP device driver, 1.6
[    1.260224] PPP generic driver version 2.4.2
[    1.260260] ehci_hcd: USB 2.0 'Enhanced' Host Controller (EHCI) Driver
[    1.260262] ehci-pci: EHCI PCI platform driver
[    1.260775] ehci-pci 0000:00:1a.7: EHCI Host Controller
[    1.260780] ehci-pci 0000:00:1a.7: new USB bus registered, assigned bus number 1
[    1.260790] ehci-pci 0000:00:1a.7: debug port 1
[    1.264680] ehci-pci 0000:00:1a.7: cache line size of 64 is not supported
[    1.264695] ehci-pci 0000:00:1a.7: irq 22, io mem 0xf7ffa000
[    1.280103] ehci-pci 0000:00:1a.7: USB 2.0 started, EHCI 1.00
[    1.280146] usb usb1: New USB device found, idVendor=1d6b, idProduct=0002
[    1.280147] usb usb1: New USB device strings: Mfr=3, Product=2, SerialNumber=1
[…]

内核消息输出中插入的时间戳由秒和微秒(实际上是秒.微秒)组成,作为从机器操作开始(或从内核时间跟踪开始)的绝对值,对应于引导加载程序将控制权传递给内核时的时间(当您在控制台上看到类似[ 0.000000] Booting Linux on physical CPU 0x0的内容)。

可以通过写入/sys/module/printk/parameters/time来在运行时控制printk时间戳,以启用和禁用printk时间戳。以下是示例:

# echo 1 >/sys/module/printk/parameters/time
# cat /sys/module/printk/parameters/time
N
# echo 1 >/sys/module/printk/parameters/time
# cat /sys/module/printk/parameters/time
Y

它不控制时间戳是否被记录。它只控制在内核消息缓冲区被转储时、在启动时或在使用dmesg时是否打印。这可能是启动时优化的一个领域。如果禁用,日志打印所需的时间将更少。

我们现在熟悉了内核打印 API 及其日志缓冲区。我们已经看到了如何调整消息缓冲区,并根据需求添加或删除信息。这些技能可以用于通过打印进行调试。然而,Linux 内核中还提供了其他调试和跟踪工具,下一节将介绍其中一些。

Linux 内核跟踪和性能分析

尽管通过打印进行调试可以满足大部分调试需求,但有时我们需要在运行时监视 Linux 内核以跟踪奇怪的行为,包括延迟、CPU 占用、调度问题等。在 Linux 世界中,实现这一目标最有用的工具是内核本身的一部分。最重要的是ftrace,它是 Linux 内核内部跟踪工具,也是本节的主要内容。

使用 Ftrace 来对代码进行仪器化

功能跟踪,简称 Ftrace,不仅仅是其名称所说的那样。例如,它可以用来测量处理中断所需的时间,跟踪耗时的函数,计算激活高优先级任务所需的时间,跟踪上下文切换等等。

Steven Rostedt开发,Ftrace 自 2008 年 2.6.27 版内核开始已经包含在内核中。这是一个提供用于记录数据的调试环形缓冲区的框架。这些数据是由内核集成的跟踪程序收集的。Ftrace 在debugfs文件系统之上工作,并且在大多数情况下,在启用时会被挂载在自己的名为tracing的目录中。在大多数现代 Linux 发行版中,默认情况下会在/sys/kernel/debug/目录中挂载(这仅对 root 用户可用),这意味着您可以在/sys/kernel/debug/tracing/中利用 Ftrace。

以下是要在系统上启用以支持 Ftrace 的内核选项:

CONFIG_FUNCTION_TRACER
CONFIG_FUNCTION_GRAPH_TRACER
CONFIG_STACK_TRACER
CONFIG_DYNAMIC_FTRACE

前面的选项取决于支持跟踪功能的架构,需要启用CONFIG_HAVE_FUNCTION_TRACERCONFIG_HAVE_DYNAMIC_FTRACECONFIG_HAVE_FUNCTION_GRAPH_TRACER选项。

要挂载tracefs目录,您可以将以下行添加到您的/etc/fstab文件中:

tracefs   /sys/kernel/debug/tracing   tracefs defaults   0   0

或者您可以使用以下命令在运行时挂载它:

mount -t tracefs nodev /sys/kernel/debug/tracing

目录的内容应该如下所示:

# ls /sys/kernel/debug/tracing/
README                      set_event_pid
available_events            set_ftrace_filter
available_filter_functions  set_ftrace_notrace
available_tracers           set_ftrace_pid
buffer_size_kb              set_graph_function
buffer_total_size_kb        set_graph_notrace
current_tracer              snapshot
dyn_ftrace_total_info       stack_max_size
enabled_functions           stack_trace
events                      stack_trace_filter
free_buffer                 trace
function_profile_enabled    trace_clock
instances                   trace_marker
max_graph_depth             trace_options
options                     trace_pipe
per_cpu                     trace_stat
printk_formats              tracing_cpumask
saved_cmdlines              tracing_max_latency
saved_cmdlines_size         tracing_on
set_event                   tracing_thresh

我们不会描述所有这些文件和子目录,因为官方文档已经涵盖了这些内容。相反,我们只会简要描述与我们上下文相关的文件:

  • available_tracers:可用的跟踪程序。

  • tracing_cpumask:允许对选定的 CPU 进行跟踪。掩码应该以十六进制字符串格式指定。例如,要跟踪只有核心0,你应该在这个文件中包含1。要跟踪核心1,你应该在其中包含2。对于核心3,应该包含数字8

  • current_tracer:当前正在运行的跟踪程序。

  • tracing_on:负责启用或禁用数据写入环形缓冲区的系统文件(要启用此功能,必须在文件中添加数字1;要禁用它,添加数字0)。

  • trace:保存跟踪数据的文件,以人类可读的格式。

现在我们已经介绍了 Ftrace 并描述了它的功能,我们可以深入了解其用法,并了解它在跟踪和调试目的中有多么有用。

可用的跟踪器

我们可以使用以下命令查看可用跟踪器的列表:

# cat /sys/kernel/debug/tracing/available_tracers 
blk function_graph wakeup_dl wakeup_rt wakeup irqsoff function nop

让我们快速看一下每个跟踪器的特点:

  • function:无参数的函数调用跟踪器。

  • function_graph:带有子调用的函数调用跟踪器。

  • blk:与块设备 I/O 操作相关的调用和事件跟踪器(这是blktrace使用的)。

  • mmiotrace:内存映射 I/O 操作跟踪器。它跟踪模块对硬件的所有调用。它通过CONFIG_MMIOTRACE启用,这取决于CONFIG_HAVE_MMIOTRACE_SUPPORT

  • irqsoff:跟踪禁用中断的区域,并保存具有最长最大延迟的跟踪。此跟踪器取决于CONFIG_IRQSOFF_TRACER

  • preemptoff:取决于CONFIG_PREEMPT_TRACER。类似于irqsoff,但跟踪和记录禁用抢占的时间。

  • preemtirqsoff:类似于irqsoffpreemptoff,但跟踪和记录中断和/或抢占被禁用的最长时间。

  • wakeupwakeup_rt,由CONFIG_SCHED_TRACER启用:前者跟踪和记录唤醒后最高优先级任务被调度所需的最大延迟,而后者跟踪和记录唤醒后仅wakeup跟踪器所需的最大延迟。

  • nop:最简单的跟踪器,顾名思义,什么也不做。nop跟踪器只是显示trace_printk()调用的输出。

irqsoffpreemptoffpreemtirqsoff是所谓的延迟跟踪器。它们测量中断被禁用的时间,抢占被禁用的时间,以及中断和/或抢占被禁用的时间。唤醒延迟跟踪器测量进程在被唤醒后运行所需的时间,可以是所有任务或仅实时任务。

函数跟踪器

我们将从函数跟踪器开始介绍 Ftrace。让我们看一个测试脚本:

# cd /sys/kernel/debug/tracing
# echo function > current_tracer
# echo 1 > tracing_on
# sleep 1
# echo 0 > tracing_on
# less trace

这个脚本非常简单,但有一些值得注意的事情。我们通过将其名称写入current_tracer文件来启用当前跟踪器。接下来,我们将1写入tracing_on,这将启用环形缓冲区。语法要求1>符号之间有一个空格;echo1> tracing_on将无法工作。一行之后,我们将其禁用(如果将0写入tracing_on,缓冲区将不会清除,Ftrace 也不会被禁用)。

为什么要这样做?在两个echo命令之间,我们看到sleep 1命令。我们启用缓冲区,运行此命令,然后禁用它。这样可以让跟踪器包含与命令运行时发生的所有系统调用相关的信息。在脚本的最后一行,我们给出了在控制台中显示跟踪数据的命令。脚本运行后,我们将看到以下打印输出(这只是一个小片段):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.1 - Ftrace 函数跟踪器快照

打印开始时包含有关缓冲区中条目数量和已写入的总条目数量的信息。这两个数字之间的差异是填充缓冲区时丢失的事件数量。然后,列出了包括以下信息的函数列表:

  • 进程名称(TASK)。

  • 进程标识符(PID)。

  • 进程运行的 CPU(CPU#)。

  • 函数开始时间(TIMESTAMP)。此时间戳是自启动以来的时间。

  • 正在跟踪的函数的名称(FUNCTION)和在<-符号后调用的父函数。例如,在我们输出的第一行中,irq_may_run函数是由handle_fasteoi_irq调用的。

现在我们熟悉了函数跟踪器及其特点,我们可以了解下一个跟踪器,它更加功能丰富,提供了更多的跟踪信息,例如调用图。

function_graph跟踪器

function_graph跟踪器的工作方式类似于函数,但更详细:显示每个函数的入口和出口点。使用此跟踪器,我们可以跟踪具有子调用的函数,并测量每个函数的执行时间。

让我们编辑我们先前示例的脚本:

# cd /sys/kernel/debug/tracing
# echo function_graph > current_tracer
# echo 1 > tracing_on
# sleep 1
# echo 0 > tracing_on
# less trace

运行此脚本后,我们得到以下打印输出:

# tracer: function_graph
#
# CPU  DURATION                  FUNCTION CALLS
# |     |   |                     |   |   |   |
 5)   0.400 us    |                } /* set_next_buddy */
 5)   0.305 us    |                __update_load_avg_se();
 5)   0.340 us    |                __update_load_avg_cfs_rq();
 5)               |                update_cfs_group() {
 5)               |                  reweight_entity() {
 5)               |                    update_curr() {
 5)   0.376 us    |                      __calc_delta();
 5)   0.308 us    |                      update_min_vruntime();
 5)   1.754 us    |                    }
 5)   0.317 us    |                   account_entity_dequeue();
 5)   0.260 us    |                   account_entity_enqueue();
 5)   3.537 us    |                  }
 5)   4.221 us    |                }
 5)   0.261 us    |                hrtick_update();
 5) + 16.852 us   |              } /* dequeue_task_fair */
 5) + 23.353 us   |            } /* deactivate_task */
 5)               |            pick_next_task_fair() {
 5)   0.286 us    |              update_curr();
 5)   0.271 us    |              check_cfs_rq_runtime();
 5)               |              pick_next_entity() {
 5)   0.441 us    |            wakeup_preempt_entity.isra.77();
 5)   0.306 us    |                clear_buddies();
 5)   1.645 us    |              }
 ------------------------------------------
 5) SCTP ti-27174  =>  Composi-2089 
 ------------------------------------------
 5)   0.632 us    |              __switch_to_xtra();
 5)   0.350 us    |              finish_task_switch();
 5) ! 271.440 us  |            } /* schedule */
 5)               |            _cond_resched() {
 5)   0.267 us    |              rcu_all_qs();
 5)   0.834 us    |            }
 5) ! 273.311 us  |          } /* futex_wait_queue_me */

在此图中,DURATION显示运行函数所花费的时间。请特别注意标有+!符号的点。加号(+)表示函数花费的时间超过 10 微秒,而感叹号(!)表示函数花费的时间超过 100 微秒。在FUNCTION_CALLS下,我们找到与每个函数调用相关的信息。用于显示每个函数启动和完成的符号与 C 编程语言中相同:大括号({})标记函数,一个在开始,一个在结束;不调用任何其他函数的叶函数用分号(;)标记。

Ftrace 还允许仅对超过一定时间的函数进行跟踪,使用tracing_thresh选项。应在微秒单位中将应记录函数的时间阈值写入该文件。这可用于查找在内核中花费很长时间的例程。可能有趣的是在内核启动时使用此功能,以帮助优化启动时间。要在启动时设置阈值,可以在内核命令行中设置如下:

tracing_thresh=200 ftrace=function_graph

这将跟踪所有执行时间超过 200 微秒(0.2 毫秒)的函数。您可以使用任何持续时间阈值。

在运行时,您可以简单地执行echo 200 > tracing_thresh

函数过滤器

挑选要跟踪的函数。不言而喻,要跟踪的函数越少,开销就越小。Ftrace 的输出可能很大,要找到自己想要的内容可能会非常困难。然而,我们可以使用过滤器来简化搜索:输出将只显示我们感兴趣的函数的信息。为此,我们只需在set_ftrace_filter文件中写入我们函数的名称,如下所示:

# echo kfree > set_ftrace_filter

要禁用过滤器,我们在这个文件中添加一个空行:

# echo  > set_ftrace_filter

然后运行以下命令:

# echo kfree > set_ftrace_notrace

结果正好相反:输出将给出有关除kfree()之外的每个函数的信息。另一个有用的选项是set_ftrace_pid。这个工具用于跟踪可以代表特定进程调用的函数。

Ftrace 还有许多其他过滤选项。要更详细地了解这些选项,您可以阅读www.kernel.org/doc/Documentation/trace/ftrace.txt上提供的官方文档。

跟踪事件

介绍跟踪事件之前,让我们先谈谈tracepoints。Tracepoints 是触发系统事件的特殊代码插入。Tracepoints 可以是动态的(意味着它们有几个附加的检查),也可以是静态的(没有附加检查)。

静态 tracepoints 不会以任何方式影响系统;它们只会在被检测的函数末尾添加几个字节的函数调用,并在一个单独的部分中添加一个数据结构。动态 tracepoints 在相关代码片段执行时调用跟踪函数。跟踪数据被写入环形缓冲区。Tracepoints 可以包含在代码的任何位置。实际上,它们已经可以在许多内核函数中找到。让我们看一下mm/slab.ckmem_cache_free函数的摘录:

void kmem_cache_free(struct kmem_cache *cachep, void *objp)
{
	[...]
	trace_kmem_cache_free(_RET_IP_, objp);
}

kmem_cache_free本身就是一个 tracepoint。我们可以通过查看其他内核函数的源代码找到无数更多的例子。

Linux 内核有一个专门的 API 用于从用户空间处理 tracepoints。在/sys/kernel/debug/tracing目录中,有一个events目录,其中保存了系统事件。这些事件可供跟踪。在这个上下文中,系统事件可以理解为内核中包含的 tracepoints。

通过运行以下命令,可以查看这些列表:

# cat /sys/kernel/debug/tracing/available_events
mac80211:drv_return_void
mac80211:drv_return_int
mac80211:drv_return_bool
mac80211:drv_return_u32
mac80211:drv_return_u64
mac80211:drv_start
mac80211:drv_get_et_strings
mac80211:drv_get_et_sset_count
mac80211:drv_get_et_stats
mac80211:drv_suspend
[...]

控制台将打印出一个长列表,格式为<subsystem>:<tracepoint>。这有点不方便。我们可以通过使用以下命令打印出更结构化的列表:

# ls /sys/kernel/debug/tracing/events
block         gpio          napi          regmap      syscalls
cfg80211      header_event  net           regulator   task
clk           header_page   oom           rpm         timer
compaction    i2c           pagemap       sched       udp
enable        irq           power         signal      vmscan
fib           kmem          printk        skb         workqueue
filelock      mac80211      random        sock        writeback
filemap       migrate       raw_syscalls  spi
ftrace        module        rcu           swiotlb

所有可能的事件都组合在子目录中。在我们开始跟踪事件之前,我们将确保已经启用了对环形缓冲区的写入。

第一章中,嵌入式开发人员的 Linux 内核概念,我们介绍了hrtimers。通过列出/sys/kernel/debug/tracing/events/timer的内容,我们将得到与定时器相关的 tracepoints,包括与hrtimer相关的内容,如下所示:

# ls /sys/kernel/debug/tracing/events/timer
enable                hrtimer_init          timer_cancel
filter                hrtimer_start         timer_expire_entry
hrtimer_cancel        itimer_expire         timer_expire_exit
hrtimer_expire_entry  itimer_state          timer_init
hrtimer_expire_exit   tick_stop             timer_start
#

现在让我们跟踪对hrtimer相关内核函数的访问。对于我们的跟踪器,我们将使用nop,因为functionfunction_graph记录了太多信息,包括我们不感兴趣的事件信息。以下是我们将使用的脚本:

# cd /sys/kernel/debug/tracing/
# echo 0 > tracing_on
# echo > trace
# echo nop > current_tracer 
# echo 1 > events/timer/enable 
# echo 1 > tracing_on;
# sleep 1;
# echo 0 > tracing_on; 
# echo 0 > events/timer/enable 
# less trace

首先,我们禁用跟踪(以防已经在运行)。然后清除环形缓冲区数据,然后将当前 tracer 设置为nop。接下来,我们启用与定时器相关的 tracepoints,或者说,我们启用定时器事件跟踪。最后,我们启用跟踪并转储环形缓冲区内容,内容如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.2 - 使用 nop tracer 快照进行 Ftrace 事件跟踪

在打印输出的末尾,我们将找到有关hrtimer函数调用的信息(这里是一个小节)。有关配置事件跟踪的更详细信息可以在这里找到:www.kernel.org/doc/Documentation/trace/events.txt

使用 Ftrace 接口跟踪特定进程

使用 Ftrace 可以让您拥有启用跟踪的内核跟踪点/函数,而不管这些函数代表哪个进程运行。要跟踪代表特定函数执行的内核函数,您应该将伪set_ftrace_pid变量设置为pgrep,例如。如果进程尚未运行,可以使用包装器 shell 脚本和exec命令以已知 PID 执行命令,如下所示:

#!/bin/sh
echo $$ > /debug/tracing/set_ftrace_pid
# [can set other filtering here]
echo function_graph > /debug/tracing/current_tracer
exec $*

在上面的例子中,$$是当前执行进程的 PID(即 shell 脚本本身)。这是在set_ftrace_pid变量中设置的,然后启用function_graph跟踪器,之后这个脚本执行命令(由脚本的第一个参数指定)。

假设脚本名称为trace_process.sh,使用示例可能如下:

sudo ./trace_command ls

现在我们熟悉了跟踪事件和跟踪点。我们能够跟踪特定的内核事件或子系统。虽然在内核开发中跟踪是必须的,但有时会影响内核的稳定性。这种情况可能需要离线分析,这在调试中得到了解决,并在下一节中进行了讨论。

Linux 内核调试技巧

编写代码并不总是内核开发中最困难的方面。调试才是真正的瓶颈,即使对经验丰富的内核开发人员也是如此。也就是说,大多数内核调试工具都是内核本身的一部分。有时,通过内核消息称为Oops来辅助找到故障发生的位置。然后调试就变成了分析消息。

Oops 和紧急情况分析

Oops 是 Linux 内核在发生错误或未处理的异常时打印的消息。它尽力描述异常并在错误或异常发生之前转储调用堆栈。

例如,考虑以下内核模块:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>

static void __attribute__ ((__noinline__)) create_oops(void) {
        *(int *)0 = 0;
}

static int __init my_oops_init(void) {
       printk("oops from the module\n");
       create_oops();
       return 0;
}
static void __exit my_oops_exit(void) {
       printk("Goodbye world\n");
}
module_init(my_oops_init);
module_exit(my_oops_exit);
MODULE_LICENSE("GPL");

在上述模块代码中,我们尝试取消引用空指针以使内核发生紧急情况。此外,我们使用__noinline__属性,以便create_oops()不会被内联,允许它在反汇编和调用堆栈中显示为单独的函数。此模块已在 ARM 和 x86 平台上构建和测试。Oops 消息和内容将因机器而异:

# insmod /oops.ko 
[29934.977983] Unable to handle kernel NULL pointer dereference at virtual address 00000000
[29935.010853] pgd = cc59c000
[29935.013809] [00000000] *pgd=00000000
[29935.017425] Internal error: Oops - BUG: 805 [#1] PREEMPT ARM
[...]
[29935.193185] systime: 1602070584s
[29935.196435] CPU: 0 PID: 20021 Comm: insmod Tainted: P           O    4.4.106-ts-armv7l #1
[29935.204629] Hardware name: Columbus Platform
[29935.208916] task: cc731a40 ti: cc66c000 task.ti: cc66c000
[29935.214354] PC is at create_oops+0x18/0x20 [oops]
[29935.219082] LR is at my_oops_init+0x18/0x1000 [oops]
[29935.224068] pc : [<bf2a8018>]    lr : [<bf045018>]    psr: 60000013
[29935.224068] sp : cc66dda8  ip : cc66ddb8  fp : cc66ddb4
[29935.235572] r10: cc68c9a4  r9 : c08058d0  r8 : c08058d0
[29935.240813] r7 : 00000000  r6 : c0802048  r5 : bf045000  r4 : cd4eca40
[29935.247359] r3 : 00000000  r2 : a6af642b  r1 : c05f3a6a  r0 : 00000014
[29935.253906] Flags: nZCv  IRQs on  FIQs on  Mode SVC_32  ISA ARM  Segment none
[29935.261059] Control: 10c5387d  Table: 4c59c059  DAC: 00000051
[29935.266822] Process insmod (pid: 20021, stack limit = 0xcc66c208)
[29935.272932] Stack: (0xcc66dda8 to 0xcc66e000)
[29935.277311] dda0:                   cc66ddc4 cc66ddb8 bf045018 bf2a800c cc66de44 cc66ddc8
[29935.285518] ddc0: c01018b4 bf04500c cc66de0c cc66ddd8 c01efdbc a6af642b cff76eec cff6d28c
[29935.293725] dde0: cf001e40 cc24b600 c01e80b8 c01ee628 cf001e40 c01ee638 cc66de44 cc66de08
[...]
[29935.425018] dfe0: befdcc10 befdcc00 004fda50 b6eda3e0 a0000010 00000003 00000000 00000000
[29935.433257] Code: e24cb004 e52de004 e8bd4000 e3a03000 (e5833000) 
[29935.462814] ---[ end trace ebc2c98aeef9342e ]---
[29935.552962] Kernel panic - not syncing: Fatal exception

让我们仔细看一下上面的转储,以了解一些重要信息:

[29934.977983] Unable to handle kernel NULL pointer dereference at virtual address 00000000

第一行描述了错误及其性质,本例中指出代码尝试取消引用NULL指针:

[29935.214354] PC is at create_oops+0x18/0x20 [oops]

create_oops函数位于oops模块中(列在方括号中)。十六进制数字表示指令指针位于函数内的24(十六进制为0x18)字节处,该函数似乎为32(十六进制为0x20)字节长:

[29935.219082] LR is at my_oops_init+0x18/0x1000 [oops]

LR是链接寄存器,它包含程序计数器在达到“从子例程返回”指令时应设置的地址。换句话说,LR保存调用当前执行函数的函数的地址(PC所在的函数)。首先,这意味着my_oops_init是调用执行代码的函数。这也意味着如果PC中的函数返回,将执行的下一行将是my_oops_init+0x18,这意味着 CPU 将在my_oops_init的起始地址偏移0x18处分支:

[29935.224068] pc : [<bf2a8018>]    lr : [<bf045018>]    psr: 60000013

在上一行代码中,pclrPCLR的真实十六进制内容,没有显示符号名称。这些地址可以与addr2line程序一起使用,这是另一个我们可以用来找到错误行的工具。这是如果内核是使用CONFIG_KALLSYMS选项禁用构建的话,我们在打印输出中会看到的内容。然后我们可以推断create_oopsmy_oops_init的地址分别是0xbf2a80000xbf045000

[29935.224068] sp : cc66dda8  ip : cc66ddb8  fp : cc66ddb4 

sp代表堆栈指针,保存堆栈中的当前位置,而fp代表帧指针,指向堆栈中当前活动的帧。当函数返回时,堆栈指针被恢复到帧指针,即在调用函数之前的堆栈指针的值。维基百科的以下示例解释得很好:

例如,DrawLine的堆栈帧将具有一个内存位置,该位置保存DrawSquare使用的帧指针值。该值在进入子例程时保存,并在返回时恢复:

[29935.235572] r10: cc68c9a4  r9 : c08058d0  r8 : c08058d0
[29935.240813] r7 : 00000000  r6 : c0802048  r5 : bf045000  r4 : cd4eca40
[29935.247359] r3 : 00000000  r2 : a6af642b  r1 : c05f3a6a  r0 : 00000014

上述是一些 CPU 寄存器的转储:

[29935.266822] Process insmod (pid: 20021, stack limit = 0xcc66c208)

前一行显示了发生 panic 的进程,这里是insmod,其 PID 为20021

也有 oops 中存在回溯的情况,就像以下这样,这是通过键入echo c > /proc/sysrq-trigger生成的 oops 的摘录:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14.3 - 内核 oops 中的回溯摘录

回溯跟踪了生成 oops 之前的函数调用历史:

[29935.433257] Code: e24cb004 e52de004 e8bd4000 e3a03000 (e5833000) 

Code是在 oops 发生时正在运行的机器代码部分的十六进制转储。

oops 上的跟踪转储

当内核崩溃时,可以使用kdump/kexeccrash实用程序来检查崩溃点的系统状态。但是,这种技术无法让您看到导致崩溃的事件之前发生了什么,这可能是理解或修复错误的良好输入。

Ftrace 附带了一个试图解决此问题的功能。为了启用它,您可以将1回显到/proc/sys/kernel/ftrace_dump_on_oops,或者在内核引导参数中启用ftrace_dump_on_oops。配置了 Ftrace 并启用了此功能将指示 Ftrace 在 oops 或 panic 时以 ASCII 格式将整个跟踪缓冲区转储到控制台。将控制台输出到串行线使得调试崩溃变得更加容易。这样,您可以设置好一切,然后等待崩溃。一旦发生崩溃,您将在控制台上看到跟踪缓冲区。然后,您将能够追溯导致崩溃的事件。您可以追溯多远取决于跟踪缓冲区的大小,因为这是存储事件历史数据的地方。

也就是说,将转储到控制台可能需要很长时间,通常在放置所有内容之前缩小跟踪缓冲区,因为默认的 Ftrace 环形缓冲区每个 CPU 超过 1 兆字节。您可以使用/sys/kernel/debug/tracing/buffer_size_kb来通过在该文件中写入所需的环形缓冲区大小(以千字节为单位)来减小跟踪缓冲区的大小。请注意,该值是每个 CPU 的值,而不是环形缓冲区的总大小。

以下是修改跟踪缓冲区大小的示例:

# echo 3 > /sys/kernel/debug/tracing/buffer_size_kb

上述命令将将 Ftrace 环形缓冲区缩小到每个 CPU 的 3 千字节(1 kb 可能足够;这取决于在崩溃之前需要回溯多远)。

使用 objdump 来识别内核模块中的错误代码行的示例

我们可以使用objdump来反汇编对象文件并识别生成 oops 的行。我们使用反汇编代码来调整符号名称和偏移量,以指向确切的错误行。

以下一行将在oops.as文件中反汇编内核模块:

arm-XXXX-objdump -fS  oops.ko > oops.as

生成的输出文件将具有类似以下内容:

[...]
architecture: arm, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x00000000
Disassembly of section .text.unlikely:
00000000 <create_oops>:
   0:	e1a0c00d 	mov	ip, sp
   4:	e92dd800 	push	{fp, ip, lr, pc}
   8:	e24cb004 	sub	fp, ip, #4
   c:	e52de004 	push	{lr}		; (str lr, [sp, #-4]!)
  10:	ebfffffe 	bl	0 <__gnu_mcount_nc>
  14:	e3a03000 	mov	r3, #0
  18:	e5833000 	str	r3, [r3]
  1c:	e89da800 	ldm	sp, {fp, sp, pc}
Disassembly of section .init.text:
00000000 <init_module>:
   0:	e1a0c00d 	mov	ip, sp
   4:	e92dd800 	push	{fp, ip, lr, pc}
   8:	e24cb004 	sub	fp, ip, #4
   c:	e59f000c    ldr   r0, [pc, #12]    ; 20    <init_module+0x20>
  10:	ebfffffe 	bl	0 <printk>
  14:	ebfffffe 	bl	0 <init_module>
  18:	e3a00000 	mov	r0, #0
  1c:	e89da800 	ldm	sp, {fp, sp, pc}
  20:	00000000 	.word	0x00000000
Disassembly of section .exit.text:
00000000 <cleanup_module>:
   0:	e1a0c00d 	mov	ip, sp
   4:	e92dd800 	push	{fp, ip, lr, pc}
   8:	e24cb004 	sub	fp, ip, #4
   c:	e59f0004 	ldr	r0, [pc, #4]	; 18    <cleanup_module+0x18>
  10:	ebfffffe 	bl	0 <printk>
  14:	e89da800 	ldm	sp, {fp, sp, pc}
  18:	00000016 	.word	0x00000016

重要提示

在编译模块时启用调试选项会使调试信息在.ko对象中可用。在这种情况下,objdump -S会插入源代码和汇编,以便更好地查看。

从 oops 中,我们看到 PC 位于create_oops+0x18,这是在create_oops地址的0x18偏移处。这将我们带到18: e5833000 str r3, [r3]行。为了理解我们感兴趣的行,让我们描述它之前的行,mov r3, #0。在这行之后,我们有r3 = 0。回到我们感兴趣的行,对于熟悉 ARM 汇编语言的人来说,它意味着将r3写入到r3指向的原始地址(C 语言中[r3]的等价物是*r3)。请记住,这对应于我们代码中的*(int *)0 = 0

总结

本章介绍了一些内核调试技巧,并解释了如何使用 Ftrace 来跟踪代码,以识别奇怪的行为,比如耗时的函数和中断延迟。我们讨论了 API 的打印,无论是针对核心代码还是设备驱动程序相关的代码。最后,我们学习了如何分析和调试内核 oops。

本章标志着本书的结束,我希望您在阅读本书时和我在写作时一样享受这段旅程。我也希望我在整本书中传授知识的最大努力对您有所帮助。

目  录 第1章 引言 1 1.1 演进 1 1.2 gnu copyleft 2 1.3 kernel.org 2 1.4 邮件列表和论坛 3 1.5 linux发行版 3 1.6 查看源代码 4 1.7 编译内核 7 1.8 可加载的模块 8 1.9 整装待发 9 第2章 内核 11 2.1 启动过程 11 2.1.1 bios-provided physical ram map 12 2.1.2 758mb lowmem available 14 2.1.3 kernel command line: ro root=/dev/hda1 14 2.1.4 calibrating delay...1197.46 .bogomips (lpj=2394935) 15 2.1.5 checking hlt instruction 16 2.1.6 net: registered protocol family 2 17 2.1.7 freeing initrd memory: 387k freed 17 2.1.8 io scheduler anticipatory registered (default) 18 2.1.9 setting up standard pci resources 18 2.1.10 ext3-fs: mounted filesystem 19 2.1.11 init: version 2.85 booting 19 2.2 内核模式和用户模式 20 2.3 进程上下文和中断上下文 20 2.4 内核定时器 21 2.4.1 hz和jiffies 21 2.4.2 长延时 22 2.4.3 短延时 24 2.4.4 pentium时间戳计数器 24 2.4.5 实时钟 25 2.5 内核中的并发 26 2.5.1 自旋锁和互斥体 26 2.5.2 原子操作 30 2.5.3 读—写锁 31 2.5.4 调试 32 2.6 proc文件系统 32 2.7 内存分配 33 2.8 查看源代码 34 第3章 内核组件 37 3.1 内核线程 37 3.1.1 创建内核线程 37 3.1.2 进程状态和等待队列 41 3.1.3 用户模式辅助程序 42 3.2 辅助接口 43 3.2.1 链表 44 3.2.2 散列链表 49 3.2.3 工作队列 49 3.2.4 通知链 51 3.2.5 完成接口 54 3.2.6 kthread辅助接口 56 3.2.7 错误处理助手 57 3.3 查看源代码 58 第4章 基本概念 61 4.1 设备和驱动程序介绍 61 4.2 中断处理 63 4.2.1 中断上下文 63 4.2.2 分配irq号 64 4.2.3 设备实例:导航杆 65 4.2.4 softirq和tasklet 68 4.3 linux设备模型 71 4.3.1 udev 71 4.3.2 sysfs、kobject和设备类 73 4.3.3 热插拔和冷插拔 76 4.3.4 微码下载 76 4.3.5 模块自动加载 77 4.4 内存屏障 78 4.5 电源管理 79 4.6 查看源代码 79 第5章 字符设备驱动程序 81 5.1 字符设备驱动程序基础 81 5.2 设备实例:系统cmos 82 5.2.1 驱动程序初始化 83 5.2.2 打开与释放 86 5.2.3 数据交换 88 5.2.4 查找 92 5.2.5 控制 94 5.3 检测数据可用性 95 5.3.1 轮询 95 5.3.2 fasync 98 5.4 和并行端口交互 99 5.5 rtc子系统 108 5.6 伪字符驱动程序 109 5.7 混杂驱动程序 110 5.8 字符设备驱动程序注意事项 115 5.9 查看源代码 115 第6章 串行设备驱动程序 118 6.1 层次架构 119 6.2 uart驱动程序 121 6.2.1 设备实例:手机 122 6.2.2 rs-485 132 6.3 tty驱动程序 132 6.4 线路规程 134 6.5 查看源代码 141 第7章 输入设备驱动程序 143 7.1 输入事件驱动程序 144 7.2 输入设备驱动程序 150 7.2.1 serio 150 7.2.2 键盘 150 7.2.3 鼠标 152 7.2.4 触摸控制器 157 7.2.5 加速度传感器 158 7.2.6 输出事件 158 7.3 调试 159 7.4 查看源代码 160 第8章 i2c协议 161 8.1 i2c/smbus是什么 161 8.2 i2c核心 162 8.3 总线事务 164 8.4 设备实例:eeprom 164 8.4.1 初始化 165 8.4.2 探测设备 167 8.4.3 检查适配器的功能 169 8.4.4 访问设备 169 8.4.5 其他函数 170 8.5 设备实例:实时时钟 171 8.6 i2c-dev 174 8.7 使用lm-sensors监控硬件 174 8.8 spi总线 174 8.9 1-wire总线 176 8.10 调试 176 8.11 查看源代码 176 第9章 pcmcia和cf 179 9.1 pcmcia/cf是什么 179 9.2 linux-pcmcia子系统 181 9.3 主机控制器驱动程序 183 9.4 pcmcia核心 183 9.5 驱动程序服务 183 9.6 客户驱动程序 183 9.6.1 数据结构 184 9.6.2 设备实例:pcmcia卡 185 9.7 将零件组装在一起 188 9.8 pcmcia存储 189 9.9 串行pcmcia 189 9.10 调试 191 9.11 查看源代码 191 第10章 pci 193 10.1 pci系列 193 10.2 寻址和识别 195 10.3 访问pci 198 10.3.1 配置区 198 10.3.2 i/o和内存 199 10.4 dma 200 10.5 设备实例:以太网—调制解调器卡 203 10.5.1 初始化和探测 203 10.5.2 数据传输 209 10.6 调试 214 10.7 查看源代码 214 第11章 usb 216 11.1 usb体系架构 216 11.1.1 总线速度 218 11.1.2 主机控制器 218 11.1.3 传输模式 219 11.1.4 寻址 219 11.2 linux-usb子系统 220 11.3 驱动程序的数据结构 221 11.3.1 usb_device结构体 221 11.3.2 urb 222 11.3.3 管道 223 11.3.4 描述符结构 223 11.4 枚举 225 11.5 设备实例:遥测卡 225 11.5.1 初始化和探测过程 226 11.5.2 卡寄存器的访问 230 11.5.3 数据传输 233 11.6 类驱动程序 236 11.6.1 大容量存储设备 236 11.6.2 usb-串行端口转换器 241 11.6.3 人机接口设备 243 11.6.4 蓝牙 243 11.7 gadget驱动程序 243 11.8 调试 244 11.9 查看源代码 245 第12章 视频驱动程序 247 12.1 显示架构 247 12.2 linux视频子系统 249 12.3 显示参数 251 12.4 帧缓冲api 252 12.5 帧缓冲驱动程序 254 12.6 控制台驱动程序 265 12.6.1 设备实例:手机 266 12.6.2 启动logo 270 12.7 调试 270 12.8 查看源代码 271 第13章 音频驱动程序 273 13.1 音频架构 273 13.2 linux声音子系统 275 13.3 设备实例:mp3播放器 277 13.3.1 驱动程序函数和结构体 278 13.3.2 alsa编程 287 13.4 调试 288 13.5 查看源代码 289 第14章 块设备驱动程序 291 14.1 存储技术 291 14.2 linux块i/o层 295 14.3 i/o调度器 295 14.4 块驱动程序数据结构和方法 296 14.5 设备实例:简单存储控制器 298 14.5.1 初始化 299 14.5.2 块设备操作 301 14.5.3 磁盘访问 302 14.6 高级主题 304 14.7 调试 306 14.8 查看源代码 306 第15章 网络接口卡 308 15.1 驱动程序数据结构 308 15.1.1 套接字缓冲区 309 15.1.2 网络设备接口 310 15.1.3 激活 311 15.1.4 数据传输 311 15.1.5 看门狗 311 15.1.6 统计 312 15.1.7 配置 313 15.1.8 总线相关内容 314 15.2 与协议层会话 314 15.2.1 接收路径 314 15.2.2 发送路径 315 15.2.3 流量控制 315 15.3 缓冲区管理和并发控制 315 15.4 设备实例:以太网nic 316 15.5 isa网络驱动程序 321 15.6 atm 321 15.7 网络吞吐量 322 15.7.1 驱动程序性能 322 15.7.2 协议性能 323 15.8 查看源代码 324 第16章 linux无线设备驱动 326 16.1 蓝牙 327 16.1.1 bluez 328 16.1.2 设备实例:cf卡 329 16.1.3 设备实例:usb适配器 330 16.1.4 rfcomm 331 16.1.5 网络 332 16.1.6 hid 334 16.1.7 音频 334 16.1.8 调试 334 16.1.9 关于源代码 334 16.2 红外 335 16.2.1 linux-irda 335 16.2.2 设备实例:超级i/o芯片 337 16.2.3 设备实例:ir dongle 338 16.2.4 ircomm 340 16.2.5 联网 340 16.2.6 irda套接字 341 16.2.7 lirc 341 16.2.8 查看源代码 342 16.3 wifi 343 16.3.1 配置 343 16.3.2 设备驱动程序 346 16.3.3 查看源代码 347 16.4 蜂窝网络 347 16.4.1 gprs 347 16.4.2 cdma 349 16.5 当前趋势 350 第17章 存储技术设备 352 17.1 什么是闪存 352 17.2 linux-mtd子系统 353 17.3 映射驱动程序 353 17.4 nor芯片驱动程序 358 17.5 nand芯片驱动程序 359 17.6 用户模块 361 17.6.1 块设备模拟 361 17.6.2 字符设备模拟 361 17.6.3 jffs2 362 17.6.4 yaffs2 363 17.7 mtd工具 363 17.8 配置mtd 363 17.9 xip 364 17.10 fwh 364 17.11 调试 367 17.12 查看源代码 367 第18章 嵌入式linux 369 18.1 挑战 369 18.2 元器件选择 370 18.3 工具链 371 18.4 bootloader 372 18.5 内存布局 374 18.6 内核移植 375 18.7 嵌入式驱动程序 376 18.7.1 闪存 377 18.7.2 uart 377 18.7.3 按钮和滚轮 378 18.7.4 pcmcia/cf 378 18.7.5 sd/mmc 378 18.7.6 usb 378 18.7.7 rtc 378 18.7.8 音频 378 18.7.9 触摸屏 379 18.7.10 视频 379 18.7.11 cpld/fpga 379 18.7.12 连接性 379 18.7.13 专用领域电子器件 380 18.7.14 更多驱动程序 380 18.8 根文件系统 380 18.8.1 nfs挂载的根文件系统 381 18.8.2 紧凑型中间件 382 18.9 测试基础设施 383 18.10 调试 383 18.10.1 电路板返工 384 18.10.2 调试器 385 第19章 用户空间的驱动程序 386 19.1 进程调度和响应时间 387 19.1.1 原先的调度器 387 19.1.2 o(1)调度器 387 19.1.3 cfs 388 19.1.4 响应时间 388 19.2 访问i/o区域 390 19.3 访问内存区域 393 19.4 用户模式scsi 395 19.5 用户模式usb 397 19.6 用户模式i2c 400 19.7 uio 401 19.8 查看源代码 402 第20章 其他设备和驱动程序 403 20.1 ecc报告 403 20.2 频率调整 407 20.3 嵌入式控制器 408 20.4 acpi 408 20.5 isa与mca 410 20.6 火线 410 20.7 智能输入/输出 411 20.8 业余无线电 411 20.9 voip 411 20.10 高速互联 412 20.10.1 infiniband 413 20.10.2 rapidio 413 20.10.3 光纤通道 413 20.10.4 iscsi 413 第21章 调试设备驱动程序 414 21.1 kdb 414 21.1.1 进入调试器 415 21.1.2 kdb 415 21.1.3 kgdb 417 21.1.4 gdb 420 21.1.5 jtag调试器 421 21.1.6 下载 423 21.2 内核探测器 423 21.2.1 kprobe 423 21.2.2 jprobe 427 21.2.3 返回探针 429 21.2.4 局限性 431 21.2.5 查看源代码 431 21.3 kexec与kdump 431 21.3.1 kexec 432 21.3.2 kdump与kexec协同工作 432 21.3.3 kdump 433 21.3.4 查看源代码 437 21.4 性能剖析 437 21.4.1 利用oprofile剖析内核性能 438 21.4.2 利用gprof剖析应用程序性能 440 21.5 跟踪 441 21.6 ltp 444 21.7 uml 444 21.8 诊断工具 444 21.9 内核修改配置选项 444 21.10 测试设备 445 第22章 维护与发布 446 22.1 代码风格 446 22.2 修改标记 446 22.3 版本控制 447 22.4 一致性检查 447 22.5 构建脚本 448 22.6 可移植代码 450 第23章 结束语 451 23.1 流程一览表 451 23.2 下一步该做什么 452 附录a linux汇编 453 附录b linux与bios 457 附录c seq文件 461
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值