4. 系统调用探针入门:tracepoint:syscalls:sys_enter_* 和 sys_exit_* 探针的使用方法
系统调用是用户态程序请求内核服务的唯一入口。在 Linux 内核中,每个系统调用都对应一对静态 tracepoint:sys_enter_* 和 sys_exit_*。bpftrace 通过挂载这些 tracepoint,可以在不修改内核代码、不重启系统的情况下,精确捕获每次系统调用的进入和返回时刻,从而计算微秒级延迟。
4.1 tracepoint:syscalls 探针体系
内核 tracepoint 子系统在 /sys/kernel/debug/tracing/events/syscalls/ 目录下暴露了所有系统调用的探针事件。每个系统调用(如 read、write、open)都有两个子事件:
| 探针事件 | 触发时机 | 可用字段 | 典型用途 |
|---|---|---|---|
sys_enter_<syscall> |
用户态进入内核态,系统调用处理函数开始执行前 | id(系统调用号)、args(参数数组) |
记录开始时间戳、捕获传入参数 |
sys_exit_<syscall> |
系统调用处理函数返回,即将切回用户态前 | ret(返回值,通常为 long 类型) |
记录结束时间戳、捕获返回值/错误码 |
提示: 使用
bpftrace -l 'tracepoint:syscalls:sys_enter_*' 可以列出当前内核支持的所有系统调用 enter 探针。不同内核版本支持的 syscall 列表可能略有差异。
4.2 探针命名与匹配模式
bpftrace 支持通配符匹配,可以一次性挂载多个系统调用探针:
- 精确匹配:
tracepoint:syscalls:sys_enter_read—— 只捕获read系统调用。 - 通配符匹配:
tracepoint:syscalls:sys_enter_*—— 捕获所有系统调用的 enter 事件。 - 前缀匹配:
tracepoint:syscalls:sys_enter_open*—— 捕获open、openat等以 open 开头的系统调用。 - 批量匹配:
tracepoint:syscalls:sys_{enter,exit}_read—— 同时捕获read的 enter 和 exit 事件。
最佳实践: 在性能分析初期,建议使用通配符
tracepoint:syscalls:sys_enter_* 进行全量采样,然后根据热点系统调用缩小范围,避免探针过多导致性能开销过大。
4.3 延迟测量核心模式:时间戳配对
测量系统调用延迟的基本思路是在 sys_enter 中记录开始时间,在 sys_exit 中计算差值。bpftrace 提供 nsecs 内置变量获取纳秒级时间戳。
#!/usr/bin/env bpftrace
// 声明一个哈希映射,用于存储每个线程的 enter 时间戳
BEGIN
{
printf("Tracing syscall latency for read/write... Hit Ctrl-C to exit.\n");
}
tracepoint:syscalls:sys_enter_read,
tracepoint:syscalls:sys_enter_write
{
// 以 线程ID 为键,存储进入时的纳秒时间戳
@enter[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_read,
tracepoint:syscalls:sys_exit_write
{
$delta_ns = nsecs - @enter[tid];
$delta_us = $delta_ns / 1000;
// 只打印延迟超过 10 微秒的调用
if ($delta_us > 10) {
printf("[%s] PID=%d TID=%d RET=%d LATENCY=%llu us\n",
probe, pid, tid, args->ret, $delta_us);
}
// 清理映射,防止内存泄漏
delete(@enter[tid]);
}
END
{
clear(@enter);
}
注意: 必须确保 enter 和 exit 探针使用相同的键(通常是
tid 或 pid)来配对。如果使用 pid,在多线程程序中,同一进程的不同线程会互相覆盖时间戳,导致延迟计算错误。推荐使用 tid(内核线程ID)作为键。
4.4 系统调用延迟测量流程图
4.5 常用字段与内置变量
在 sys_enter_* 和 sys_exit_* 探针中,bpftrace 提供了丰富的上下文信息:
| 变量/字段 | 类型 | 说明 | 可用探针 |
|---|---|---|---|
nsecs |
内置变量 | 当前纳秒时间戳(单调时钟) | enter / exit |
tid |
内置变量 | 内核线程 ID(推荐作为配对键) | enter / exit |
pid |
内置变量 | 进程 ID(多线程时注意冲突) | enter / exit |
args->ret |
字段 | 系统调用返回值(成功为正数,失败为负 errno) | exit |
args->args[0..5] |
字段 | 系统调用参数(最多 6 个,具体含义依赖 syscall) | enter |
probe |
内置变量 | 当前探针全名(如 tracepoint:syscalls:sys_enter_read) |
enter / exit |
comm |
内置变量 | 当前进程名(最多 16 字节) | enter / exit |
4.6 实战:测量所有系统调用的延迟分布
以下脚本使用通配符捕获所有系统调用,并输出延迟直方图:
#!/usr/bin/env bpftrace
BEGIN
{
printf("Tracing ALL syscalls latency... Hit Ctrl-C to exit.\n");
}
tracepoint:syscalls:sys_enter_*
{
@enter[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_*
{
$delta_ns = nsecs - @enter[tid];
$delta_us = $delta_ns / 1000;
// 按系统调用名称分组统计延迟直方图
@latency[probe] = hist($delta_us);
// 记录总调用次数
@count[probe]++;
delete(@enter[tid]);
}
END
{
printf("\n=== Latency Histogram by Syscall ===\n");
print(@latency);
printf("\n=== Call Count by Syscall ===\n");
print(@count);
clear(@enter);
clear(@latency);
clear(@count);
}
性能注意事项: 全量捕获所有系统调用会产生较大的性能开销(尤其是在高 IOPS 场景下)。建议在生产环境中先使用
syscalls:sys_enter_* 采样 10-30 秒,识别出热点系统调用后,再针对性地只跟踪那几个高频调用。
4.7 常见问题与避坑指南
- 时间戳溢出:
nsecs是 64 位无符号整数,理论上不会溢出。但如果在 enter 中记录了时间戳,而对应的 exit 探针由于某种原因(如进程被 kill)没有触发,会导致映射项残留。建议在END块中执行clear()清理。 - 返回值解析:
args->ret是 long 类型,负值表示错误。可以使用- args->ret转换为正数的 errno 值,然后对照/usr/include/asm-generic/errno-base.h查看具体错误。 - 参数含义: 不同系统调用的
args->args[]含义不同。例如read(fd, buf, count)中args->args[0]是文件描述符,args->args[2]是读取字节数。建议查阅对应系统调用的 man 手册。 - 探针冲突: 如果同时使用
sys_enter_*和sys_exit_*通配符,bpftrace 会自动为每个匹配的 syscall 创建一对探针。但如果手动指定了某个 syscall 的 enter 和 exit,又使用了通配符,会导致重复挂载,bpftrace 会报错退出。