使用可视化跟踪诊断评估基于 Yocto 的 Linux 系统
扫描二维码
随时随地手机看文章
基于 Yocto 的 Linux 发行版上测试 Percepio 的 Tracealyzer 中的 Linux 支持功能的经验。在此过程中,我重点介绍了此类可视化跟踪诊断工具如何帮助开发人员评估其嵌入式系统的性能,从分析驱动程序和中断处理程序到检查用户空间应用程序和编译器选项。
首先让我们看看如何评估 Linux 驱动程序实现。值得注意的是,Tracealyzer for Linux 利用了 LTTng,这是一个开源跟踪器和分析器,允许开发人员通过解析 LTTng 的输出并生成可视化和详细统计数据来评估内核的性能。
图 1. 自定义 Linux 驱动程序。
正在评估驱动程序的设备有三个主要接口。I2C 接口控制设备,SPI 接口用于将数据流回 Linux 设备,GPIO 是一条中断线,用于指示何时有数据可供使用。当 GPIO 线被置位时,驱动程序会发送一个 I2C 命令来指示设备开始流式传输,这将通过 SPI 接口完成。驱动程序将指示嵌入式 Linux 系统中的 DMA 控制器 (DMAC) 管理 SPI 总线和系统 RAM 之间的数据传输,以确保 CPU 能够管理其他任务。最后,Linux 设备上的应用程序代码用于从 RAM 检索流式传输的数据并将其存储在非易失性存储器中。
Tracealyzer 用于验证两个重要指标。首先,从 GPIO 被置为有效到发出 I2C 命令的时间量保持在最短。其次,Linux 内核为驱动程序提供了足够的执行周期,使其能够定期管理 DMAC 遇到的任何问题。目标是保证在流式传输过程中丢失最少的数据,Tracealyzer 用于帮助确保这一保证。
设备开发套件
Tracealyzer 使用 LTTng 生成的文件,因此通过在设备的基于 Yocto 项目的板级支持包 (BSP) 中创建自定义层,将嵌入式 Linux 平台配置为支持 LTTng。生成的 Linux 映像加载到 SD 卡上并在开发套件上启动。加载设备驱动程序并将生成的跟踪数据存储在卡上以供离线分析。然后在主机上启动 Tracealyzer 以查看和分析收集到的跟踪。
对于调查内核空间,“跟踪视图”、“参与者实例”、“CPU 负载图”和“上下文切换强度”视图最为合适。跟踪视图(图 2)显示内核中使用的交换器(空闲任务)在整个捕获过程中占用了最多的执行资源,这是预料之中的。当驱动程序正在运行并将数据从设备传输到开发板时,特定内核线程将获得平台上更大的执行资源块来执行传输。
图 2. 垂直跟踪视图使用垂直时间线显示事件流。时间从顶部开始向下增长。每列代表系统中的一个执行上下文(通常是一个任务或中断处理程序),列中的矩形显示特定任务的运行时间。水平标签(左侧)标记记录的软件事件。图表完全响应,放大后可显示更多细节。Tracealyzer 中的大多数其他视图都链接回跟踪视图,因此单击数据点即可显示时间线中相应事件发生的位置。
下一个重要视图是参与者实例。选择下拉菜单中的“执行时间”将生成一个图表,该图表指示任何特定“参与者”(定义为任何执行元素,例如线程或进程)所占用的时间量。选择一个内核线程会显示其执行时间在 350、450 和 550 微秒处出现几个峰值。要了解这些峰值是否真的值得关注,我们需要了解系统的时序要求或将其与系统在“正常”条件下的运行情况评估进行比较。
图 3. Actor 实例窗口。
从跟踪中选择另一个内核线程会显示其中一个内核线程的执行时间出现相对较大的峰值。“CPU 负载”视图描述了不同参与者的 CPU 利用率。我们可以看到,突出显示的内核线程的执行时间峰值不会导致 CPU 利用率异常高(最大 CPU 利用率略高于 1%)。因此,该峰值无需担心。
图 4. 使用 CPU 的不同参与者。
最后一个视图是“上下文切换强度”视图,它显示内核驱动程序是否性能良好且不会导致内核崩溃。
图 5。“上下文切换强度”视图。
与其他线程相比,这并未显示任何特定内核线程的任何重大上下文切换。如果驱动程序中存在性能问题,则内核线程可能会发生重大上下文切换。这可能是由于内核调度程序将执行时间分配给内核线程,然后在一段时间后移至另一个线程,但如果该线程需要执行资源,则立即切换回该线程。同样,确定大约 20 次上下文切换是否可以接受取决于系统要求或系统正常运行时执行的测量。
这些视图还提供了一种快速概览跟踪并定位“热点”或感兴趣的异常以供进一步研究的方法。这是 Tracealyzer 的主要优点之一,否则在包含数千甚至数百万个事件的长跟踪中很难找到它们。
发现 Linux 中断处理程序中的性能问题
大多数 Linux 设备驱动程序都使用中断。关于中断,首先要记住的是,限制分配给中断处理程序的操作数量很重要,因为内核处于敏感状态,整个处理器也是如此。举个例子,当中断处理程序运行时,所有中断都被屏蔽——也就是说,没有其他中断可以触发;如果中断处理程序执行时间很长,其他中断可能会被错过。因此,在处理中断时,必须坚持最低限度,例如寄存器操作和在处理器内存中移动数据。管理数据传输等其他操作应委托给 tasklet 或其他内核机制。在驱动程序示例中,设备规范指出中断设置为每 80 毫秒触发一次,所以这定义了中断处理程序执行所需的最大时间。
知道何时使用 printk
Tracealyzer 可用于确保中断处理程序执行的操作尽可能少。例如,它几乎消除了使用 printk 语句、比较内核日志中的时间戳以及浏览无休止的 LTTng 跟踪来评估性能的所有需要。相反,它提供了中断处理程序的清晰而详细的视图。
在数据采集设备中,GPIO 信号通知驱动程序已准备好从设备收集数据,驱动程序中的 printk 调用只是在内核日志中记录已收到中断,返回值 IRQ_HANDLED 通知内核已处理中断。
但是,在加载内核模块并连接设备之前,LTTng 会在目标设备中启动,并被指示只捕获中断处理程序。将 LTTng 跟踪传输到主机并启动 Tracealyzer 后,参与者实例图仅显示中断处理程序和相关信息,以便更集中注意力。只需一眼,我们就可以知道中断处理程序的触发频率(在此示例中大约为预期的 80 微秒)。但是,通过“选择详细信息”窗口深入挖掘并打开“执行时间”选项可以发现,中断处理程序平均需要 3.3 毫秒来执行实例。
图 6. printk 调用的平均处理时间为 3.3 毫秒。
当删除 printk 调用时,显示参与者实例图并且视图仍然设置为“执行时间”,Tracealyzer 显示处理时间显著下降,从使用 printk 时的 3.3 毫秒减少到不使用 printk 时的 14 微秒。
这种差异清楚地说明了 printk 调用涉及大量处理,在打印到内核日志时必须考虑各种不同情况。因此,为了实现最佳性能,显而易见的结论是尽量避免在中断处理程序中使用 printk 或其任何派生函数。
Tracealyzer 还显示执行时间往往变化无常。虽然微秒的差异确实很小,但了解为什么会出现这种差异仍然很重要。
虽然捕获的中间部分显示中断处理程序每 80 毫秒触发一次,正如预期的那样,但记录的调用次数在开始和结束时要高得多。在捕获结束时,事情变得非常不稳定,在一个实例中,执行在 325 毫秒后被调用。
这可能是因为中断处理程序中的设备没有被指示停止触发其中断。由于中断始终存在,Linux 调度程序会不断将执行资源交还给中断处理程序;这种不良现象通常称为“抖动”,但 printk 语句掩盖了这个错误。
有趣的是,即使没有指示设备停止调用中断,中断处理程序的周期性也会在一段时间后再次稳定下来。仔细查看设备规范会发现,设备上存在一种故障安全机制,如果在 I2C 总线上没有确认中断,该机制会在一段时间后自动取消断言中断。由于在本例中,使用 printk 执行中断处理程序之间的时间约为 80 毫秒,因此执行时间会进入取消断言阶段,从而掩盖了代码没有充分取消断言中断的事实。
如果不使用 Tracealyzer,这个错误实际上会被隐藏起来,直到发布前不久删除了多余的 printk 调用时才会被发现或显现出来。在那个后期阶段,必要的驱动程序修改将导致严重的延迟和成本。
评估 Linux 系统性能
Tracealyzer 还可用于调整 Linux 系统以最大限度提高性能;我们将使用 Linux 系统(例如 Nvidia 的 Jetson Nano)和用户空间应用程序(例如 iperf)来说明这一点。
图 7 显示了本实验的基本设置。
图 7:使用 Nvidia 的 Jetson Nano 评估 Linux 系统性能。
调整 Jetson Nano 上 iperf 服务器的 CPU 亲和性可以揭示该参数如何影响客户端和服务器之间的总体吞吐量。术语“CPU 亲和性”表示执行上下文固定的特定 CPU 核心。通常,亲和性是根据应用程序设置的。如果中断和相应处理程序的 CPU 亲和性与数据包接收过程的 CPU 亲和性相匹配,则应该最大限度地减少数据包丢失,因为不会浪费时间在核心之间移动数据 - 至少这是想法。
此分析和可能的优化首先需要确定 Jetson Nano 上以太网接口 (eth0) 的亲和性。这将指示哪个处理器核心处理来自 eth0 接口的中断。通过不允许 Linux 选择处理器核心,而是将 iperf 服务器执行固定到特定核心,Mohammed 旨在评估对吞吐量的影响。由于 CPU0 负责处理来自 eth0 接口的中断,因此他将 iperf 服务器固定到 CPU3。
从主机运行 iperf 测试,然后停止并销毁 LTTng 会话,以避免产生大量无关事件的痕迹。
当 iperf 固定到 CPU3 时,这在捕获中产生了一些有趣的结果。首先,Tracealyzer 显示有四个 iperf 进程实例正在运行,尽管 Linux 只列出了一个实例。但与 Linux 报告的 PID 相对应的 iperf 实例只执行了两次:一次在 iperf 测量开始时,一次在测量结束时。
尽管 iperf 被固定到 CPU3,但 iperf 的其他实例仍在不同的 CPU 核心上执行。这实际上并不罕见,因为应用程序可以实现自己的逻辑来选择合适的 CPU 进行执行。看来 iperf 也实现了这样的逻辑。
图 8. Tracealyzer 跟踪 iperf 以评估嵌入式多核 Linux 系统性能
该跟踪还显示了 eth0 中断处理程序的一系列执行实例,这些实例与 iperf 实验大约同时执行,显示了 eth0 中断处理程序执行时间与 iperf 实例执行时间之间的相关性。
Tracealyzer 显示,从 eth0 中断处理程序完成到 iperf 实例开始执行需要 55 微秒。当系统在所有 CPU 上负载 20 个进程时,新的 iperf 测量显示吞吐量保持不变。
打开 Tracealyzer 并在人工强调 CPU 核心的情况下进行捕获,显示中断处理程序和 iperf 实例的执行顺序,其中 eth0 中断处理程序执行完成和 iperf 执行开始之间的时间约为 40 微秒。
图 9. 显示重负载下的 CPU 的视图。
虽然数字本身并不重要(尽管有趣的是,在负载下时间实际上更少),但系统负载和非负载时的时间数量级相同 - 40 微秒对 55 微秒。这是 Linux 内核的一个很棒的特性,即使用户空间应用程序似乎占用了系统的所有四个核心,它仍然可以确保其他用户空间应用程序不会缺乏 CPU 资源,并且核心间通信不会受到影响。
分析正常和紧张条件下不同执行元素之间的相互作用,可以发现 Linux 内核的一个巧妙特性,即尽最大努力为所有进程提供公平的 CPU 资源份额。