0%

动态插桩工具

概述

动态插桩(Dynamic Instrumentation)是在程序运行时插入监控代码的技术,无需重新编译程序即可进行性能分析和调试。本文介绍 C/C++ 程序中常用的动态插桩工具,重点关注函数调用次数和耗时统计(平均、最小、最大、总计),以及是否支持 attach 到正在运行的进程。

重要说明:耗时统计的范围

不同工具在统计函数耗时时的行为存在重要差异:

  • 墙上时钟时间(Wall-clock Time):包括函数执行期间的所有时间,包括 CPU 执行时间、IO 等待时间、sleep 时间等。这是函数从开始到结束的”真实”耗时。
  • CPU 时间(CPU Time):只包括函数在 CPU 上实际执行的时间,不包括 IO 等待和 sleep 时间。
  • 用户态 CPU 时间(User CPU Time):只包括在用户态执行的时间,不包括内核态时间。

大多数动态插桩工具默认统计的是墙上时钟时间,这意味着如果函数中包含 IO 操作(如文件读写、网络通信)或 sleep,这些时间也会被计入总耗时。这对于理解函数的”真实”执行时间很有帮助,但需要注意区分 CPU 密集型操作和 IO 密集型操作。

动态插桩工具对比

工具 插桩方式 调用次数统计 耗时统计(avg/min/max/total) 耗时范围 Attach 支持 权限要求 开销 适用场景
eBPF/BCC 内核级动态插桩 墙上时钟时间 root 或 CAP_BPF 极低 Linux 现代系统分析
bptrace 内核级动态插桩(eBPF) 墙上时钟时间 root 或 CAP_BPF 极低 Linux 函数级性能分析
SystemTap 内核级动态插桩 墙上时钟时间 root 或 stapdev/stapusr 组 低-中 Linux 系统级分析
perf + uprobes 内核级动态插桩 墙上时钟时间(可配置) root 或 perf_event_paranoid Linux 系统级分析
DTrace 内核级动态插桩 墙上时钟时间 root 或特殊权限 Solaris/FreeBSD/macOS
Intel Pin 二进制插桩 墙上时钟时间 普通用户权限 详细分析,需要启动时插桩
DynamoRIO 二进制插桩 墙上时钟时间 普通用户权限 跨平台分析,需要启动时插桩
Valgrind Callgrind 二进制插桩 CPU 时间(不包括IO/sleep) 普通用户权限 极高 详细调用图分析
LD_PRELOAD 库函数拦截 墙上时钟时间 普通用户权限 简单场景,库函数级别
ltrace 库函数跟踪 部分 墙上时钟时间 普通用户权限(attach 需 ptrace) 库函数调用跟踪

详细工具介绍

1. eBPF/BCC

简介:基于 eBPF(Extended Berkeley Packet Filter)的现代动态跟踪工具集,BCC 提供了高级封装。

特点

  • ✅ 支持 attach 到正在运行的进程
  • ✅ 极低开销,内核验证保证安全
  • ✅ 可以统计函数调用次数和耗时(min/max/avg/total)
  • ✅ 丰富的工具集(funccount, funclatency, trace 等)
  • ⏱️ 耗时统计范围:统计墙上时钟时间(包括 IO、sleep 等所有时间)

权限要求

  • root 权限:拥有所有 eBPF/BCC 功能
  • 非 root 用户:需要 CAP_BPF 能力(Linux 5.8+):
    1
    2
    3
    4
    # 授予用户 CAP_BPF 能力
    sudo setcap cap_bpf+ep /usr/bin/python3
    # 或授予特定 BCC 工具
    sudo setcap cap_bpf+ep /usr/share/bcc/tools/funccount
  • attach 到其他用户的进程:需要 root 权限或 CAP_SYS_PTRACE 能力
  • 加载 eBPF 程序:需要 root 权限或 CAP_BPF 能力(Linux 5.8+)
  • 读取内核符号:需要 root 权限或 CAP_SYS_ADMIN 能力

