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 系统调用延迟测量流程图

系统调用延迟测量流程图(tracepoint 配对模式) 用户态应用程序 sys_enter_read/write 记录 @enter[tid] = nsecs 内核系统调用处理 执行实际功能 sys_exit_read/write 计算延迟 = nsecs - @enter[tid] 返回用户态 输出延迟 > 10us 的记录 清理映射 delete(@enter[tid]) 关键配对规则 1. enter 和 exit 探针必须使用相同的键(tid)进行时间戳配对 2. 在 exit 探针中计算延迟后,必须 delete 映射项,防止内存泄漏 3. 延迟单位:nsecs 为纳秒,除以 1000 得到微秒(us) 延迟分布统计(直方图示例) @latency_us = hist($delta_us); // bpftrace 内置直方图函数 输出格式:每 2 的幂次一个桶,如 1us、2us、4us、8us…… 便于识别延迟分布特征

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 会报错退出。

4.8 小结