要获取带调用栈的采样,需在perf_event_attr中设置sample_type |= PERF_SAMPLE_CALLCHAIN,启用CONFIG_FRAME_POINTER或CONFIG_UNWINDER_ORC,编译带-g符号,并通过mmap解析变长样本结构,动态读取nr+ip[]调用链。

perf_event_open 怎么配才能拿到带调用栈的采样?
默认用 perf_event_open 只能拿到 IP(指令指针),也就是“当前在哪条指令”,但没法知道“它被谁调用的”。要带上下文,必须显式启用调用图(call graph)支持,且依赖内核配置和用户态符号信息。
- 内核需开启
CONFIG_PERF_EVENTS=y和CONFIG_FRAME_POINTER=y(或CONFIG_UNWINDER_ORC=y,推荐后者,更准) -
perf_event_attr中必须设sample_type |= PERF_SAMPLE_CALLCHAIN - 用户态程序得带调试符号(编译加
-g),否则即使采到栈帧也解析不出函数名 - 采样频率不能太高(如
sample_period = 100000),否则栈展开开销大,容易丢样本
示例关键配置:
struct perf_event_attr pe = {
.type = PERF_TYPE_HARDWARE,
.size = sizeof(pe),
.config = PERF_COUNT_HW_INSTRUCTIONS,
.sample_type = PERF_SAMPLE_IP | PERF_SAMPLE_TID | PERF_SAMPLE_TIME | PERF_SAMPLE_CALLCHAIN,
.sample_period = 100000,
.wakeup_events = 1,
.disabled = 1
};
为什么 mmap 缓冲区里读不到完整调用链?
不是没采,是读取逻辑没对上——perf_event_mmap_page 结构体之后的环形缓冲区里,每个样本数据是变长的,而 PERF_SAMPLE_CALLCHAIN 会让样本多出一个 u64 nr + u64 ip[nr] 段。如果按固定长度解析,会直接越界或错位。
- 必须先读
struct perf_event_header判断type,再根据sample_type动态跳过字段 - 调用链长度不固定,
nr字段在ip数组前,不能假设栈深度恒为 128 - 若用
perf_read()(非 mmap)方式读,会丢弃 callchain 数据——mmap 是唯一支持全字段采样的路径 - 注意:x86_64 上栈帧可能含内核地址(如系统调用返回路径),用户态解析时需过滤掉
0xffff...段
如何把 IP 映射回 C++ 函数名和行号?
光有地址没用,得靠符号解析。Linux 不提供运行时自动 demangle + 行号映射,得自己做两件事:加载 ELF 符号表 + 处理 C++ 名称修饰。
立即学习“C++免费学习笔记(深入)”;
- 用
libelf或libdw读取可执行文件的.symtab和.debug_line段 - C++ 函数名要用
cxxabi__cxa_demangle()解码,否则看到的是_ZStlsIcSt11char_traitsIcESaIcEERSt13basic_ostreamIT_T0_ES7_RKSt7__cxx1112basic_stringIS4_S5_T1_E - 行号匹配别硬算:用
dwarf_getsrcfiles()+dwarf_getsrclines()查 IP 对应源码位置,比二分搜索可靠 - 注意 inline 函数:同一 IP 可能对应多个
DW_TAG_inlined_subroutine,需遍历调用链逐层还原
在多线程 C++ 程序里采样,怎么避免线程 ID 混淆?
perf_event_open 默认只监控调用它的线程(pid = 0, cpu = -1),但多线程热点常跨线程迁移,比如主线程派发任务、工作线程执行。不显式控制,你会看到大量样本标记为 tid=0 或错乱。
- 想监控整个进程:用
pid = getpid(),并设flags |= PERF_FLAG_PID_CGROUP(需 cgroup v1/v2 支持) - 想精确绑定某线程:在目标线程内调用
perf_event_open,传入pid = gettid(),且cpu = sched_getcpu()锁定 CPU - 避免
clone()后子线程继承 fd 导致重复采样:创建时加flags |= PERF_FLAG_FD_CLOEXEC - 注意:C++ 线程池(如
std::thread或boost::thread_pool)中线程复用频繁,建议采样周期 > 10ms,否则同一线程 tid 在不同样本中反复出现,统计失真
最麻烦的其实是符号延迟——程序刚启动时,动态链接库(.so)还没完全加载,dlopen 后的符号表得重新扫描。这部分没标准 API,得自己轮询 /proc/self/maps + dladdr() 补全。