限制

  • 需要 Linux 4.1+ 内核(eBPF 支持)

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# funclatency - 统计函数耗时分布
funclatency -p <pid> 'target_function'

# funccount - 统计函数调用次数
funccount -p <pid> 'target_function'

# 自定义 BCC 脚本统计详细指标
from bcc import BPF

bpf_text = """
#include <uapi/linux/ptrace.h>

BPF_HASH(start, u32);
BPF_HASH(count, u32);
BPF_HASH(total_time, u64);
BPF_HASH(min_time, u64);
BPF_HASH(max_time, u64);

int trace_entry(struct pt_regs *ctx) {
u32 pid = bpf_get_current_pid_tgid();
u64 ts = bpf_ktime_get_ns();
start.update(&pid, &ts);
u64 zero = 0;
count.update(&pid, &zero);
u64 *val = count.lookup(&pid);
if (val) {
(*val)++;
count.update(&pid, val);
}
return 0;
}

int trace_return(struct pt_regs *ctx) {
u32 pid = bpf_get_current_pid_tgid();
u64 *tsp = start.lookup(&pid);
if (tsp == 0) {
return 0;
}
u64 delta = bpf_ktime_get_ns() - *tsp;

// 更新统计信息
u64 *total = total_time.lookup(&pid);
u64 *min = min_time.lookup(&pid);
u64 *max = max_time.lookup(&pid);

if (total) {
*total += delta;
} else {
total_time.update(&pid, &delta);
}

if (!min || delta < *min) {
min_time.update(&pid, &delta);
}

if (!max || delta > *max) {
max_time.update(&pid, &delta);
}

start.delete(&pid);
return 0;
}
"""

# attach 到进程
b = BPF(text=bpf_text)
b.attach_uprobe(name="target_program", sym="target_function", fn_name="trace_entry")
b.attach_uretprobe(name="target_program", sym="target_function", fn_name="trace_return")

2. bptrace

简介:基于 eBPF 的轻量级动态追踪工具,专门用于监控和分析正在运行的 C/C++ 程序。bptrace 提供了简洁的命令行接口,可以方便地统计函数调用次数和执行时间。

特点

  • ✅ 支持 attach 到正在运行的进程
  • ✅ 可以统计函数调用次数和耗时(min/max/avg/total)
  • ✅ 极低开销,基于 eBPF 技术
  • ✅ 简洁的命令行接口,易于使用
  • ✅ 无需修改程序源码或重新编译
  • ⏱️ 耗时统计范围:统计墙上时钟时间(包括 IO、sleep 等所有时间)

权限要求

  • root 权限:拥有所有 bptrace 功能
  • 非 root 用户:需要 CAP_BPF 能力(Linux 5.8+):
    1
    2
    # 授予用户 CAP_BPF 能力
    sudo setcap cap_bpf+ep /usr/bin/bptrace
  • attach 到其他用户的进程:需要 root 权限或 CAP_SYS_PTRACE 能力
  • 加载 eBPF 程序:需要 root 权限或 CAP_BPF 能力(Linux 5.8+)

限制

  • 需要 Linux 内核支持 eBPF(通常 4.1+)
  • Linux 5.8+ 才支持非 root 用户使用 CAP_BPF
  • 主要适用于用户态函数追踪
  • 需要目标程序包含调试符号信息(或使用地址)

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 统计函数调用次数
bptrace -p <pid> -f 'target_function' -c

# 统计函数耗时(包括平均、最小、最大、总耗时)
bptrace -p <pid> -f 'target_function' -t

# 同时统计调用次数和耗时
bptrace -p <pid> -f 'target_function' -c -t

# 统计多个函数
bptrace -p <pid> -f 'function1,function2' -c -t

# 指定输出格式
bptrace -p <pid> -f 'target_function' -t --format json

# 持续监控并定期输出统计信息
bptrace -p <pid> -f 'target_function' -t --interval 5

输出示例

