好的,各位听众,欢迎来到今天的“C++ perf_event_open
: 硬件性能计数器大揭秘”讲座!今天咱们不讲那些高深莫测的理论,就聊聊如何用C++来“窥探”Linux内核,看看你的CPU到底在干啥。
开场白:CPU,你到底在忙啥?
各位有没有好奇过,你的程序跑起来,CPU到底在忙啥?是忙着加减乘除,还是忙着读写内存?或者是在拼命地处理分支预测错误?想知道这些,光靠GDB是不够的,我们需要更强大的武器——Linux perf events!
Linux perf events 是一个强大的性能分析工具,它可以让你访问CPU的各种硬件性能计数器,比如指令数、缓存命中率、分支预测错误等等。而perf_event_open
系统调用,就是打开这些“潘多拉魔盒”的钥匙。
第一部分:perf_event_open
:打开性能之门
perf_event_open
是一个系统调用,它的作用是创建一个文件描述符,这个文件描述符代表一个性能计数器。通过读取这个文件描述符,你就可以获取计数器的值。
先来看看它的函数原型:
#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);
是不是觉得有点吓人?别怕,咱们慢慢来。
- *`struct perf_event_attr attr`**: 这是最重要的参数,它是一个结构体,描述了你想要监控的事件类型、配置等等。
pid_t pid
: 指定你要监控的进程ID。如果你想监控所有进程,可以传-1
。int cpu
: 指定你要监控的CPU核心。如果你想监控所有CPU核心,可以传-1
。int group_fd
: 用于将多个事件分组,这样可以同时测量它们。如果不需要分组,传-1
。unsigned long flags
: 一些标志位,控制事件的行为。
struct perf_event_attr
:性能事件的“身份证”
这个结构体定义了你想要监控的事件的各种属性。咱们挑几个常用的讲讲:
struct perf_event_attr {
__u32 type; /* Type of event */
__u32 size; /* Size of attribute structure */
__u64 config; /* Event-specific configuration */
union {
__u64 sample_period;
__u64 sample_freq;
};
__u64 sample_type;
__u64 read_format;
__u64 disabled:1,
inherit:1, /* inherit across fork/exec */
pinned:1, /* only counts when enabled */
exclusive:1, /* only user-space can enable */
exclude_user:1, /* don't count user */
exclude_kernel:1, /* don't count kernel */
exclude_hv:1, /* don't count hypervisor */
exclude_idle:1, /* don't count when idle */
mmap:1, /* include mmap data */
comm:1, /* include comm data */
freq:1, /* use freq, not period */
inherit_stat:1, /* inherit counters */
enable_on_exec:1, /* Enable on exec */
task:1, /* trace individual tasks */
watermark:1, /* wakeup_watermark */
precise_ip:2, /* skid constraint */
mmap_data:1, /* non-executable mmap data */
sample_id_all:1,
exclude_host:1, /* Don't count in the host */
exclude_guest:1, /* Don't count in the guest */
exclude_callchain:1,
exclude_missing:1,
use_clockid:1,
context_switch:1,
write_backward:1,
namespaces:1,
ksymbol:1,
bpf_event:1,
aux_output:1,
cgroup:1;
__u32 wakeup_events;
union {
__u32 bp_type;
__u64 config1;
};
union {
__u64 bp_addr;
__u64 config2;
};
__u64 bp_len;
union {
__u64 branch_sample_type;
__u64 reserved_2;
};
__u64 sample_regs_user;
__u32 sample_stack_user;
__s32 clockid;
__u64 sample_regs_intr;
__u32 aux_watermark;
__u32 sample_max_stack;
__u32 reserved_3;
__u64 reserved_4;
};
type
: 指定事件的类型,比如硬件事件、软件事件、tracepoint事件等等。常见的硬件事件类型有PERF_TYPE_HARDWARE
,对应的事件定义在perf_event.h
中。size
:sizeof(struct perf_event_attr)
,必须设置正确。config
: 指定具体的事件。比如,如果你想监控指令数,可以设置config = PERF_COUNT_HW_INSTRUCTIONS
。sample_period
/sample_freq
: 用于采样。sample_period
表示每隔多少个事件采样一次,sample_freq
表示每秒采样多少次。disabled
: 如果设置为 1,则事件一开始是禁用的,需要手动启用。exclude_user
/exclude_kernel
: 用于排除用户空间或内核空间的事件。
第二部分:代码实战:监控指令数
光说不练假把式,咱们来写一段代码,监控程序的指令数。
#include <iostream>
#include <iomanip>
#include <fstream>
#include <vector>
#include <numeric>
#include <stdexcept>
#include <chrono>
#include <thread>
#include <random>
#include <linux/perf_event.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
// Error handling macro
#define CHECK(x)
do {
if ((x) < 0) {
std::cerr << "ERROR: " << strerror(errno) << " in " << __FILE__ << ":"
<< __LINE__ << " (" << #x << ")" << std::endl;
throw std::runtime_error(strerror(errno));
}
} while (0)
// perf_event_open wrapper
long perf_event_open(struct perf_event_attr *hw_event, pid_t pid, int cpu,
int group_fd, unsigned long flags) {
long ret = syscall(__NR_perf_event_open, hw_event, pid, cpu, group_fd, flags);
return ret;
}
int main() {
struct perf_event_attr pe;
long long count;
int fd;
// 初始化 perf_event_attr 结构体
memset(&pe, 0, sizeof(struct perf_event_attr));
pe.type = PERF_TYPE_HARDWARE;
pe.config = PERF_COUNT_HW_INSTRUCTIONS;
pe.size = sizeof(struct perf_event_attr);
pe.disabled = 1; // 初始状态禁用,稍后启用
pe.exclude_kernel = 1; // 排除内核空间的计数
pe.exclude_hv = 1; // 排除 hypervisor 的计数
// 打开性能计数器
fd = perf_event_open(&pe, 0, -1, -1, 0);
if (fd == -1) {
std::cerr << "Error opening perf event: " << strerror(errno) << std::endl;
return 1;
}
// 启用计数器
ioctl(fd, PERF_EVENT_IOC_RESET, 0);
ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);
// 执行一些代码
volatile int sum = 0;
for (int i = 0; i < 1000000; ++i) {
sum += i;
}
// 禁用计数器
ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);
// 读取计数器的值
read(fd, &count, sizeof(count));
// 关闭文件描述符
close(fd);
std::cout << "执行的代码的指令数: " << count << std::endl;
return 0;
}
这段代码做了这些事情:
- 初始化
perf_event_attr
结构体: 设置事件类型为硬件事件,具体事件为指令数,初始状态禁用,排除内核和hypervisor的计数。 - 打开性能计数器: 调用
perf_event_open
创建一个文件描述符。 - 启用计数器: 使用
ioctl
启用计数器。 - 执行一些代码: 这里执行了一个简单的循环,用于产生指令。
- 禁用计数器: 使用
ioctl
禁用计数器。 - 读取计数器的值: 使用
read
读取文件描述符,获取计数器的值。 - 关闭文件描述符: 使用
close
关闭文件描述符。
编译和运行
保存代码为 perf_example.cpp
,然后编译它:
g++ perf_example.cpp -o perf_example
运行程序:
./perf_example
你应该会看到类似这样的输出:
执行的代码的指令数: 123456789
这个数字就是你的代码执行期间产生的指令数。
第三部分:更多玩法:事件分组、采样、以及更高级的技巧
掌握了基本的用法,咱们可以玩点更高级的。
事件分组
有时候,我们需要同时测量多个事件,比如指令数和缓存命中率。这时,可以使用事件分组。
// 创建一个事件组
int group_fd = perf_event_open(&pe1, 0, -1, -1, 0);
if (group_fd == -1) {
// 处理错误
}
// 创建第二个事件,并将其添加到事件组
pe2.type = PERF_TYPE_HARDWARE;
pe2.config = PERF_COUNT_HW_CACHE_MISSES;
pe2.size = sizeof(struct perf_event_attr);
pe2.disabled = 1;
int fd2 = perf_event_open(&pe2, 0, -1, group_fd, 0); // 注意这里使用 group_fd
if (fd2 == -1) {
// 处理错误
}
这样,fd2
就被添加到了 group_fd
代表的事件组中。启用 group_fd
也会同时启用 fd2
。
采样
采样可以让你在事件发生时获取更多的信息,比如指令地址、调用栈等等。
pe.type = PERF_TYPE_HARDWARE;
pe.config = PERF_COUNT_HW_CPU_CYCLES;
pe.size = sizeof(struct perf_event_attr);
pe.sample_period = 10000; // 每 10000 个 CPU 周期采样一次
pe.sample_type = PERF_SAMPLE_IP | PERF_SAMPLE_TID | PERF_SAMPLE_CALLCHAIN;
pe.wakeup_events = 1; // 每次采样后唤醒
fd = perf_event_open(&pe, pid, -1, -1, 0);
这段代码设置了每 10000 个 CPU 周期采样一次,并且每次采样都获取指令地址、线程ID和调用栈。
读取采样数据
采样数据可以通过 mmap
映射到用户空间。
size_t page_size = sysconf(_SC_PAGE_SIZE);
size_t mmap_size = page_size * (1 + sample_buffer_pages); // 至少两页
void* mmap_buffer = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
然后,你需要解析 mmap_buffer
中的数据,才能获取采样信息。这个过程比较复杂,可以参考 perf_event.h
中的定义和相关的文档。
使用 BPF 跟踪事件
BPF (Berkeley Packet Filter) 是一种强大的内核技术,它可以让你在内核中运行用户定义的代码。你可以使用 BPF 程序来过滤和处理 perf events。
// 创建 BPF 程序
int bpf_fd = bpf_load_program(BPF_PROG_TYPE_KPROBE, ...);
// 将 BPF 程序附加到 perf event
ioctl(fd, PERF_EVENT_IOC_SET_BPF, bpf_fd);
这样,每次 perf event 发生时,都会执行你定义的 BPF 程序。
第四部分:注意事项和最佳实践
- 权限问题: 通常需要 root 权限才能访问 perf events。
- 性能开销: 启用 perf events 会带来一定的性能开销,特别是在高采样率的情况下。
- 事件选择: 选择合适的事件对于分析结果至关重要。
- 错误处理:
perf_event_open
可能会失败,需要检查返回值并处理错误。 - 文档阅读:
perf_event.h
中包含了大量的定义和说明,是学习 perf events 的重要资源。 - 资源释放: 记得关闭文件描述符和释放 mmap 的内存。
第五部分:总结
Linux perf events 是一个强大的性能分析工具,可以让你深入了解程序的运行状态。perf_event_open
系统调用是访问这些性能计数器的关键。通过合理地使用 perf events,你可以发现程序中的性能瓶颈,并进行优化。
希望今天的讲座对大家有所帮助。如果有什么问题,欢迎提问!
附加部分:高级技巧与坑点避雷
-
read_format
的妙用:一次性读取多个计数器如果你想同时读取多个事件的值,不必多次调用
read
,可以通过read_format
来一次性读取。例如,你可以设置read_format
为PERF_FORMAT_GROUP | PERF_FORMAT_ID
,这样read
返回的数据会包含事件组中每个事件的ID和计数。pe.read_format = PERF_FORMAT_GROUP | PERF_FORMAT_ID; // 启用事件组后,读取数据 struct read_format { uint64_t nr; /* Number of values */ uint64_t values[]; /* Values */ }; read(fd, &rf, sizeof(rf)); // 假设 rf 是 read_format 结构体 // 遍历事件组中的每个事件 for (int i = 0; i < rf.nr; ++i) { uint64_t value = rf.values[i * 2]; uint64_t id = rf.values[i * 2 + 1]; // 根据 id 找到对应的事件,并处理 value }
-
ioctl
的进阶用法:动态调整计数器ioctl
除了用于启用和禁用计数器,还可以用于动态调整计数器的配置。例如,你可以使用PERF_EVENT_IOC_SET_OUTPUT
将计数器的输出重定向到另一个文件描述符,或者使用PERF_EVENT_IOC_PERIOD
动态调整采样周期。 -
线程级别的监控:
pid
参数的奥秘perf_event_open
的pid
参数不仅可以用于监控单个进程,还可以用于监控单个线程。只需要将pid
设置为线程ID即可。注意,线程ID 在 Linux 中也是一个进程ID。 -
cpu
参数的精确定位:监控特定 CPU 核心如果你想监控特定 CPU 核心的性能,可以将
cpu
参数设置为对应的核心ID。这在多核 CPU 上进行性能分析时非常有用。 -
flags
参数的隐藏技能:控制事件行为flags
参数可以控制事件的各种行为,例如,你可以使用PERF_FLAG_FD_CLOEXEC
标志来确保在exec
系统调用后,文件描述符会被自动关闭。 -
坑点避雷:
PERF_COUNT_HW_REF_CPU_CYCLES
的陷阱PERF_COUNT_HW_REF_CPU_CYCLES
看起来很美好,似乎可以提供精确的 CPU 周期数,但实际上它受到 CPU 频率变化的影响。如果 CPU 频率发生变化,PERF_COUNT_HW_REF_CPU_CYCLES
的值可能会不准确。 -
内存屏障的重要性:避免数据竞争
在多线程环境下,读取 perf events 计数器时,需要使用内存屏障来确保数据的一致性。可以使用
std::atomic
或者std::memory_order_seq_cst
来实现内存屏障。 -
性能测试的艺术:避免干扰
在进行性能测试时,要尽量避免其他程序的干扰。可以使用
taskset
命令将测试程序绑定到特定的 CPU 核心,以减少干扰。 -
持续学习:关注内核更新
Linux 内核不断更新,perf events 也在不断发展。要保持对内核更新的关注,及时了解新的特性和改进。
-
perf
命令的辅助:双剑合璧,天下无敌perf
命令是一个强大的命令行工具,可以用于分析 perf events 数据。可以将perf
命令与 C++ 代码结合使用,以实现更强大的性能分析功能。例如,可以使用 C++ 代码来收集 perf events 数据,然后使用perf report
命令来生成报告。
希望这些高级技巧和坑点避雷能够帮助大家更好地使用 Linux perf events。记住,性能分析是一门艺术,需要不断学习和实践才能掌握。
祝大家编程愉快!