> 技术文档 > linux之perf(17)PMU事件采集脚本_linux pmu

linux之perf(17)PMU事件采集脚本_linux pmu


Linux之perf(17)PMU事件采集脚本

Author: Once Day Date: 2025年2月22日

一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦…

漫漫长路,有人对你微笑过嘛…

全系列文章可参考专栏: Perf性能分析_Once_day的博客-CSDN博客。

参考文章:

  • Tutorial - Perf Wiki (kernel.org)
  • perf-top(1) - Linux manual page (man7.org)

文章目录

  • Linux之perf(17)PMU事件采集脚本
        • 1. Perf stat介绍
        • 2. 设计与实现
          • 2.1 采集事件来源
          • 2.2 使用CSV格式输出数据
          • 2.3 Python解析数据和保存数据
          • 2.4 制作图表
        • 3. 源码文件
1. Perf stat介绍

perf 是 Linux 内核提供的 性能分析工具,用于监控和分析 CPU、缓存、内存、I/O 等系统性能指标。

常用于统计 CPU 使用情况,分析函数调用热点(热点分析),监测硬件事件(如缓存未命中、指令执行),发现性能瓶颈。

在Linux系统上可以直接通过软件包管理器安装:

sudo apt install linux-tools-common linux-tools-$(uname -r) # Ubuntu/Debiansudo yum install perf  # CentOS/RHEL

perf statperf 的子命令,用于统计系统或进程的性能指标,比如指令执行数、CPU 时钟周期、缓存未命中等。

下面是一个基本用法示例:

root@linux:/var# perf stat  Performance counter stats for \'system wide\': 1969.215600 cpu-clock (msec) # 2.000 CPUs utilized  24504 context-switches # 0.012 M/sec    0 cpu-migrations # 0.000 K/sec    1221 page-faults  # 0.620 K/sec   1969063645 cycles  # 1.000 GHz   1085510412 instructions  # 0.55 insn per cycle  115590912 branches  # 58.699 M/sec    4341812 branch-misses # 3.76% of all branches  0.984769200 seconds time elapsed 

常用选项:

选项 作用 -e 指定监测的事件(如 cycles, cache-misses-a 监视整个系统 -C 监视指定 CPU -p 监视指定进程 -I ms 毫秒输出一次统计数据 -x 分隔字段(如 CSV 格式)
2. 设计与实现
2.1 采集事件来源

perf listperf 工具的一个子命令,用于列出所有可用的性能监控事件(PMU 事件),包括 CPU 指令、缓存、内存、软件计数等。

perf list 事件分类:

事件类型 说明 示例 硬件事件(hardware events) 由 CPU 直接提供的性能指标 cycles, instructions, cache-misses 软件事件(software events) 由内核统计的操作系统事件 context-switches, cpu-clock, page-faults 缓存事件(cache events) 监测 CPU 缓存访问情况 L1-dcache-loads, LLC-load-misses Tracepoint 事件 监测内核行为(调度、系统调用等) sched:sched_switch, syscalls:sys_enter_read PMU(Performance Monitoring Unit) 事件 处理器特定的硬件计数器 armv8_pmuv3/l1d_cache/, intel_pt//

通过使用 perf list 找到要监测的事件,然后用 perf stat -e 进行分析

例如,在设备上找到了以下与cache相关的事件:

硬件事件 bus-cycles: 总线周期数 cache-misses: cache miss次数 cache-references: cache访问次数硬件cache事件 L1-dcache-load-misses: L1数据cache读miss次数 L1-dcache-loads: L1数据cache读次数 L1-dcache-store-misses: L1数据cache写miss次数 L1-dcache-stores: L1数据cache写次数 L1-icache-load-misses: L1指令cache读miss次数 L1-icache-loads: L1指令cache读次数 branch-load-misses: 分支预测表读miss次数 branch-loads: 分支预测表读次数 dTLB-load-misses: 数据TLB读miss次数 iTLB-load-misses: 指令TLB读miss次数内核PMU事件 armv8_pmuv3/br_immed_retired/: 直接分支指令数 armv8_pmuv3/br_mis_pred/: 预测失败分支数 armv8_pmuv3/br_pred/: 预测成功分支数 armv8_pmuv3/bus_access/: 总线访问事件 armv8_pmuv3/bus_cycles/: 总线周期 armv8_pmuv3/cid_write_retired/: 上下文ID寄存器写入事件 armv8_pmuv3/cpu_cycles/: CPU周期 armv8_pmuv3/exc_return/: 异常返回事件 armv8_pmuv3/exc_taken/: 发生异常事件 armv8_pmuv3/inst_retired/: 执行指令数 armv8_pmuv3/l1d_cache/: L1数据cache访问事件 armv8_pmuv3/l1d_cache_refill/: L1数据cache refill事件 armv8_pmuv3/l1d_cache_wb/: L1数据cache写回事件 armv8_pmuv3/l1d_tlb_refill/: L1数据TLB refill事件 armv8_pmuv3/l1i_cache/: L1指令cache访问事件 armv8_pmuv3/l1i_cache_refill/: L1指令cache refill事件 armv8_pmuv3/l1i_tlb_refill/: L1指令TLB refill事件 armv8_pmuv3/l2d_cache/: L2数据cache访问事件 armv8_pmuv3/l2d_cache_refill/: L2数据cache refill事件 armv8_pmuv3/l2d_cache_wb/: L2数据cache写回事件 armv8_pmuv3/ld_retired/: 执行的load指令数 armv8_pmuv3/mem_access/: 数据内存访问事件 armv8_pmuv3/memory_error/: memory error事件 armv8_pmuv3/pc_write_retired/: 执行的PC寄存器写指令数 armv8_pmuv3/st_retired/: 执行的store指令数 armv8_pmuv3/sw_incr/: 软件增加事件计数 armv8_pmuv3/unaligned_ldst_retired/: 执行的非对齐的load/store指令数

然后分类为不同组,每组4-6个事件,不能太多,设备的PMU单元有限。当事件数超过硬件计数器支持的数量时,perf 会采用 多路复用(Multiplexing) 机制,导致每个事件只能在部分时间窗口内进行测量。

下面拆分为8个组事件:

# 定义以上提到的PMU事件, 单次最多支持6个事件THIS_PMU_EVENTS = { \"base-events\": [\"cycles\", \"instructions\", \"bus-cycles\", \"cache-misses\", \"cache-references\"], \"L1-dcache-events\": [\"L1-dcache-load-misses\", \"L1-dcache-loads\", \"L1-dcache-store-misses\", \"L1-dcache-stores\", \"dTLB-load-misses\"], \"L1-icache-events\": [\"L1-icache-load-misses\", \"L1-icache-loads\", \"iTLB-load-misses\"], \"armv8-base-events\": [\"bus_cycles\", \"bus_access\", \"mem_access\", \"memory_error\", \"cpu_cycles\", \"inst_retired\"], \"armv8-dcache-events\": [\"l1d_cache\", \"l1d_cache_refill\", \"l1d_cache_wb\", \"l1d_tlb_refill\"], \"armv8-icache-events\": [\"l1i_cache\", \"l1i_cache_refill\", \"l1i_tlb_refill\"], \"armv8-l2cache-events\": [\"l2d_cache\", \"l2d_cache_refill\", \"l2d_cache_wb\"], \"armv8-ldst-events\": [\"ld_retired\", \"st_retired\", \"unaligned_ldst_retired\"],}
2.2 使用CSV格式输出数据

perf stat支持输出CSV格式数据,我们不选择聚合数据,而是按照CPU分别输出,且每1s输出一次数据,如下:

perf stat -a -A -I 1000 -x , -e {events_str} sleep 10.5 2>&1

选项说明:

  • -a:系统范围(system-wide),监控所有 CPU,而不仅限于当前进程。
  • -A:每个 CPU 单独显示统计数据(per-CPU statistics)。
  • -I 1000:以 1000 毫秒(1 秒)为间隔,定期输出统计数据(定时采样)。
  • -x ,:使用逗号(,)作为字段分隔符,以 CSV 格式输出数据,便于解析。
  • -e {events_str}:指定要监控的硬件/软件事件,{events_str} 需要替换为具体的事件列表(如 cpu-cycles,instructions,cache-misses)。
  • sleep 10.5:让 perf stat 运行 10.5 秒,然后退出。
  • 2>&1:重定向标准错误输出到标准输出,确保所有输出信息都可以被捕获(例如写入文件或管道处理)。

下面是一个运行示例:

root@linux:/var# perf stat -e unaligned_ldst_retired,l2d_cache_refill -a -A -I 1000 -x , # time,cpu,counter,unit,event,duration(ns),unit,, 1.000226480,CPU0,73480,,unaligned_ldst_retired,1000147600,100.00,, 1.000226480,CPU1,2807316,,unaligned_ldst_retired,1000158640,100.00,, 1.000226480,CPU0,1343260,,l2d_cache_refill,1000152080,100.00,, 1.000226480,CPU1,1282233,,l2d_cache_refill,1000149120,100.00,,

perf stat 命令定期(每秒)采样两个硬件事件 unaligned_ldst_retired(未对齐的加载/存储指令退役)和 l2d_cache_refill(L2 数据缓存未命中导致的填充),并按 CPU(CPU0 和 CPU1)分别统计。

从示例数据来看,在 1 秒内:

  • 未对齐的加载/存储指令退役 (unaligned_ldst_retired),CPU0 发生 73,480 次,CPU1 发生 2,807,316 次,CPU1 远高于 CPU0,可能存在未对齐内存访问较多的任务。
  • L2 数据缓存填充 (l2d_cache_refill),CPU0 发生 1,343,260 次,CPU1 发生 1,282,233 次,两个 CPU 的 L2 缓存填充次数接近,说明 L2 缓存访问特性相似。

采样时间间隔 约 1 秒duration(ns) 约 1,000,000,000 纳秒)。