1
2
3
4
5
6
Function: target_function
Call Count: 1000
Total Time: 50000 us
Average Time: 50 us
Min Time: 10 us
Max Time: 200 us

与 eBPF/BCC 的关系

  • bptrace 可以看作是 BCC 工具集的简化版本,专门针对函数级性能分析
  • 相比 BCC,bptrace 提供了更简洁的命令行接口,适合快速分析
  • 如果需要更复杂的自定义逻辑,仍需要使用 BCC 编写 Python/C 脚本

3. SystemTap

简介:Linux 系统级动态跟踪工具,功能强大,支持用户态和内核态插桩。

特点

  • ✅ 支持 attach 到正在运行的进程
  • ✅ 可以精确统计函数调用次数和耗时(包括 min/max/avg/total)
  • ✅ 灵活的脚本语言,可以自定义统计逻辑
  • ✅ 低开销(取决于脚本复杂度)
  • ⏱️ 耗时统计范围:统计墙上时钟时间(包括 IO、sleep 等所有时间)

权限要求

  • root 权限:拥有所有 SystemTap 功能
  • 非 root 用户:需要加入特定组:
    • stapdev 组:可以加载任意 SystemTap 模块(需要 root 权限添加)
    • stapusr 组:只能使用预编译的 SystemTap 模块(更安全)
  • attach 到其他用户的进程:需要 root 权限或 CAP_SYS_PTRACE 能力
  • 内核模块加载:需要 root 权限或 CAP_SYS_MODULE 能力

限制

  • 需要安装 kernel-devel 包(用于编译 SystemTap 模块)

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 统计函数调用次数和耗时
probe process("/path/to/program").function("target_function") {
start_time = gettimeofday_us()
}

probe process("/path/to/program").function("target_function").return {
call_count++
elapsed = gettimeofday_us() - start_time
total_time += elapsed
if (elapsed < min_time || min_time == 0) min_time = elapsed
if (elapsed > max_time) max_time = elapsed
}

probe end {
printf("调用次数: %d\n", call_count)
printf("总耗时: %d us\n", total_time)
printf("平均耗时: %d us\n", total_time / call_count)
printf("最小耗时: %d us\n", min_time)
printf("最大耗时: %d us\n", max_time)
}

# attach 到运行中的进程
stap -x <pid> script.stp

4. perf + uprobes

简介:Linux 内核自带的性能分析工具,通过 uprobes 机制实现用户态动态插桩。

特点

  • ✅ 支持 attach 到正在运行的进程
  • ✅ 低开销,基于采样和事件计数
  • ✅ 可以统计函数调用次数和耗时
  • ✅ 无需修改程序源码或重新编译
  • ⏱️ 耗时统计范围:默认统计墙上时钟时间(包括 IO、sleep 等),也可配置为统计 CPU 时间

权限要求

  • root 权限:最直接的方式,拥有所有 perf 功能
  • 非 root 用户:需要设置 /proc/sys/kernel/perf_event_paranoid
    • -1:允许所有用户使用 perf(不推荐,安全风险)
    • 0:允许用户分析自己的进程
    • 1:允许用户分析自己的进程和内核(默认值)
    • 2:只允许 root 使用 perf
  • attach 到其他用户的进程:需要 root 权限或 CAP_SYS_PTRACE 能力
  • 查看内核符号:需要 root 权限或设置 perf_event_paranoid <= 1

限制

  • 统计详细耗时需要额外脚本处理

使用示例

1
2
3
4
5
6
7
# 统计函数调用次数
perf probe -x ./program function_name
perf record -e probe_program:function_name ./program

# 统计函数耗时(需要自定义脚本或结合其他工具)
perf record -g -p <pid> # attach 到运行中的进程
perf report

5. DTrace

简介:Sun Microsystems 开发的动态跟踪框架,现支持 Solaris、FreeBSD、macOS。

特点

  • ✅ 支持 attach 到正在运行的进程
  • ✅ 可以统计函数调用次数和耗时(min/max/avg/total)
  • ✅ 低开销,功能强大
  • ✅ 支持聚合统计(aggregations)
  • ⏱️ 耗时统计范围:统计墙上时钟时间(包括 IO、sleep 等所有时间)

