哈喽,各位好!今天咱们来聊聊C++里怎么玩转Linux perf_event_open
,也就是直接操纵CPU性能计数器。准备好了吗?Let’s dive in!
开场白:为啥要碰这玩意儿?
想象一下,你写了个超牛逼的C++程序,自我感觉良好,觉得速度飞起。但真相往往是残酷的:CPU可能在偷偷摸摸地打盹、缓存未命中让你欲哭无泪、分支预测错误让你心态爆炸。这时候,你就需要一个“显微镜”,能帮你观察CPU的一举一动,找到性能瓶颈。perf_event_open
就是这把显微镜的镜片!
直接使用 perf_event_open
系统调用,可以让你更精准、更灵活地收集性能数据。虽然有很多高级工具(比如 perf record
、perf top
)帮你做了封装,但直接用 perf_event_open
就像是自己造火箭,能深入理解底层原理,定制化测量方案。
perf_event_open
:主角登场
perf_event_open
是 Linux 内核提供的一个系统调用,允许用户空间程序访问 CPU 的性能计数器。它的原型长这样:
#include <linux/perf_event.h>
#include <sys/syscall.h>
#include <unistd.h>
long perf_event_open(struct perf_event_attr *attr, pid_t pid, int cpu, int group_fd, unsigned long flags);
别怕,虽然看起来有点吓人,但其实没那么复杂。
attr
: 这是一个指向perf_event_attr
结构体的指针,这个结构体定义了你想要监控的事件类型、配置等等。 它是灵魂人物,所有配置都在这里面。pid
: 你想监控哪个进程?填进程ID。如果想监控所有进程,填-1
。cpu
: 你想监控哪个CPU?填CPU编号。如果想监控所有CPU,填-1
。group_fd
: 把多个事件放在一个组里,可以一起启动、停止、读取。如果不需要分组,填-1
。flags
: 一些额外的控制标志,一般填0
就行。
返回值:成功时返回一个文件描述符,失败时返回 -1
,并设置 errno
。
perf_event_attr
:配置你的监控目标
这个结构体是关键,我们来仔细看看它有哪些重要的成员(省略了一些不常用的):
成员名 | 类型 | 描述 |
---|---|---|
type |
__u32 |
事件类型,比如硬件事件、软件事件、tracepoint事件等等。常用的有 PERF_TYPE_HARDWARE 、PERF_TYPE_SOFTWARE 。 |
size |
__u32 |
perf_event_attr 结构体的大小,必须设置为 sizeof(struct perf_event_attr) 。 |
config |
__u64 |
事件的具体配置,取决于 type 。比如,如果是硬件事件,config 可以指定要监控哪个硬件计数器(比如 PERF_COUNT_HW_CPU_CYCLES 、PERF_COUNT_HW_INSTRUCTIONS )。 |
union { ... } |
union |
一个联合体,里面定义了几个用于更精细配置的成员。常用的有 sample_period 和 sample_freq ,用于设置采样频率。 |
disabled |
__u32 |
是否禁用计数器。1 表示禁用,0 表示启用。一开始可以先禁用,配置好之后再启用。 |
inherit |
__u32 |
是否让子进程继承计数器。1 表示继承,0 表示不继承。 |
pinned |
__u32 |
是否将计数器固定在特定的硬件计数器上。这可以避免计数器在不同事件之间切换,提高精度。但硬件计数器的数量是有限的,如果所有计数器都被 pinned,可能会导致 perf_event_open 失败。 |
exclude_user |
__u32 |
是否排除用户空间的采样。1 表示排除,0 表示不排除。 |
exclude_kernel |
__u32 |
是否排除内核空间的采样。1 表示排除,0 表示不排除。 |
exclude_hv |
__u32 |
是否排除 Hypervisor 的采样。1 表示排除,0 表示不排除。 |
exclude_idle |
__u32 |
是否排除 CPU 空闲时的采样。1 表示排除,0 表示不排除。 |
mmap_data |
__u32 |
是否使用 mmap 映射数据页。如果设置为 1 ,则可以通过 mmap 读取性能数据。 |
mmap_instr |
__u32 |
是否使用 mmap 映射指令页。如果设置为 1 ,则可以通过 mmap 读取指令数据。 |
read_format |
__u64 |
读取格式,用于指定从文件描述符读取的数据的格式。常用的有 PERF_FORMAT_TOTAL_TIME_ENABLED 、PERF_FORMAT_TOTAL_TIME_RUNNING 、PERF_FORMAT_ID 、PERF_FORMAT_GROUP 。 |
sample_id_all |
__u32 |
是否在每次采样时都包含完整的 sample ID 信息。 |
inherit_stat |
__u32 |
是否让子进程继承统计信息。 |
enable_on_exec |
__u32 |
是否在 exec 系统调用时启用计数器。 |
use_clockid |
__u32 |
使用哪个时钟源。 |
clockid |
__s32 |
时钟源 ID。 |
sample_type |
__u64 |
采样类型,用于指定采样数据中包含哪些信息。常用的有 PERF_SAMPLE_IP 、PERF_SAMPLE_TID 、PERF_SAMPLE_TIME 、PERF_SAMPLE_ADDR 、PERF_SAMPLE_READ 、PERF_SAMPLE_CALLCHAIN 、PERF_SAMPLE_RAW 、PERF_SAMPLE_BRANCH_STACK 。 |
read_freq |
__u64 |
读取频率。 |
freq |
__u32 |
是否使用频率采样。1 表示使用,0 表示使用周期采样。 |
wakeup_events |
__u32 |
唤醒事件数量。 |
bp_type |
__u32 |
断点类型。 |
bp_addr |
__u64 |
断点地址。 |
bp_len |
__u64 |
断点长度。 |
union { ... } |
union |
一个联合体,里面定义了几个用于指定采样周期的成员。常用的有 sample_period 和 sample_freq 。 |
是不是有点眼花缭乱?没关系,刚开始用的时候,掌握几个最常用的就够了。
实战演练:监控CPU周期
咱们来写一个简单的例子,监控 CPU 周期数:
#include <iostream>
#include <linux/perf_event.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
// 定义 perf_event_open 系统调用的宏
#define perf_event_open(hw_event, pid, cpu, group_fd, flags)
syscall(__NR_perf_event_open, hw_event, pid, cpu, group_fd, flags)
int main() {
struct perf_event_attr pe;
memset(&pe, 0, sizeof(struct perf_event_attr)); // 初始化结构体
pe.type = PERF_TYPE_HARDWARE; // 硬件事件
pe.config = PERF_COUNT_HW_CPU_CYCLES; // 监控 CPU 周期
pe.size = sizeof(struct perf_event_attr); // 设置结构体大小
pe.disabled = 1; // 初始状态禁用,稍后启用
pe.exclude_kernel = 1; // 排除内核空间
pe.exclude_hv = 1; // 排除 Hypervisor
pe.read_format = PERF_FORMAT_TOTAL_TIME_ENABLED | PERF_FORMAT_TOTAL_TIME_RUNNING;
int fd = perf_event_open(&pe, 0, -1, -1, 0); // 监控当前进程,所有CPU,不分组
if (fd == -1) {
fprintf(stderr, "Error opening leader %llx: %sn", pe.config, strerror(errno));
exit(EXIT_FAILURE);
}
// 启用计数器
ioctl(fd, PERF_EVENT_IOC_RESET, 0);
ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);
// 模拟一些工作
long long int a = 0;
for (int i = 0; i < 100000000; ++i) {
a += i;
}
printf("Result: %lldn", a);
// 禁用计数器
ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);
// 读取计数器值
long long count[3];
read(fd, &count, sizeof(count));
// 输出结果
double time_enabled = (double)count[1] / 1000000000.0;
double time_running = (double)count[2] / 1000000000.0;
double utilization = (time_enabled > 0) ? time_running / time_enabled : 0;
printf("CPU Cycles: %lldn", count[0]);
printf("Time enabled: %.3f sn", time_enabled);
printf("Time running: %.3f sn", time_running);
printf("Utilization: %.3f %%n", utilization * 100);
close(fd);
return 0;
}
代码解读:
- 包含头文件: 包含了必要的头文件,比如定义
perf_event_attr
的<linux/perf_event.h>
,以及系统调用相关的<sys/syscall.h>
和<unistd.h>
。 - 定义系统调用宏: 因为
perf_event_open
不是标准的 POSIX 函数,所以需要手动定义系统调用宏。 - 初始化
perf_event_attr
: 创建一个perf_event_attr
结构体,并用memset
初始化为 0。 - 设置事件类型和配置: 设置
type
为PERF_TYPE_HARDWARE
,config
为PERF_COUNT_HW_CPU_CYCLES
,表示要监控硬件事件中的 CPU 周期数。 - 设置结构体大小:
size
必须设置为sizeof(struct perf_event_attr)
。 - 禁用计数器:
disabled
设置为1
,表示初始状态禁用计数器。 - 排除内核和 Hypervisor:
exclude_kernel
和exclude_hv
设置为1
,表示排除内核空间和 Hypervisor 的采样。 - 设置读取格式:
read_format
设置为PERF_FORMAT_TOTAL_TIME_ENABLED | PERF_FORMAT_TOTAL_TIME_RUNNING
,表示读取的数据包含计数器启用时间和运行时间。 - 调用
perf_event_open
: 调用perf_event_open
创建性能计数器,pid
设置为0
表示监控当前进程,cpu
设置为-1
表示监控所有 CPU,group_fd
设置为-1
表示不分组。 - 错误处理: 检查
perf_event_open
的返回值,如果失败则输出错误信息并退出。 - 启用计数器: 使用
ioctl
启用计数器,PERF_EVENT_IOC_RESET
用于重置计数器,PERF_EVENT_IOC_ENABLE
用于启用计数器。 - 模拟工作: 执行一段简单的循环,模拟一些 CPU 工作。
- 禁用计数器: 使用
ioctl
禁用计数器,PERF_EVENT_IOC_DISABLE
用于禁用计数器。 - 读取计数器值: 使用
read
从文件描述符读取计数器值。由于read_format
设置了PERF_FORMAT_TOTAL_TIME_ENABLED | PERF_FORMAT_TOTAL_TIME_RUNNING
,所以读取的数据包含三个long long int
类型的值:计数器值、启用时间和运行时间。 - 输出结果: 将读取到的计数器值和时间信息输出到控制台。
- 关闭文件描述符: 使用
close
关闭文件描述符。
编译运行:
保存为 cpu_cycles.cpp
,然后编译:
g++ cpu_cycles.cpp -o cpu_cycles
运行:
sudo ./cpu_cycles
注意:需要 sudo
权限,因为访问性能计数器通常需要 root 权限。
输出结果:
你可能会看到类似这样的输出:
Result: 4999999950000000
CPU Cycles: 1234567890
Time enabled: 1.234 s
Time running: 1.000 s
Utilization: 81.030 %
这表示程序在运行期间,CPU 消耗了 1234567890 个周期,启用时间为 1.234 秒,运行时间为 1.000 秒,CPU 利用率为 81.030%。
更进一步:采样
上面的例子只是简单地统计了 CPU 周期数。perf_event_open
还可以进行采样,即在满足一定条件(比如每隔一定数量的事件发生)时,记录 CPU 的状态(比如指令指针、调用栈等)。
要进行采样,需要设置 sample_period
或 sample_freq
,以及 sample_type
。
sample_period
: 每隔多少个事件发生时采样一次。sample_freq
: 每秒采样多少次。sample_type
: 指定采样数据中包含哪些信息。
例如,要每隔 10000 个 CPU 周期采样一次,并记录指令指针和线程ID,可以这样设置:
pe.sample_period = 10000;
pe.sample_type = PERF_SAMPLE_IP | PERF_SAMPLE_TID;
pe.mmap_data = 1; // 启用 mmap
然后,需要使用 mmap
将性能数据映射到用户空间,并解析采样数据。这部分代码比较复杂,就不在这里详细展开了。可以参考 Linux 内核文档和一些开源工具的源码。
高级技巧:事件分组
可以将多个事件放在一个组里,一起启动、停止、读取。这可以方便地计算一些比例,比如 CPI(Cycles Per Instruction)。
要将事件分组,需要:
- 创建一个 leader 事件,作为组的代表。
- 创建 follower 事件,并将
group_fd
设置为 leader 事件的文件描述符。 - 设置
read_format
为PERF_FORMAT_GROUP
,以便读取整个组的数据。
注意事项:
- 权限: 访问性能计数器通常需要 root 权限。
- 资源限制: 硬件计数器的数量是有限的,如果同时监控的事件太多,可能会导致
perf_event_open
失败。 - 性能影响: 监控性能会对程序本身产生一定的性能影响,尤其是采样时。
- 内核版本: 不同的内核版本可能支持不同的事件类型和配置。
总结:
perf_event_open
是一个强大的工具,可以让你深入了解 CPU 的运行状态,找到性能瓶颈。虽然直接使用它比较复杂,但可以让你更好地理解底层原理,定制化测量方案。希望今天的讲解能让你对 perf_event_open
有一个初步的了解。
下次再见!