perf_event_open场景下mmap()是否比read()更快及PMU访问问题
关于PMU性能监控的两个问题
我需要在应用中监控硬件、软件及硬件缓存类事件,性能是这类profiling应用的核心需求。理想情况下我希望通过MRS指令直接读取CPU PMU寄存器获取周期计数等数据,但内核默认禁用EL0级别的PMU访问,因此只能借助perf工具。目前我的read_values()函数使用read()读取事件结果,为提升性能,参考PMU硬件计数器访问文档后提出两个问题:
- 使用
mmap()替代read()从文件描述符读取事件值是否能提升性能?若可以该如何实现? - 是否可以通过MRS指令直接读取PMU寄存器?文档显示可行,但找不到相关实现示例。
我尝试将read_values()中的read()替换为mmap(),但映射后读取到的事件数量nr为0,无法获取有效事件值。相关代码如下:
原始代码
struct read_format { uint64_t nr; /* The number of events */ struct { uint64_t value; /* The value of the event */ uint64_t id; /* if PERF_FORMAT_ID */ } values[nr]; }; int main() { struct perf_event_attr attr1; attr1.type = PERF_TYPE_HARDWARE; attr1.config = PERF_COUNT_HW_CPU_CYCLES; attr1.read_format = PERF_FORMAT_GROUP | PERF_FORMAT_ID; int main_fd = syscall(__NR_perf_event_open, &attr1, 0, -1, -1, 0); uint64_t id1; ioctl(main_fd, PERF_EVENT_IOC_ID, &id1); ioctl(main_fd, PERF_EVENT_IOC_RESET, 0); ioctl(main_fd, PERF_EVENT_IOC_ENABLE, 0); struct perf_event_attr attr2; attr2.type = PERF_TYPE_HARDWARE; attr2.config = PERF_COUNT_HW_CACHE_REFERENCES; attr2.read_format = PERF_FORMAT_GROUP | PERF_FORMAT_ID; int fd2 = syscall(__NR_perf_event_open, &attr2, 0, -1, main_fd, 0); uint64_t id2; ioctl(fd2, PERF_EVENT_IOC_ID, &id2); ioctl(fd2, PERF_EVENT_IOC_RESET, 0); ioctl(fd2, PERF_EVENT_IOC_ENABLE, 0); // read_values and log "START" // action // read_values and log "END" return 0; } read_values() { char buffer[4096]; int read_bytes = read(main_fd, &buffer, sizeof(buffer)); if (read_bytes == -1) { return 1; } struct read_format* rf = (struct read_format*) buffer; int values[rf->nr]; for (int i=0; i<rf->nr; i++) { values[i] = rf->values[i].value; } }
尝试修改后的read_values()
read_values() { char* buffer = mmap(NULL, sizeof(read_format), PROT_READ, MAP_SHARED, main_fd, 0); if (buffer == MAP_FAILED) { return 1; } struct read_format* rf = (struct read_format*) buffer; if (rf->nr == 0) { return 1; } int values[rf->nr]; for (int i=0; i<rf->nr; i++) { values[i] = rf->values[i].value; } }
问题解答
1. 使用mmap()替代read()的可行性与正确实现
用mmap()确实能提升性能,原因是read()每次调用都会陷入内核拷贝数据,而mmap()是将内核空间的计数器数据直接映射到用户空间,避免了数据拷贝,适合高频读取场景。
你当前的问题出在映射的大小和使用方式上:
sizeof(read_format)是错误的,因为这个结构体包含柔性数组,实际大小取决于事件数量,正确的映射大小应该是PAGE_SIZE(通常4KB),perf工具的映射接口默认使用整页大小。- 不能每次调用
read_values()都重新mmap(),应该在初始化阶段完成一次映射,后续直接读取映射区域即可,频繁mmap()/munmap()反而会带来性能开销。
正确的实现步骤:
- 在
main()中打开perf事件后,立即对主事件fd执行mmap():
#define PAGE_SIZE 4096 char* perf_map = mmap(NULL, PAGE_SIZE, PROT_READ, MAP_SHARED, main_fd, 0); if (perf_map == MAP_FAILED) { // 错误处理 }
- 修改
read_values()直接读取映射区域,注意先触发数据更新:
void read_values(char* perf_map, int main_fd) { // 先触发内核更新用户空间的映射数据 ioctl(main_fd, PERF_EVENT_IOC_REFRESH, 1); struct read_format* rf = (struct read_format*)perf_map; if (rf->nr == 0) { // 错误处理 return; } uint64_t values[rf->nr]; // 用uint64_t避免计数器值截断 for (int i=0; i<rf->nr; i++) { values[i] = rf->values[i].value; } // 处理values数据 }
- 程序结束前调用
munmap(perf_map, PAGE_SIZE)释放映射。
2. 通过MRS指令直接读取PMU寄存器
可以直接用MRS指令读取,但需要满足几个条件:
- ARM64架构:内核需开启
CONFIG_ARM64_PMU_USER_ACCESS=y配置,并且运行前设置系统参数:echo 0 > /proc/sys/kernel/perf_event_paranoid echo 1 > /sys/devices/system/cpu/pmu/userspace_access - x86架构:需将
perf_event_paranoid设为-1,内核支持用户空间RDPMC指令。
ARM64下读取周期计数器的简单实现示例:
#include <stdint.h> #include <stdio.h> // 读取PMU周期计数器(PMCCNTR_EL0寄存器) static inline uint64_t read_pmu_cycle() { uint64_t val; asm volatile("mrs %0, pmccntr_el0" : "=r"(val)); return val; } int main() { uint64_t start = read_pmu_cycle(); // 执行需要监控的操作 for (int i=0; i<1000000; i++); uint64_t end = read_pmu_cycle(); printf("Cycles: %lu\n", end - start); return 0; }
注意:如果要监控其他事件(如缓存访问),需要先通过PMU配置寄存器(如PMCR_EL0、PMSELR_EL0)选择对应事件,具体配置需参考对应架构的官方参考手册。
内容的提问来源于stack exchange,提问作者Kron
相关产品推荐
相关产品推荐