权限要求

  • macOS
    • 需要关闭 SIP(System Integrity Protection)或使用特殊权限
    • 或者使用 sudo 运行(需要管理员权限)
  • Solaris/FreeBSD
    • 需要 root 权限或 dtrace_kernel 权限
  • Linux
    • 支持有限(需要 Oracle Linux 或通过 SystemTap)
    • 通常需要 root 权限

限制

  • Linux 上支持有限(需要 Oracle Linux 或通过 SystemTap)

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/sbin/dtrace -s

pid$target:target:function_name:entry
{
self->start = timestamp;
@count[probefunc] = count();
}

pid$target:target:function_name:return
/self->start/
{
this->elapsed = timestamp - self->start;
@time["total"] = sum(this->elapsed);
@time["avg"] = avg(this->elapsed);
@time["min"] = min(this->elapsed);
@time["max"] = max(this->elapsed);
self->start = 0;
}

END
{
printa("调用次数: %@d\n", @count);
printa("总耗时: %@d ns\n", @time["total"]);
printa("平均耗时: %@d ns\n", @time["avg"]);
printa("最小耗时: %@d ns\n", @time["min"]);
printa("最大耗时: %@d ns\n", @time["max"]);
}

# 使用方式
dtrace -s script.d -p <pid>

简介:基于 eBPF 的轻量级动态追踪工具,专门用于监控和分析正在运行的 C/C++ 程序。bptrace 提供了简洁的命令行接口,可以方便地统计函数调用次数和执行时间。

特点

  • ✅ 支持 attach 到正在运行的进程
  • ✅ 可以统计函数调用次数和耗时(min/max/avg/total)
  • ✅ 极低开销,基于 eBPF 技术
  • ✅ 简洁的命令行接口,易于使用
  • ✅ 无需修改程序源码或重新编译
  • ⏱️ 耗时统计范围:统计墙上时钟时间(包括 IO、sleep 等所有时间)

权限要求

  • root 权限:拥有所有 bptrace 功能
  • 非 root 用户:需要 CAP_BPF 能力(Linux 5.8+):
    1
    2
    # 授予用户 CAP_BPF 能力
    sudo setcap cap_bpf+ep /usr/bin/bptrace
  • attach 到其他用户的进程:需要 root 权限或 CAP_SYS_PTRACE 能力
  • 加载 eBPF 程序:需要 root 权限或 CAP_BPF 能力(Linux 5.8+)

限制

  • 需要 Linux 内核支持 eBPF(通常 4.1+)
  • Linux 5.8+ 才支持非 root 用户使用 CAP_BPF
  • 主要适用于用户态函数追踪
  • 需要目标程序包含调试符号信息(或使用地址)

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 统计函数调用次数
bptrace -p <pid> -f 'target_function' -c

# 统计函数耗时(包括平均、最小、最大、总耗时)
bptrace -p <pid> -f 'target_function' -t

# 同时统计调用次数和耗时
bptrace -p <pid> -f 'target_function' -c -t

# 统计多个函数
bptrace -p <pid> -f 'function1,function2' -c -t

# 指定输出格式
bptrace -p <pid> -f 'target_function' -t --format json

# 持续监控并定期输出统计信息
bptrace -p <pid> -f 'target_function' -t --interval 5

输出示例

1
2
3
4
5
6
Function: target_function
Call Count: 1000
Total Time: 50000 us
Average Time: 50 us
Min Time: 10 us
Max Time: 200 us

与 eBPF/BCC 的关系

  • bptrace 可以看作是 BCC 工具集的简化版本,专门针对函数级性能分析
  • 相比 BCC,bptrace 提供了更简洁的命令行接口,适合快速分析
  • 如果需要更复杂的自定义逻辑,仍需要使用 BCC 编写 Python/C 脚本

6. Intel Pin