2.3 Python解析数据和保存数据

get_pmu_eventsrecord_pmu_events 是两个用于收集和记录PMU(性能监控单元)事件数据的函数。

def get_pmu_events(events_str: str) -> dict[str, list[dict[str, str]]]: cmd_str = f\"perf stat -a -A -I 1000 -x , -e {events_str} sleep 10.5 2>&1\" logging.info(f\"Start to collect PMU event data, command: {cmd_str}\") result = os.popen(cmd_str).read() pmu_events = {} for line in result.split(\"\\n\"): line_data = line.strip().split(\",\") if len(line_data) != 9: logging.debug(f\"Invalid data, skip: {line}\") continue timestamp, cpu, counter, _, event_name, duration, _, _, _ = line_data timestamp = timestamp.strip() logging.info(f\"Collect PMU event data: {timestamp}: {cpu}, {duration}, {event_name}: {counter}\") event_data = {\"timestamp\": timestamp, \"cpu\": cpu, \"cycles\": duration, \"counter\": counter} if event_name not in pmu_events: pmu_events[event_name] = [] pmu_events[event_name].append(event_data) return pmu_events

get_pmu_events 函数的作用是执行 perf stat 命令来收集指定的PMU事件数据,并解析结果,返回一个包含解析后数据的字典。

解析 perf stat 数据的步骤:

  1. 构建 perf stat 命令字符串,指定要收集的事件、采集间隔和持续时间。
  2. 使用 os.popen 异步执行 perf stat 命令,并读取输出结果。
  3. 将结果按行分割,并逐行解析数据。
  4. 每行数据按逗号分隔,提取时间戳、CPU编号、事件值和事件名称等信息。
  5. 将解析后的数据存入字典中,字典的键是事件名称,值是包含事件数据的列表。

