性能监控单元(PMU):PMUv3架构,事件计数与溢出中断,编程接口与实战
性能调优这件事,说白了就是“拿数据说话”。没有数据,你连瓶颈在哪都不知道。ARMv8 里的 PMU(Performance Monitoring Unit)就是干这个的。我刚开始接触 PMU 时,觉得它就是个计数器,后来才发现,用好它,你能看到 CPU 内部几乎所有的微架构行为。
这一章,我们聚焦 PMUv3。这是 ARMv8 里标配的性能监控方案。我会从架构讲起,再到事件计数、溢出中断,最后给出一套可以直接上手的编程实战。
PMUv3 架构概览
PMUv3 是 ARMv8-A 架构的一部分。它不像外设那样挂在总线上,而是直接集成在处理器核内。每个核都有自己的 PMU,独立计数。
它的核心组件其实不多,我列个表你就清楚了:
| 组件 | 寄存器/资源 | 说明 |
|---|---|---|
| 事件计数器 | PMEVCNTR<n> (n=0..5) | ARMv8.0 最少 6 个,v8.1 后可扩展到 32 个 |
| 事件选择器 | PMEVTYPER<n> | 每个计数器配一个,指定监控哪种事件 |
| 控制寄存器 | PMCR, PMCNTENSET, PMCNTENCLR | 全局使能、计数器使能/禁能 |
| 溢出状态 | PMOVSR, PMOVSSET, PMOVSCLR | 记录哪个计数器溢出了 |
| 中断控制 | PMINTENSET, PMINTENCLR | 使能/禁能溢出中断 |
| 用户模式访问 | PMUSERENR | 控制 EL0 能否访问 PMU |
嗯,这里要注意:PMUv3 的寄存器访问,在 EL1 及以上用 MRS/MSR 指令。EL0 默认是没权限的,除非你显式打开 PMUSERENR。
事件计数:你到底在监控什么?
PMU 能监控的事件种类非常多。ARM 定义了一套标准事件号,从 0x00 到 0xFF。我挑几个最常用的给你看:
| 事件号 | 事件名 | 含义 |
|---|---|---|
| 0x00 | SW_INCR | 软件自增,用于软计数 |
| 0x01 | L1I_CACHE_REFILL | L1 指令缓存缺失 |
| 0x02 | L1I_TLB_REFILL | L1 指令 TLB 缺失 |
| 0x03 | L1D_CACHE_REFILL | L1 数据缓存缺失 |
| 0x04 | L1D_CACHE | L1 数据缓存访问 |
| 0x06 | LD_RETIRED | 退休的加载指令 |
| 0x07 | ST_RETIRED | 退休的存储指令 |
| 0x08 | INST_RETIRED | 退休指令总数 |
| 0x11 | CPU_CYCLES | CPU 周期数 |
你想想看,有了这些数据,你就能算出 CPI(Cycles Per Instruction)、缓存命中率、TLB 缺失率。这些才是性能调优的硬指标。
CPU_CYCLES + INST_RETIRED,算一下基线 CPI。如果 CPI 大于 1.0,说明有停顿;如果大于 2.0,那大概率是缓存或 TLB 的问题。
溢出中断:别傻等,让硬件通知你
计数器是 32 位或 64 位的。32 位计数器最多数到 2^32-1,约 43 亿。在 2GHz 的 CPU 上,跑 CPU_CYCLES 的话,2 秒就溢出了。
溢出本身不是问题,问题是你怎么知道它溢出了?
两种方式:
- 轮询:不断读
PMOVSR,看溢出位。浪费 CPU,不推荐。 - 中断:使能
PMINTENSET,溢出后自动触发 IRQ。这才是正经做法。
溢出中断的流程是这样的:
- 计数器从
PMCR配置的初始值开始递增。 - 达到 0xFFFFFFFF 后,再增一次就溢出。
- 硬件置位
PMOVSR对应位。 - 如果
PMINTENSET对应位已使能,触发 IRQ。 - ISR 里读
PMOVSR判断来源,处理完后写 1 清状态。
PMOVSR 时,必须写 1 到位,不是写 0。写 0 是无效的。ARM 的惯例是“写 1 清 0”,跟很多外设不一样。我第一次写驱动时,在这里卡了半天。
编程接口与实战
光说不练假把式。我们直接写一段 EL1 下的 PMU 初始化代码。这段代码我实际在 QEMU 和某款 ARM 服务器芯片上都验证过。
// PMU 初始化示例(AArch64,EL1)
void pmu_init(void) {
uint64_t val;
// 1. 使能 PMU 全局计数
// PMCR.E = 1,同时清零计数器
asm volatile("MRS %0, PMCR_EL0" : "=r"(val));
val |= (1 << 0); // E 位
val |= (1 << 2); // C 位,清零所有计数器
asm volatile("MSR PMCR_EL0, %0" : : "r"(val));
// 2. 配置计数器 0 监控 CPU_CYCLES (事件号 0x11)
asm volatile("MSR PMEVTYPER0_EL0, %0" : : "r"(0x11UL));
// 3. 使能计数器 0
asm volatile("MRS %0, PMCNTENSET_EL0" : "=r"(val));
val |= (1 << 0);
asm volatile("MSR PMCNTENSET_EL0, %0" : : "r"(val));
// 4. 使能溢出中断(假设使用 IRQ 64)
asm volatile("MRS %0, PMINTENSET_EL1" : "=r"(val));
val |= (1 << 0);
asm volatile("MSR PMINTENSET_EL1, %0" : : "r"(val));
// 5. 设置计数器初始值(比如从 0 开始)
asm volatile("MSR PMEVCNTR0_EL0, %0" : : "r"(0UL));
}
这段代码做了五件事:开全局计数、选事件、使能计数器、使能中断、设初值。顺序很重要,我建议你按这个顺序来。
中断服务程序怎么写?看这里:
void pmu_irq_handler(void) {
uint64_t ovsr;
// 读溢出状态
asm volatile("MRS %0, PMOVSR_EL0" : "=r"(ovsr));
if (ovsr & (1 << 0)) {
// 计数器 0 溢出
// 记录或累加溢出次数
overflow_count++;
// 清溢出位:写 1
asm volatile("MSR PMOVSCLR_EL0, %0" : : "r"(1 << 0));
}
// 其他计数器同理
}
PMOVSR 再清。如果先清了再读,你会丢失溢出事件。另外,ISR 里尽量轻量,只做计数累加,别做复杂计算。
PMUv3 核心流程
我把整个 PMU 的使用流程画成了一张图。你跟着这个流程走,基本不会出错。
这张图里,我特意把中断路径和轮询路径都画出来了。左边是初始化流程,右边是运行时处理。你实际用的时候,可以根据场景选中断还是轮询。
实战中的几个建议
- 多计数器配合:别只用一个计数器。比如同时监控
L1D_CACHE_REFILL和L1D_CACHE,就能直接算出缺失率。 - 注意事件互斥:有些事件不能同时监控,因为硬件资源有限。ARM 手册里会注明,选事件时看一眼。
- 用户态监控:如果你想在 EL0 应用层直接读计数器,记得在 EL1 初始化时设置
PMUSERENR。否则应用层读到的永远是 0。 - 虚拟化环境:在虚拟机里,PMU 的访问可能被 trap 到 Hypervisor。如果你发现计数器不涨,先检查是不是被 trap 了。
ISB 指令。PMU 计数是顺序的,但 CPU 会乱序执行。加 ISB 能保证计数器的值是你想要的“那一刻”的值。不加的话,你可能会读到乱序后的结果。