简介:Intel 开发的动态二进制插桩框架,功能强大但开销较高。

特点

  • 不支持 attach,必须在程序启动时插桩
  • ✅ 可以统计函数调用次数和耗时(min/max/avg/total)
  • ✅ 支持细粒度插桩(指令级)
  • ⚠️ 高开销(通常 10-100 倍)
  • ⏱️ 耗时统计范围:统计墙上时钟时间(包括 IO、sleep 等所有时间)

权限要求

  • 普通用户权限:Intel Pin 不需要特殊权限,普通用户即可使用
  • 读取目标程序:需要目标程序的读取权限
  • 写入输出文件:需要输出目录的写入权限

限制

  • 不支持 attach 到运行中的进程
  • 高开销,不适合生产环境
  • 主要适用于详细分析和研究

使用示例

1
2
3
4
5
# 使用 Pin 工具统计函数调用
pin -t source/tools/ManualExamples/obj-intel64/inscount0.so -- ./program

# 自定义 Pin 工具统计函数耗时
# 需要编写 Pin 工具(C++)

7. DynamoRIO

简介:跨平台的动态二进制插桩框架,支持 Windows、Linux、macOS。

特点

  • 不支持 attach,必须在程序启动时插桩
  • ✅ 可以统计函数调用次数和耗时(min/max/avg/total)
  • ✅ 跨平台支持
  • ⚠️ 高开销
  • ⏱️ 耗时统计范围:统计墙上时钟时间(包括 IO、sleep 等所有时间)

权限要求

  • 普通用户权限:DynamoRIO 不需要特殊权限,普通用户即可使用
  • 读取目标程序:需要目标程序的读取权限
  • 写入输出文件:需要输出目录的写入权限
  • Windows:可能需要管理员权限(取决于目标程序)

限制

  • 不支持 attach 到运行中的进程
  • 高开销
  • 需要编写客户端工具

使用示例

1
2
3
4
5
6
# 使用 DynamoRIO 工具
drrun -tool calltrace -- ./program
drrun -tool memtrace -- ./program

# 自定义工具统计函数耗时
# 需要编写 DynamoRIO 客户端(C++)

8. Valgrind Callgrind

简介:Valgrind 工具集中的调用图分析工具。

特点

  • 不支持 attach,必须在程序启动时插桩
  • ✅ 可以统计函数调用次数和耗时
  • ✅ 生成详细的调用图
  • ⚠️ 极高开销(通常 20-100 倍)
  • ⏱️ 耗时统计范围:统计 CPU 时间(不包括 IO 等待和 sleep 时间),只统计函数在 CPU 上实际执行的时间

权限要求

  • 普通用户权限:Valgrind 不需要特殊权限,普通用户即可使用
  • 读取目标程序:需要目标程序的读取权限
  • 写入输出文件:需要输出目录的写入权限
  • 内存访问:Valgrind 需要访问进程内存,但不需要 root 权限

限制

  • 不支持 attach
  • 极高开销,不适合生产环境
  • 主要用于开发阶段的详细分析

使用示例

1
2
3
4
5
6
# 使用 Callgrind 分析
valgrind --tool=callgrind ./program

# 查看结果
callgrind_annotate callgrind.out.<pid>
kcachegrind callgrind.out.<pid> # GUI 工具

9. LD_PRELOAD + 自定义库

简介:通过 LD_PRELOAD 机制拦截库函数调用,实现简单的动态插桩。

