在Armv8/v9架构中,系统性能监控单元(System PMU)是处理器微架构的重要组成部分,它为开发者提供了硬件级的性能数据采集能力。与传统的PMU相比,系统PMU具有更丰富的事件类型和更灵活的配置方式,特别是在多核异构系统中表现出色。
系统PMU包含一组关键寄存器,其中SPMEVTYPER_EL0作为事件类型配置寄存器,直接决定了性能监控的行为模式。整个寄存器组采用分层设计:
配置类寄存器:
计数类寄存器:
控制类寄存器:
注意:实际可用的寄存器取决于具体实现的FEAT_SPMU特性版本,在访问前必须通过ID寄存器验证支持情况。
一个完整的性能监控周期包含以下阶段:
初始化阶段:
assembly复制// 选择PMU实例0
MOV x0, #0
MSR SPMSELR_EL0, x0
// 验证PMU是否存在
MRS x1, SPMIIDR_EL1
CBZ x1, unsupported_pmu
事件配置阶段:
c复制// 设置事件类型为L1D_CACHE_REFILL (事件编码0x03)
uint64_t evt_config = (0x03 << 0) | // 事件类型
(1 << 31); // 使能计数
write_sysreg(evt_config, SPMEVTYPER_EL0);
计数阶段:
bash复制# 使用perf工具读取计数
perf stat -e armv8_pmuv3_0/event=0x03/ ./workload
中断处理阶段(可选):
c复制// 在中断服务例程中处理溢出
if (read_sysreg(SPMOVSSET_EL0) & (1 << counter_idx)) {
// 处理溢出并重新配置
}
SPMEVTYPER_EL0是64位寄存器,其字段布局随实现可能有所不同,但标准格式如下:
| 位域 | 名称 | 描述 |
|---|---|---|
| [63] | P | 特权模式过滤位 |
| [62] | U | 用户模式过滤位 |
| [61:32] | RES0 | 保留位 |
| [31] | EN | 计数器使能位 |
| [30:16] | RES0 | 保留位 |
| [15:10] | EVTCOUNT | 事件计数阈值 |
| [9:0] | EVTYPEID | 事件类型编码 |
关键字段详解:
在big.LITTLE架构中,不同核心类型可能需要不同的配置:
c复制void configure_pmu(unsigned int cpu_type) {
uint64_t config = 0;
switch (cpu_type) {
case CORTEX_A78:
config = (0x03 << 0) | (1 << 31); // L1D缓存失效
break;
case CORTEX_X1:
config = (0x08 << 0) | (1 << 31); // 内存访问
break;
}
isb();
write_sysreg(config, SPMEVTYPER_EL0);
isb();
}
实践技巧:通过读取SPMIIDR_EL1可以获取PMU实现的具体信息,据此编写差异化的配置代码。
SPMEVTYPER_EL0的访问控制涉及多级安全检查:
code复制 开始
│
▼
检查FEAT_SPMU支持
│
▼
检查当前EL级别
│
├── EL0 ──┤ MDSCR_EL1.EnSPM?
│ │
├── EL1 ──┤ MDCR_EL2.EnSPM?
│ │
├── EL2 ──┤ MDCR_EL3.EnPM2?
│ │
└── EL3 ──┘ 直接访问
│
▼
检查SPMACCESSR权限
│
▼
验证计数器是否实现
│
▼
执行访问操作
c复制// 首先在EL1启用用户空间访问
write_sysreg(read_sysreg(MDSCR_EL1) | (1 << 16), MDSCR_EL1);
write_sysreg(0x3, SPMACCESSR_EL1); // 允许EL0读写
// EL0用户程序配置
void user_pmu_config() {
asm volatile(
"mov x0, #0x03\n" // 事件类型
"msr SPMEVTYPER_EL0, x0\n"
:
:
: "x0"
);
}
c复制// 在EL2配置虚拟机PMU访问
write_sysreg(read_sysreg(MDCR_EL2) | (1 << 12), MDCR_EL2); // 启用PMU
write_sysreg(0x3, SPMACCESSR_EL2); // 允许EL1访问
// 配置FGT过滤
if (FEAT_FGT2_IMPLEMENTED) {
write_sysreg(read_sysreg(HDFGWTR2_EL2) & ~(1 << 45), HDFGWTR2_EL2);
}
通过组合不同事件类型,可以构建完整的缓存分析方案:
c复制struct cache_events {
uint64_t l1d_refill;
uint64_t l1d_access;
uint64_t l2d_refill;
};
void measure_cache(struct cache_events *results) {
// 配置事件组
uint64_t saved_typer = read_sysreg(SPMEVTYPER_EL0);
// L1D缓存访问
write_sysreg(0x04, SPMEVTYPER_EL0);
isb();
results->l1d_access = read_sysreg(SPMEVCNTR_EL0);
// L1D缓存失效
write_sysreg(0x03, SPMEVTYPER_EL0);
isb();
results->l1d_refill = read_sysreg(SPMEVCNTR_EL0);
// 恢复原始配置
write_sysreg(saved_typer, SPMEVTYPER_EL0);
}
对于长时间运行的性能监控,可采用中断驱动模式:
c复制// 初始化中断
void pmu_irq_init(void) {
// 配置计数器0在溢出时触发中断
uint64_t typer = read_sysreg(SPMEVTYPER_EL0);
write_sysreg(typer | (1 << 31), SPMEVTYPER_EL0);
write_sysreg(1 << 0, SPMINTENSET_EL1);
// 注册中断处理程序
request_irq(PMU_IRQ, pmu_irq_handler);
}
// 中断处理程序
irqreturn_t pmu_irq_handler(int irq, void *dev) {
uint64_t ovf = read_sysreg(SPMOVSSET_EL0);
if (ovf & (1 << 0)) {
// 处理计数器0溢出
write_sysreg(1 << 0, SPMOVSCLR_EL0);
record_sample();
}
return IRQ_HANDLED;
}
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 计数器不递增 | 事件类型配置错误 | 检查EVTYPEID是否支持 |
| 计数器未使能 | 设置EN位(bit31)为1 | |
| 权限错误 | 当前EL无访问权限 | 检查SPMACCESSR和MDCR相关位 |
| 计数结果异常 | 阈值设置不当 | 调整EVTCOUNT阈值 |
| 中断未触发 | 中断未使能 | 配置SPMINTENSET_EL1 |
事件分组策略:
低开销监控技巧:
c复制// 采用采样模式减少开销
void lightweight_profile(void) {
// 设置高阈值
uint64_t config = (0x03 << 0) | (1 << 31) | (1000 << 10);
write_sysreg(config, SPMEVTYPER_EL0);
// 周期性读取
while (1) {
uint64_t cnt = read_sysreg(SPMEVCNTR_EL0);
if (cnt > THRESHOLD) {
trigger_analysis();
write_sysreg(0, SPMEVCNTR_EL0);
}
}
}
多核同步监控:
在实际项目中,我们发现某些ARM实现存在计数器溢出的延迟问题。一个有效的解决方案是提前设置溢出阈值,并通过以下方式检测:
c复制#define SAFE_THRESHOLD 0xFFFF0000
void safe_counter_config(void) {
// 设置初始值接近溢出点
write_sysreg(SAFE_THRESHOLD, SPMEVCNTR_EL0);
// 配置较小的事件阈值
uint64_t typer = (0x03 << 0) | (1 << 31) | (10 << 10);
write_sysreg(typer, SPMEVTYPER_EL0);
}