a photo of Whexy

Wenxuan

CyberSecurity Researcher at Northwestern University

PMU 中断:如何处理它们

Whexy /
October 23, 2021

PMU 中断在硬件和软件之间扮演着重要角色。它赋予软件深入了解其在硬件上执行情况的能力。在本文中,我将介绍 PMU 和 PMI 的强大功能以及如何使用它们来实现你的目标。

在深入了解 PMU 中断处理之前,我需要向你介绍思维模型。PMU 不仅仅是一个硬件组件。它也是物理实现和算法模型之间的桥梁。如果你对什么是 PMU 和 PMI(从我独特的角度)不感兴趣,你可以直接跳转到 "在内核中处理 PMU 中断"。

PMU

如果你从事硬件辅助项目,你总有一天会遇到 PMU。PMU 是 Performance Monitor Unit(性能监控单元)的缩写。正如名字所说,它提供与性能相关的功能。它在不同架构上广泛可用,从服务器上安装的强大英特尔至强处理器到嵌入式物联网设备中使用的 Arm Cortex-M。

简而言之,PMU 是一组可以计数核心中任何可用事件的计数器。例如,如果你想知道你的程序使用了多少个周期,你去找 PMU。如果你想知道程序中执行了多少指令,你去找 PMU。我知道你的想法,"好吧,一个计数我不关心的事情的计数器。" 那这个如何:PMU 让你知道有多少分支被错误预测和预执行,这用于进一步改善程序的性能。

是的,硬件世界是狂野和疯狂的。即使你的算法是完美的,由于实现问题,它仍然会面临许多问题。例如,影响运行时花费的问题,如内存碎片、内存泄漏、过多的上下文切换、持续错误的分支预测等。糟糕的是 —— 你甚至不知道这些事情正在发生!作为软件设计师,很难想象在编码时硬件上发生的一切。这就是为什么我们需要 PMU,一个专门计数各种硬件事件的组件。

当我说事件时,我是字面意思。这些事件发生在真实的物理世界中。它们不像 GUI 编程事件,如按钮被鼠标悬停或点击。我指的是这样的事件:

  • CPU 完成处理指令并开始处理下一条指令。
  • 晶振完成一次振荡。
  • 缓存命中。

PMU 中断

正如你可能推断的:PMU 是一个计数器。为了及时了解硬件中发生的情况,程序需要重复检查计数器并观察变化,这将占用大量 CPU 时钟,因为它最终会成为开销负担。

PMU 中断是通知软件而不是浪费时间观察的绝佳方式。请注意,PMU 本身不会报告其计数器寄存器的每次增量。中断仅在 PMU 计数器溢出时生成。PMU 中断的目的是通知软件,由于溢出,计数器将重置为零,它不应该信任结束值减去开始值的结果。

如果我们想要尽快得到硬件中发生的事件通知,我们希望每次计数器增长时都被中断。这可能吗?答案是肯定的。PMU 计数器作为硬件中的 32 位无符号寄存器,设计为既可读又可写。所以我们可以控制硬件中断的时机。例如,我们让软件在每次中断发生时将寄存器的值重置为最大整数(即 0xffffffffffff)。在这种情况下,我们可以在每次事件发生时被中断。我们也可以动态更改寄存器中设置的数字,以在性能和准确性之间寻求平衡。

在内核中处理 PMU 中断

以下部分基于 Armv8,aarch64。

处理中断对你来说可能听起来不熟悉。毕竟,这应该由设备驱动程序完成。当然,Linux 配备了 PMU 驱动程序,它正确处理中断。但是 Linux 没有给我们机会自定义内置中断处理过程。这就是为什么我们需要重新实现它。

在我们开始之前,你可以通过输入以下命令检查 Linux 中断处理程序

cat /proc/interrupts

你可以看到其中名为 "arm-pmu" 的 PMU 中断处理程序。请注意表中有两个关键数字。一个是逻辑中断 ID,位于表的第一列。另一个是硬件中断 ID,跟在 Level 后面。我们将在以下步骤中同时使用它们。

例如,在我的设备上,第一个 CPU 中 PMU 的逻辑中断 ID 是 36,而硬件中断 ID 是 36。我机器上的逻辑中断 ID 36~41 是为 PMU 预留的。

💡

进一步思考

你是否曾经想过为什么 Linux 将 "硬件中断 ID" 映射到 "逻辑中断 ID"? 这是因为系统中有各种 IRQ(中断请求)域。其中,不同的 IRQ 处理程序对同一个硬件中断生效。通过中断映射,当不是你的业务时, 你可以忽略你关心的中断。Linux 中有不同类型的中断映射。 例如,我们的 Arm64 Linux 使用线性映射策略。 运行在 MIPS 架构上的 Linux 使用基数树映射。

现在我们可以用这些 ID 处理中断。程序在用户模式下不能触及硬件,所以内核模块是必需的。在该模块中,你可以使用 Linux API request_irq()。API 将逻辑中断 ID 作为其第一个参数。这里是一个例子。

int irq_id = 36; // 处理 CPU 0 上的 PMU 中断
unsigned long irq_flags = IRQF_PERCPU
                 | IRQF_NOBALANCING
                 | IRQF_NO_THREAD
                 | IRQF_SHARED;
request_irq(irq_id, pmu_irq_handler,
            // pmu_irq_handler 是一个函数指针
            irq_flags, "pmi_handler",
            (void*)pmu_irq_handler);

但是,你不能简单地通过运行此代码注册 PMU 中断的处理程序。通常,一个中断只能有一个处理程序。由于 Linux 已经有一个,我们需要首先取消注册它。

我希望内置处理程序可以与我的处理程序一起工作。所以我去了 Linux 源代码并修改了名为 arm-pmu 的 Linux 版本处理程序。我添加了标志 IRQF_SHARED,使中断可共享,这意味着其他处理程序可以同时挂钩它。

💭 [额外] 在固件中安全处理 PMU 中断

在固件中处理 PMU 中断似乎很奇怪,但这很有用,特别是对于一些不想侵入 操作系统的跟踪器。这里我只会涵盖一些基本思想。 首先,你需要配置 GIC,它是 Global Interrupt Controller(全局中断控制器) 的缩写,将中断路由到最高安全级别 EL3。然后,在 ATF 中,你应该注册 自己的处理程序。你也可以重新路由并在 Secure-EL1 中处理它们, 在那里你可以拥有自己的 TEE-OS。

Perf:使用 PMU 的最简单方式

使用 PMU 的最简单方式是不要自己处理中断!著名、强大的分析工具 perf 就是 Linux 拥有自己中断处理程序的原因。Perf 使用 PMU 监控很多东西。你可以去看 Brendan Gregg 的优秀文章 Linux perf Examples,看看 perf 能做什么。

© LICENSED UNDER CC BY-NC-SA 4.0