特点

  • 不支持 attach,需要在启动时设置环境变量
  • ✅ 可以统计库函数调用次数和耗时
  • ✅ 低开销
  • ⚠️ 只能拦截库函数,不能拦截静态函数
  • ⏱️ 耗时统计范围:统计墙上时钟时间(包括 IO、sleep 等所有时间),取决于使用的计时函数(如 gettimeofday()

权限要求

  • 普通用户权限:LD_PRELOAD 不需要特殊权限,普通用户即可使用
  • 读取目标程序:需要目标程序的读取权限
  • 加载共享库:需要共享库的读取权限
  • 设置环境变量:需要设置 LD_PRELOAD 环境变量的权限(通常都有)

限制

  • 不支持 attach
  • 只能拦截库函数,不能拦截静态函数或内联函数
  • 需要手动编写包装代码

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// wrapper.c - 包装库函数
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <time.h>
#include <sys/time.h>

static unsigned long call_count = 0;
static unsigned long total_time = 0;
static unsigned long min_time = ULONG_MAX;
static unsigned long max_time = 0;

void __attribute__((constructor)) init() {
// 初始化
}

void __attribute__((destructor)) fini() {
printf("调用次数: %lu\n", call_count);
printf("总耗时: %lu us\n", total_time);
if (call_count > 0) {
printf("平均耗时: %lu us\n", total_time / call_count);
printf("最小耗时: %lu us\n", min_time);
printf("最大耗时: %lu us\n", max_time);
}
}

// 包装目标函数
int target_function(int arg) {
struct timeval start, end;
gettimeofday(&start, NULL);

// 调用原始函数
int (*original_func)(int) = dlsym(RTLD_NEXT, "target_function");
int result = original_func(arg);

gettimeofday(&end, NULL);
unsigned long elapsed = (end.tv_sec - start.tv_sec) * 1000000 +
(end.tv_usec - start.tv_usec);

call_count++;
total_time += elapsed;
if (elapsed < min_time) min_time = elapsed;
if (elapsed > max_time) max_time = elapsed;

return result;
}
1
2
3
4
5
# 编译包装库
gcc -shared -fPIC -o wrapper.so wrapper.c -ldl

# 使用
LD_PRELOAD=./wrapper.so ./program

10. ltrace

简介:Linux 库函数调用跟踪工具。

特点

  • ✅ 支持 attach 到正在运行的进程
  • ✅ 可以统计库函数调用次数
  • ⚠️ 只能统计库函数,不能统计自定义函数
  • ⚠️ 耗时统计功能有限
  • ⏱️ 耗时统计范围:统计墙上时钟时间(包括 IO、sleep 等所有时间),但功能有限

权限要求

  • 跟踪自己的进程:普通用户权限即可
  • attach 到其他用户的进程:需要 root 权限或 CAP_SYS_PTRACE 能力
  • 读取目标程序:需要目标程序的读取权限
  • ptrace 系统调用:attach 功能依赖 ptrace,受 /proc/sys/kernel/yama/ptrace_scope 限制:
    • 0:允许同一用户调试其权限范围内的任意进程
    • 1:只允许调试直接子进程(默认值)
    • 2:只有 root 或具备 CAP_SYS_PTRACE 的进程可以使用 ptrace
    • 3:完全禁用 ptrace

限制

  • 只能跟踪库函数
  • 耗时统计功能有限
  • 不适合统计自定义函数

使用示例

1
2
3
4
5
# 跟踪库函数调用
ltrace -p <pid> -c # 统计调用次数

# 跟踪特定函数
ltrace -p <pid> -e 'malloc+free'

耗时统计范围详解

墙上时钟时间 vs CPU 时间

理解不同工具统计的耗时范围对于正确解读性能数据至关重要:

1. 墙上时钟时间(Wall-clock Time)

包括的内容

  • ✅ CPU 执行时间
  • ✅ IO 等待时间(文件读写、网络通信等)
  • ✅ sleep 时间(sleep(), usleep(), nanosleep() 等)
  • ✅ 线程阻塞时间(等待锁、条件变量等)
  • ✅ 上下文切换时间

适用场景

  • 了解函数的”真实”执行时间
  • 分析 IO 密集型函数的性能
  • 诊断包含阻塞操作的函数
  • 评估用户体验相关的性能指标

示例

1
2
3
4
5
6
7
8
9
10
11
12
void slow_function() {
// CPU 执行:1ms
do_computation();

// IO 等待:100ms
read_from_disk();

// sleep:50ms
sleep(0.05);

// 总墙上时钟时间:~151ms
}

使用墙上时钟时间的工具

  • SystemTap、DTrace、eBPF/BCC、bptrace
  • Intel Pin、DynamoRIO
  • LD_PRELOAD(使用 gettimeofday() 等)
  • perf(默认配置)

2. CPU 时间(CPU Time)

包括的内容

  • ✅ CPU 执行时间
  • 不包括 IO 等待时间
  • 不包括 sleep 时间
  • 不包括 线程阻塞时间

适用场景

  • 分析 CPU 密集型函数的性能
  • 评估算法的计算复杂度
  • 识别 CPU 热点
  • 优化计算逻辑

示例

1
2
3
4
5
6
7
8
9
10
11
12
void slow_function() {
// CPU 执行:1ms(计入)
do_computation();

// IO 等待:100ms(不计入)
read_from_disk();

// sleep:50ms(不计入)
sleep(0.05);

// CPU 时间:~1ms(只包括 CPU 执行时间)
}

使用 CPU 时间的工具

  • Valgrind Callgrind(主要统计 CPU 时间)

3. 实际应用建议

选择统计范围的原则

  1. IO 密集型函数:使用墙上时钟时间

    • 文件操作、网络通信、数据库查询
    • 需要了解包括等待时间在内的总耗时
  2. CPU 密集型函数:两种时间都关注

    • 算法计算、数据处理
    • CPU 时间用于评估算法效率
    • 墙上时钟时间用于评估用户体验
  3. 混合型函数:优先使用墙上时钟时间

    • 大多数实际应用中的函数
    • 墙上时钟时间更能反映真实性能

注意事项

  • ⚠️ 多线程环境:墙上时钟时间可能小于 CPU 时间(并行执行)
  • ⚠️ IO 操作:如果函数包含 IO,墙上时钟时间会显著大于 CPU 时间
  • ⚠️ sleep 操作:如果函数包含 sleep,墙上时钟时间会包含 sleep 时间
  • ⚠️ 上下文切换:频繁的上下文切换会增加墙上时钟时间

如何区分 CPU 时间和 IO 时间

如果使用统计墙上时钟时间的工具,可以通过以下方式区分:

  1. 结合系统调用跟踪:使用 straceperf trace 查看 IO 系统调用
  2. 分析函数内部:如果函数耗时很长但 CPU 使用率低,可能是 IO 等待
  3. 使用 perf 的 CPU 时间模式perf record -e cpu-clock 可以统计 CPU 时间

权限要求总结

权限类型说明

1. root 权限

  • 含义:拥有系统最高权限
  • 获取方式:使用 sudo 或切换到 root 用户
  • 适用工具:perf、SystemTap、DTrace、eBPF/BCC、bptrace(默认需要)

2. Linux Capabilities(能力)

现代 Linux 系统使用 capabilities 机制,允许非 root 用户执行特定操作:

  • CAP_BPF:加载 eBPF 程序(Linux 5.8+)
    1
    sudo setcap cap_bpf+ep /path/to/tool
  • CAP_SYS_PTRACE:使用 ptrace attach 到其他进程
    1
    sudo setcap cap_sys_ptrace+ep /path/to/tool
  • CAP_SYS_ADMIN:访问内核符号和系统管理功能
  • CAP_SYS_MODULE:加载内核模块

3. 普通用户权限

  • 含义:不需要特殊权限,普通用户即可使用
  • 适用工具:Intel Pin、DynamoRIO、Valgrind Callgrind、LD_PRELOAD

4. 组权限

  • stapdev 组:SystemTap 开发组,可以加载任意模块
  • stapusr 组:SystemTap 用户组,只能使用预编译模块

权限配置示例

配置 perf 非 root 使用

1
2
3
4
5
6
# 允许用户分析自己的进程
echo 0 | sudo tee /proc/sys/kernel/perf_event_paranoid

# 或永久配置
echo "kernel.perf_event_paranoid = 0" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

配置 eBPF/BCC 非 root 使用(Linux 5.8+)

1
2
3
4
5
6
# 授予 Python 解释器 CAP_BPF 能力
sudo setcap cap_bpf+ep /usr/bin/python3

# 或授予特定 BCC 工具
sudo setcap cap_bpf+ep /usr/share/bcc/tools/funccount
sudo setcap cap_bpf+ep /usr/share/bcc/tools/funclatency

配置 SystemTap 非 root 使用

1
2
3
4
# 将用户添加到 stapusr 组
sudo usermod -a -G stapusr $USER

# 需要重新登录使组权限生效

配置 ptrace(用于 attach 功能)

1
2
3
4
5
6
7
8
# 查看当前 ptrace_scope 设置
cat /proc/sys/kernel/yama/ptrace_scope

# 允许同一用户调试其权限范围内的进程(开发环境)
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope

# 或永久配置
echo "kernel.yama.ptrace_scope = 0" | sudo tee -a /etc/sysctl.conf

权限要求快速参考

工具 跟踪自己的进程 attach 到其他用户的进程 内核级插桩
eBPF/BCC CAP_BPF(Linux 5.8+) root root
bptrace CAP_BPF(Linux 5.8+) root root
SystemTap stapusr 组 root root
perf 普通用户(需配置) root root
DTrace root root root
Intel Pin 普通用户 N/A N/A
DynamoRIO 普通用户 N/A N/A
Valgrind 普通用户 N/A N/A
LD_PRELOAD 普通用户 N/A N/A
ltrace 普通用户 root 或 CAP_SYS_PTRACE N/A

安全注意事项

⚠️ 生产环境建议

  • 避免使用 perf_event_paranoid = -1(允许所有用户)
  • 避免将用户添加到 stapdev 组(安全风险)
  • 谨慎配置 ptrace_scope = 0(允许任意进程调试)
  • 使用 capabilities 而非 root 权限(最小权限原则)
  • 定期审查已授予的 capabilities

工具选择建议

需要 attach 到运行中进程

  1. eBPF/BCC(推荐):现代、低开销、功能强大
  2. bptrace(推荐):简洁易用,专门针对函数级性能分析
  3. SystemTap:功能强大,脚本灵活
  4. perf + uprobes:系统自带,简单易用
  5. DTrace:如果使用 Solaris/FreeBSD/macOS

不需要 attach(可以重新启动程序)

  1. Intel Pin / DynamoRIO:需要详细分析时使用
  2. Valgrind Callgrind:需要调用图分析时使用
  3. LD_PRELOAD:简单场景,只统计库函数

统计指标对比

工具 调用次数 平均耗时 最小耗时 最大耗时 总耗时
eBPF/BCC
bptrace
SystemTap
perf + uprobes ⚠️ 需脚本 ⚠️ 需脚本 ⚠️ 需脚本 ⚠️ 需脚本
DTrace
Intel Pin
DynamoRIO
Valgrind Callgrind
LD_PRELOAD
ltrace

总结

对于 C/C++ 程序的动态插桩,推荐使用以下工具:

  1. 生产环境 + 需要 attachbptraceeBPF/BCCSystemTap
  2. 开发调试 + 详细分析Intel PinDynamoRIO
  3. 简单场景 + 库函数统计LD_PRELOAD
  4. 系统级分析perf + uprobes

选择工具时需要考虑:

  • 是否需要 attach 到运行中的进程
  • 对性能开销的容忍度
  • 需要统计的详细程度
  • 系统平台和权限限制
  • 耗时统计范围:大多数工具统计墙上时钟时间(包括 IO、sleep),只有 Valgrind Callgrind 统计 CPU 时间(不包括 IO、sleep)
  • 权限要求
    • 内核级插桩工具(perf、SystemTap、eBPF/BCC、bptrace)通常需要 root 权限或特殊 capabilities
    • 二进制插桩工具(Intel Pin、DynamoRIO、Valgrind)通常只需要普通用户权限
    • attach 到其他用户的进程需要 root 权限或 CAP_SYS_PTRACE 能力