record_pmu_events 函数的作用是遍历指定的PMU事件组,逐个采集数据,并将数据保存到指定的文件中。

def record_pmu_events(event_groups: dict[str, list[str]], output_file: str, tag: str): for group_name, events in event_groups.items(): logging.info(f\"Start to collect PMU events data for group: {group_name} - {events}\") events_str = \",\".join(events) pmu_events = get_pmu_events(events_str) with open(output_file, \"a+\") as f: for event_name, event_data in pmu_events.items(): for data in event_data:  f.write(f\"{tag},{event_name},{data[\'timestamp\']},{data[\'cpu\']},{data[\'cycles\']},{data[\'counter\']}\\n\") logging.info(f\"Save PMU events {group_name} data to file: {output_file}\") logging.info(f\"Record PMU events data finished, save to file: {output_file}\")

保存数据到文件的步骤:

  1. 遍历事件组,逐个采集PMU事件数据。
  2. 调用 get_pmu_events 函数获取指定事件组的PMU事件数据。
  3. 将解析后的数据以CSV格式保存到文件中。每条记录包含标签、事件名、时间戳、CPU编号、统计时长和计数器值。
  4. 如果文件不存在,则创建文件并写入CSV表头。
  5. 记录完成后,日志记录保存文件的操作。
2.4 制作图表

在设备上运行脚本后,可以生成CSV文件,信息如下:

tag,event_name,timestamp,cpu,cycles,counterskip_soft_checksum,cycles,1.000446320,CPU0,1000195920,1000054507skip_soft_checksum,cycles,1.000446320,CPU1,1000217200,1000190918skip_soft_checksum,cycles,2.001181600,CPU0,1000808960,1000729035skip_soft_checksum,cycles,2.001181600,CPU1,1000796640,1000787052skip_soft_checksum,cycles,3.001844080,CPU0,1000659440,1000584718skip_soft_checksum,cycles,3.001844080,CPU1,1000659520,1000649955......

但这个数据不够形象,需要转换为图表,有很多种方式,比如python处理成图表,导入Excel处理等。

我们这里选择导入飞书多维数据表格,然后生成需要的图表。

在这里插入图片描述

3. 源码文件
\'\'\'SPDX-License-Identifier: BSD-3-ClauseCopyright (c) 2025 Once Day , All rights reserved.FilePath: /linux/perf/pmu-collect/pmu-collect.py@Author: Once Day .Date: 2025-02-22 12:57@info: Encoder=utf-8,Tabsize=4,Eol=\\n.@Description: 收集指定的PMU事件数据, 使用perf stat命令收集数据, 并将数据保存到指定的文件中.@History: 2025-02-22: 支持 【收集 + 触发开关 + 收集】 的简易控制变量模型\'\'\'from math import logimport osimport sysimport logging# 日志配置, 默认INFO级别, 格式: 时间  文件名-代码行: 消息, 重定向到标准输出logging.basicConfig(level=logging.INFO, format=\"%(asctime)s  %(filename)s-%(lineno)d: %(message)s\", stream=sys.stdout)# PMU事件列表\"\"\"硬件事件 bus-cycles: 总线周期数 cache-misses: cache miss次数 cache-references: cache访问次数硬件cache事件 L1-dcache-load-misses: L1数据cache读miss次数 L1-dcache-loads: L1数据cache读次数 L1-dcache-store-misses: L1数据cache写miss次数 L1-dcache-stores: L1数据cache写次数 L1-icache-load-misses: L1指令cache读miss次数 L1-icache-loads: L1指令cache读次数 branch-load-misses: 分支预测表读miss次数 branch-loads: 分支预测表读次数 dTLB-load-misses: 数据TLB读miss次数 iTLB-load-misses: 指令TLB读miss次数内核PMU事件 armv8_pmuv3/br_immed_retired/: 直接分支指令数 armv8_pmuv3/br_mis_pred/: 预测失败分支数 armv8_pmuv3/br_pred/: 预测成功分支数 armv8_pmuv3/bus_access/: 总线访问事件 armv8_pmuv3/bus_cycles/: 总线周期 armv8_pmuv3/cid_write_retired/: 上下文ID寄存器写入事件 armv8_pmuv3/cpu_cycles/: CPU周期 armv8_pmuv3/exc_return/: 异常返回事件 armv8_pmuv3/exc_taken/: 发生异常事件 armv8_pmuv3/inst_retired/: 执行指令数 armv8_pmuv3/l1d_cache/: L1数据cache访问事件 armv8_pmuv3/l1d_cache_refill/: L1数据cache refill事件 armv8_pmuv3/l1d_cache_wb/: L1数据cache写回事件 armv8_pmuv3/l1d_tlb_refill/: L1数据TLB refill事件 armv8_pmuv3/l1i_cache/: L1指令cache访问事件 armv8_pmuv3/l1i_cache_refill/: L1指令cache refill事件 armv8_pmuv3/l1i_tlb_refill/: L1指令TLB refill事件 armv8_pmuv3/l2d_cache/: L2数据cache访问事件 armv8_pmuv3/l2d_cache_refill/: L2数据cache refill事件 armv8_pmuv3/l2d_cache_wb/: L2数据cache写回事件 armv8_pmuv3/ld_retired/: 执行的load指令数 armv8_pmuv3/mem_access/: 数据内存访问事件 armv8_pmuv3/memory_error/: memory error事件 armv8_pmuv3/pc_write_retired/: 执行的PC寄存器写指令数 armv8_pmuv3/st_retired/: 执行的store指令数 armv8_pmuv3/sw_incr/: 软件增加事件计数 armv8_pmuv3/unaligned_ldst_retired/: 执行的非对齐的load/store指令数\"\"\"# 定义以上提到的PMU事件, 单次最多支持6个事件THIS_PMU_EVENTS = { \"base-events\": [\"cycles\", \"instructions\", \"bus-cycles\", \"cache-misses\", \"cache-references\"], \"L1-dcache-events\": [\"L1-dcache-load-misses\", \"L1-dcache-loads\", \"L1-dcache-store-misses\", \"L1-dcache-stores\", \"dTLB-load-misses\"], \"L1-icache-events\": [\"L1-icache-load-misses\", \"L1-icache-loads\", \"iTLB-load-misses\"], \"armv8-base-events\": [\"bus_cycles\", \"bus_access\", \"mem_access\", \"memory_error\", \"cpu_cycles\", \"inst_retired\"], \"armv8-dcache-events\": [\"l1d_cache\", \"l1d_cache_refill\", \"l1d_cache_wb\", \"l1d_tlb_refill\"], \"armv8-icache-events\": [\"l1i_cache\", \"l1i_cache_refill\", \"l1i_tlb_refill\"], \"armv8-l2cache-events\": [\"l2d_cache\", \"l2d_cache_refill\", \"l2d_cache_wb\"], \"armv8-ldst-events\": [\"ld_retired\", \"st_retired\", \"unaligned_ldst_retired\"],}# 采集PMU事件并且解析结果, 存入文件里面\"\"\"root@linux:/var# perf stat -e unaligned_ldst_retired,l2d_cache_refill -a -A -I 1000 -x , # time,cpu,counter,unit,event,duration(ns),unit,, 1.000226480,CPU0,73480,,unaligned_ldst_retired,1000147600,100.00,, 1.000226480,CPU1,2807316,,unaligned_ldst_retired,1000158640,100.00,, 1.000226480,CPU0,1343260,,l2d_cache_refill,1000152080,100.00,, 1.000226480,CPU1,1282233,,l2d_cache_refill,1000149120,100.00,,\"\"\"def get_pmu_events(events_str: str) -> dict[str, list[dict[str, str]]]: \"\"\" 执行perf stat命令, 并且解析结果, 返回解析后的结果 -a -A: 收集所有CPU的数据, 且按照CPU编号进行区分 -I 1000: 每隔1s收集一次数据 -x ,: 使用逗号分隔数据 sleep 60: 收集60s的数据 返回数据格式: { \"unaligned_ldst_retired\": [ {\"timestamp\": \"1.000226480\", \"cpu\": \"CPU0\", \"counter\": \"1000147600\"}, {\"timestamp\": \"1.000226480\", \"cpu\": \"CPU1\", \"counter\": \"1000158640\"} ], ...... } \"\"\" cmd_str = f\"perf stat -a -A -I 1000 -x , -e {events_str} sleep 10.5 2>&1\" # 异步执行perf stat命令, 并且将结果保存到文件中(添加到文件末尾) logging.info(f\"Start to collect PMU event data, command: {cmd_str}\") # 使用pipe读取数据 result = os.popen(cmd_str).read() # logging.debug(f\"Collect PMU event data finished, output result: {result}\") # 解析结果 pmu_events = {} for line in result.split(\"\\n\"): # 解析每一行数据 line_data = line.strip().split(\",\") # 数据格式: time,cpu,counter,unit,event,duration,unit,, 9个数据 if len(line_data) != 9: logging.debug(f\"Invalid data, skip: {line}\") continue # 时间戳, CPU编号, 事件值, 事件名称 timestamp, cpu, counter, _, event_name, duration, _, _, _ = line_data # 去除timestamp中的空格 timestamp = timestamp.strip() logging.info(f\"Collect PMU event data: {timestamp}: {cpu}, {duration}, {event_name}: {counter}\") # 保存数据 event_data = {\"timestamp\": timestamp, \"cpu\": cpu, \"cycles\": duration, \"counter\": counter} if event_name not in pmu_events: pmu_events[event_name] = [] pmu_events[event_name].append(event_data) return pmu_eventsdef record_pmu_events(event_groups: dict[str, list[str]], output_file: str, tag: str): \"\"\" 遍历PMU事件组, 逐个采集数据, 并且将数据保存到文件中. \"\"\" for group_name, events in event_groups.items(): # 采集PMU事件数据 logging.info(f\"Start to collect PMU events data for group: {group_name} - {events}\") events_str = \",\".join(events) pmu_events = get_pmu_events(events_str) # 解析数据, 保存到文件中, 记录为CSV格式: 标签, 事件名, 时间戳(s), CPU编号, 统计时长(us), 计数器值 with open(output_file, \"a+\") as f: for event_name, event_data in pmu_events.items(): for data in event_data:  f.write(f\"{tag},{event_name},{data[\'timestamp\']},{data[\'cpu\']},{data[\'cycles\']},{data[\'counter\']}\\n\") logging.info(f\"Save PMU events {group_name} data to file: {output_file}\") logging.info(f\"Record PMU events data finished, save to file: {output_file}\")if __name__ == \"__main__\": save_file = \"this_pmu_events.csv\" # 如果文件不存在, 则创建文件 if not os.path.exists(save_file): with open(save_file, \"w\") as f: # CSV格式: 标签, 事件名, 时间戳(s), CPU编号, 统计时长(us), 计数器值 f.write(\"tag,event_name,timestamp,cpu,cycles,counter\\n\") logging.info(f\"Create new file: {save_file}\") else: logging.info(f\"Use existing file: {save_file}\") # 标签名称来自于命令行参数 if len(sys.argv) > 1: tag_str = sys.argv[1] else: tag_str = \"default\" logging.info(f\"Start to collect PMU events data, tag: {tag_str}\") # 采集THIS_PMU_EVENTS中定义的PMU事件, 并且保存到文件中 record_pmu_events(THIS_PMU_EVENTS, save_file, tag_str)

Alt

Once Day

也信美人终作土,不堪幽梦太匆匆......

如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注,再加上一个小小的收藏⭐!

(。◕‿◕。)感谢您的阅读与支持~~~