引言

在生产环境中,我们经常面临这样的困境:某个内核路径偶发性地出现延迟尖刺,但复现频率极低,传统的 printk 日志要么信息不足,要么因为开启过多而导致系统负载加重。Linux 内核的 Tracepoints 机制正是为了解这种"零开销探针"场景而设计的——它们在关闭状态下仅有极低的执行成本(一条条件分支),一旦启用则能提供精确到函数参数级别的观测数据。

本文将深入解析 Linux 内核 Tracepoints 的完整技术栈:从 TRACE_EVENT 宏展开后的内存布局,到 Ring Buffer 的无锁设计;从 static probe 的调用约定到 ftrace 的 hist trigger 实战;以及 trace-cmd/kernelshark 在生产诊断中的应用。我们将以 x86_64 和 ARM64 双平台视角展开,覆盖 5.x/6.x 内核的关键差异。

一、Tracepoints 基础设施:从 DEFINE_TRACE 到 __do_trace

1.1 TRACE_EVENT 宏的展开艺术

TRACE_EVENT 是 Linux 内核中最复杂的宏之一,它通过 C 预处理器魔法将一个事件声明展开为多个代码路径:

// include/trace/events/sched.h
TRACE_EVENT(sched_switch,

    TP_PROTO(bool preempt,
         struct task_struct *prev,
         struct task_struct *next),

    TP_ARGS(prev, next, preempt),

    TP_STRUCT__entry(
        __field(    char,    comm[TASK_COMM_LEN]    )
        __field(    pid_t,    pid            )
        __field(    int,    prio            )
        __field(    long,    state            )
        __field(    char,    next_comm[TASK_COMM_LEN]    )
        __field(    pid_t,    next_pid        )
        __field(    int,    next_prio        )
    ),

    TP_fast_assign(
        __entry->prev_pid    = prev->pid;
        __entry->prev_prio    = prev->prio;
        __entry->prev_state    = prev_state;
        memcpy(__entry->prev_comm, prev->comm, TASK_COMM_LEN);
        __entry->next_pid    = next->pid;
        memcpy(__entry->next_comm, next->comm, TASK_COMM_LEN);
        __entry->next_prio    = next->prio;
    ),

    TP_printk("prev_pid=%d prev_prio=%d prev_state=%s ==> "
          "next_pid=%d next_prio=%d",
        __entry->prev_pid, __entry->prev_prio,
        __print_symbolic(__entry->prev_state,
                { 0x01, "S" }, { 0x02, "D" }, { 0x04, "T" }),
        __entry->next_pid, __entry->next_prio)
);

这个宏展开后生成以下关键结构:

  • trace_event_class:将同一子系统的事件(如sched_*)归类管理,对应一个 tracepoint 探测点表
  • trace_event_call:单个事件的运行时结构,包含函数指针表(enter/exit/probe)、event filter、perf 事件句柄
  • trace_event_raw_sched_switch:从 tracepoint 入口获取数据的函数骨架
  • __tracepoint_sched_switch:嵌入在代码段中的 tracepoint 符号,位于 __stop___tracepoints/__start___tracepoints 区间
  • __trace_sched_switch:实际的插桩点函数,包含条件跳转逻辑

1.2 静态插桩点的工作流程

在调用点处,代码被展开为如下模式(x86_64):

# 未启用时(热补丁后):
jmp    __tracepoint_sched_switch_next    ; 5字节 NOP
nop
nop
nop
nop

# 启用时(通过 ftrace 修改):
call    __traceiter_sched_switch        ; 保存参数并写入 ring buffer
# 原始代码继续执行

这种设计依赖 ftrace 的 mcount 机制(如果编译器支持 -mfentry):

// kernel/ftrace.c 中的核心逻辑
void __weak ftrace_trace_call(unsigned long ip, unsigned long parent_ip,
                  struct ftrace_ops *op, struct pt_regs *regs)
{
    struct ftrace_probe_ops *probe_ops;
    
    // 遍历注册在该 IP 上的所有 probe
    probe_ops = rcu_dereference_raw(*ip_probe_ops);
    while (probe_ops) {
        probe_ops->func(ip, parent_ip, probe_ops, regs);
        probe_ops = rcu_dereference_raw(probe_ops->next);
    }
}

1.3 tracepoint 模块热插拔

Tracepoints 通过 tracepoint_update_call() 实现动态注册/注销,它使用 RCU 机制保证安全:

// kernel/tracepoint.c
static void tracepoint_update_call(struct tracepoint *tp, void *priv,
                   enum tp_update_type type)
{
    // 获取所有注册的 probe
    probe_func *funcs = tp->probes;
    int num_probes = tp->num_probes;
    
    switch (type) {
    case TP_REGISTRATION:
        // 追加新的 probe 到链表
        new = krealloc(funcs, ...) 
        rcu_assign_pointer(tp->probes, new);
        break;
    case TP_UNREGISTRATION:
        // 通过 RCU 安全移除
        new = remove_from_array(funcs, probe);
        rcu_assign_pointer(tp->probes, new);
        break;
    }
    call_rcu(&old->rcu, free_old_probes);
}

二、Ring Buffer:内核中最快的 Trace 存储引擎

2.1 设计理念:无锁、Page-Based、时间有序

Linux 内核的 trace ring buffer 被称为 kernel ring buffer (kernel/trace/ring_buffer.c),其核心创新是:

  • Page-based 管理:以内存页为单位分配缓冲区,避免碎片化
  • 无锁写入:使用 cmpxchg 实现的生产者-消费者模型,每个 CPU 独立 buffer
  • 时间戳有序:通过时间戳合并(time stamp merge)保证跨 CPU 的输出顺序
  • 头部嵌入:每个页的头部嵌入 commit/overrun/timestamp 元数据

2.2 页内数据结构

// 每个 trace page 的布局
struct buffer_data_page {
    u64     time_stamp;    // 页首个事件的时间戳
    local_t        commit;        // 已提交的数据长度
    local_t        overrun;    // 溢出丢弃的事件数
    unsigned char    data[];        // 柔性数组,存放事件数据
} ____cacheline_aligned;

// 事件记录头
struct ring_buffer_event {
    u32        type_len:5,    // 事件类型(4 bits)或数据长度编码
        flags:2,
        preempt_count:4,
        reserved:21;
    u32        array[];    // 变长数据
};

type_len 字段的编码方式(4 bits 类型指示 + 变长长度):

  • type_len == 0:填充记录(padding),表示此处时间戳被嵌入
  • type_len == 1..28:数据长度,单位为 sizeof(long)
  • type_len == 29:时间戳扩展(array[0] 为扩展时间戳)
  • type_len == 30:数据超长(array[0] = 总长度,array[1..] 为实际数据)
  • type_len == 31:小时间戳差分(负向)

2.3 写入路径的无锁协议

// 简化的写入流程
1. reserve_page(page)
   - 获取 per_cpu buffer 的 write pointer
   - 如果当前页剩余空间不足 → 切换到下一个 page(head page)
   - cmpxchg 原子推进 write pointer

2. write_event(page, event)
   - 将事件头和 data 写入 page->data[offset]
   - 如果数据跨页 → 会自动 split

3. commit_page(page)
   - 使用 smp_wmb() 保证写入可见
   - commit += event_size
   - 此时事件对 reader 可见

4. 检查是否需要唤醒 reader(poll_wait 机制)

2.4 快照机制与 Instant Trace

Ring buffer 支持两种特殊模式:

  • Snapshot 模式:缓冲区回绕时,保留 N 秒前的数据(通过 SWAP 到 snap buffer)
  • Max latency 追踪:只记录最大延迟事件,丢弃常规数据
// 使用示例:通过 tracefs 启用 snapshot
echo 1 > /sys/kernel/debug/tracing/trace_options/snapshot
echo 1000000 > /sys/kernel/debug/tracing/buffer_size_kb
echo 1 > /sys/kernel/debug/tracing/tracing_on
# ... 模拟压力测试 ...
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/snapshot > /tmp/crash.dump

三、ftrace 函数追踪器:从 mcount 到函数图谱

3.1 ftrace 的三层架构

+--------------------------------------------------+
|             用户接口层 (tracefs)                    |
|  trace_options, set_ftrace_filter, function_profile |
+--------------------------------------------------+
|             调度层 (ftrace.c)                       |
|  register_ftrace_function(), ftrace_run_update_code  |
+--------------------------------------------------+
|            指令修改层 (arch specific)                 |
|  x86: ftrace_make_nop / ftrace_make_call            |
|  ARM64: aarch64_insn_gen_branch_imm                 |
+--------------------------------------------------+

3.2 函数入口热补丁机制

当内核启用 -pg 或 -mfentry 编译选项时,每个函数入口都有一条 NOP 指令:

// x86_64 下的典型函数入口(~5字节 NOP)
push   %rbp
mov    %rsp,%rbp
nop    nop    nop    nop    nop    # &__ftrace_call 注入点

启用函数追踪时,该 NOP 被动态替换为 <ftrace_caller>:

// ftrace_caller 伪代码
ftrace_caller:
    push   %rdi          # 保存第一个参数(用于探针)
    push   %rsi
    push   %rdx
    push   %rcx
    push   %r8
    push   %r9
    mov    RIP_AFTER_CALL,%rdi  # 当前函数地址
    mov    PARENT_IP,%rsi       # 调用方返回地址
    call   ftrace_ops_list_func  # 遍历所有注册的 ops
    # 恢复寄存器和执行原始代码
    pop    %r9
    ...
    ret

3.3 函数图谱追踪(Function Graph Tracer)

Function Graph Tracer 不仅追踪函数是否被调用,还能记录函数调用栈和时间戳:

// 启用 function_graph 追踪器
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo do_sys_openat2 > /sys/kernel/debug/tracing/set_graph_function
echo 1 > /sys/kernel/debug/tracing/tracing_on

# 输出示例(trace 文件)
# CPU  DURATION                  FUNCTION CALLS
# |     |   |                     |   |   |   |
 0)               |  do_sys_openat2() {
 0)   1.234 us    |    getname();
 0)   0.876 us    |    alloc_fd();
 0) + 12.456 us   |    do_filp_open();
 0) + 14.567 us   |  }

3.4 ftrace 的 filter 机制

ftrace 支持通配符过滤和正则匹配来减少追踪开销:

# 只追踪 scsi_ 开头的函数
echo 'scsi_*' > set_ftrace_filter

# 只追踪不在 net_ 中的函数 (NOTRACE)
echo 'net_*' > set_ftrace_notrace

# 函数图谱只追踪某函数子调用
echo 'tcp_sendmsg' > set_graph_function

# 基于 PID 过滤
echo 1234 > set_ftrace_pid

四、Hist Triggers 与 Synthetic Events:超越数据采集

4.1 Hist Triggers:内核内直方图统计

Hist triggers 允许在 Ring Buffer 层面直接聚合数据,避免用户空间后处理的开销:

# 创建 hist trigger:按时延统计上下文切换频率
echo 'hist:keys=prev_comm,next_comm:vals=hitcount,lat=common_timestamp.usecs' > \
  /sys/kernel/debug/tracing/events/sched/sched_switch/trigger

echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable

# 查看聚合结果(无需等待 trace 数据回绕)
cat /sys/kernel/debug/tracing/events/sched/sched_switch/hist

输出示例:

{ next_comm: swapper/0, prev_comm: nginx } hitcount=1245, lat: 1288 ulat: 1206
{ next_comm: postgres, prev_comm: swapper/2 } hitcount=892, lat: 756 ulat: 712
... (截断输出)

4.2 Synthetic Events:用户定再造事件

Linux 5.0 引入的 Synthetic Events 允许用户自定义"虚拟事件",由多个真实事件组合触发:

# 创建合成事件:将 sched_switch 和 wakeup 组合为 cross_latency
echo 'synthetic/wake_latency \
    u64 wake_t; \
    u64 sleep_ns;' \
    > /sys/kernel/debug/tracing/synthetic_events

# 关联:在 sched_wakeup 时记录 wake_t
echo 'hist:keys=pid:wake_t=common_timestamp.usecs \
    if comm=="my_server"' \
    > events/sched/sched_wakeup/trigger

# 关联:在 sched_switch 时计算 sleep_ns(sched_switch 到下次 wakeup 间隔)
echo 'hist:keys=pid,prio:\
    sleep_ns=common_timestamp.usecs-$wake_t \
    if comm=="my_server"' \
    > events/sched/sched_switch/trigger

# 将 hist 结果导入合成事件
echo 'hist:keys=pid,sleep_ns:\
    onmatch(sched.sched_switch).wake_latency($wake_t,sleep_ns) \
    if comm=="my_server"' \
    > events/sched/sched_switch/trigger

五、trace-cmd 与 KernelShark:生产级工具链

5.1 trace-cmd 核心用法

trace-cmd 是 ftrace 的命令行封装,支持跨主机分布式追踪和快照捕获:

# 基础追踪
trace-cmd start -p function_graph -g tcp_sendmsg
trace-cmd show > /tmp/trace.out

# 条件过滤
trace-cmd start -e sched_switch -f 'prev_prio < 120 && next_comm ~ "nginx*"'

# 差分分析(与基准对比)
trace-cmd start -e sched_switch
stress-ng --cpu 8 --timeout 120s
trace-cmd stop
trace-cmd extract -o /tmp/stress.dat
trace-cmd split -o /tmp/split/ -m 1000000  # 按文件大小分割

# 跨主机追踪(使用 trace-cmd listen)
# 在目标机:
trace-cmd listen -p 5678
# 在诊断机:
trace-cmd start --host target:5678 -e sched_switch

5.2 KernelShark 图形化分析

KernelShark 是 ftrace 的 GUI 分析工具,支持:

  • 多 CPU 时间轴同步显示
  • Task Interleave Analysis(同一任务在多个 CPU 上的调度)
  • Event Filter(正则+函数名匹配)
  • Markers(手动标注特定时间点)
  • 导出高分辨率 SVG 时序图
# 启动分析
kernelshark /tmp/stress.dat

# 加载多个 trace data 对比
kernelshark -m /tmp/baseline.dat /tmp/stress.dat

六、Tracepoints 与 eBPF/perf 的融合

6.1 从 Tracepoint 到 BPF Program

Tracepoints 天然支持 eBPF attach(类型为 BPF_PROG_TYPE_TRACEPOINT),使得 BPF 程序可以直接在静态探针点执行:

// BPF 程序(定义在 BPF 代码中)
SEC("tracepoint/sched/sched_switch")
int tracepoint__sched_sched_switch(struct trace_event_raw_sched_switch *ctx)
{
    u32 prev_pid = ctx->prev_pid;
    u32 next_pid = ctx->next_pid;
    
    // 使用 BPF 内置函数读用户态内存(无需 copy_to_user)
    bpf_probe_read_str(ctx->prev_comm, sizeof(ctx->prev_comm));
    
    // 推送到 BPF ring buffer(比 perf buffer 更高效)
    struct e *e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
    if (e) {
        e->prev = prev_pid;
        e->next = next_pid;
        bpf_ringbuf_submit(e, 0);
    }
    return 0;
}

6.2 Perf 与 Tracepoints 集成

Perf 可以将 tracepoints 作为采样点(sampling event):

# 使用 perf stat 统计调度次数
perf stat -e 'sched:sched_switch' -p 1234 sleep 10

# 使用 perf record 采样并生成火焰图
perf record -e sched:sched_switch -g -a sleep 30
perf script | stackcollapse-perf.pl | flamegraph.pl > sched_switch.svg

# 使用 perf trace 直接追踪
perf trace -e sched:sched_switch --call-graph=dwarf

6.3 Tracepoint vs kprobe 性能对比

维度Tracepointkprobes
性能开销(启用时)~50ns (x86_64)~200ns (使用 breakpoint)
静态注册编译期运行时动态
稳定性(跨内核)ABI 保证变化极小函数签名变化导致失效
C 参数获取TP_PROTO 宏自动生成需要 struct pt_regs 判读
BPF attachBPF_PROG_TYPE_TRACEPOINTBPF_PROG_TYPE_KPROBE
Reentrancy已知(有明确的 RCU 上下文)需注意中断禁用
覆盖数内核 1500+ 点几乎任意非内联函数

七、实战:生产环境延迟尖刺诊断

7.1 诊断框架

我们面对的问题:某金融交易系统偶发 50ms 延迟尖刺(99 分位),不可复现。

诊断步骤:
1. 使用 trace-cmd + snapshot 模式持续追踪调度器和块层事件
2. 当延迟尖刺发生时通过 GPIO 触发 snapshot
3. 用 KernelShark 分析 50ms 时间窗口内的调度/block/irq 事件
4. 使用合成事件关联关键时间戳
5. 用 BPF 程序自动捕获毫秒级统计数据

7.2 完整诊断脚本

#!/bin/bash
# setup_latency_trace.sh
SYSFS=/sys/kernel/debug/tracing

# Step 1: 配置环形缓冲区 + snapshot
echo 8192 > $SYSFS/buffer_size_kb
echo 1 > $SYSFS/trace_options/snapshot

# Step 2: 启用关键事件
echo 1 > $SYSFS/events/sched/sched_switch/enable
echo 1 > $SYSFS/events/sched/sched_wakeup/enable
echo 1 > $SYSFS/events/irq/irq_handler_entry/enable
echo 1 > $SYSFS/events/block/block_rq_issue/enable
echo 1 > $SYSFS/events/block/block_rq_complete/enable

# Step 3: 设置过滤器 - 只追踪关键 PID
echo 0 > $SYSFS/set_event_pid  # 先清空
echo $TRADING_PID >> $SYSFS/set_event_pid

# Step 4: 添加 synthetic 延迟统计
echo 'hist:keys=common_pid:lat=common_timestamp.usecs-$last_ts' \
    > $SYSFS/events/sched/sched_switch/trigger
echo 'hist:keys=common_pid:last_ts=common_timestamp.usecs' \
    > $SYSFS/events/sched/sched_switch/trigger

# Step 5: 启动追踪
echo 1 > $SYSFS/tracing_on

echo "跟踪已启动。PID: $TRADING_PID"
echo "尖刺发生后执行: echo 1 > $SYSFS/snapshot"

7.3 BPF 监控程序实时统计

// latency_monitor.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, u32);   // cpu
    __type(value, u64); // last sched time
    __uint(max_entries, 512);
} last_time SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __type(key, u32);   // bucket index
    __type(value, u64); // count
    __uint(max_entries, 64);
} hist SEC(".maps");

SEC("tp_btf/sched_switch")
int BPF_PROG(trace_sched_switch, bool preempt,
         struct task_struct *prev, struct task_struct *next)
{
    u64 now = bpf_ktime_get_ns();
    u32 cpu = bpf_get_smp_processor_id();
    u64 *last = bpf_map_lookup_elem(&last_time, &cpu);
    u64 delta = last ? now - *last : 0;
    
    // 更新直方图(10us 一档,64 档覆盖 640us)
    u32 bucket = delta / 10000;
    if (bucket >= 64) bucket = 63;
    u64 *count = bpf_map_lookup_elem(&hist, &bucket);
    if (count) __sync_fetch_and_add(count, 1);
    
    if (last) *last = now;
    return 0;
}

char _license[] SEC("license") = "GPL";

八、高级主题:Tracepoint 扩展与未来演进

8.1 自定义 Tracepoint 开发

驱动开发者可以创建子系统专属的 tracepoint:

// my_driver.h
#undef TRACE_SYSTEM
#define TRACE_SYSTEM my_driver

#if !defined(_TRACE_MY_DRIVER_H) || defined(TRACE_HEADER_MULTI_READ)
#define _TRACE_MY_DRIVER_H

#include <linux/tracepoint.h>

TRACE_EVENT(my_driver_dma_complete,
    TP_PROTO(struct device *dev, dma_addr_t handle, int status),
    TP_ARGS(dev, handle, status),
    
    TP_STRUCT__entry(
        __string(    dev_name,    dev_name(dev))
        __field(    dma_addr_t,    handle)
        __field(    int,        status)
    ),
    
    TP_fast_assign(
        __assign_str(dev_name, dev_name(dev));
        __entry->handle = handle;
        __entry->status = status;
    ),
    
    TP_printk("dev=%s handle=%pad status=%d",
          __get_str(dev_name), &__entry->handle, __entry->status)
);

#endif /* _TRACE_MY_DRIVER_H */

// 确保只包含一次
#undef TRACE_INCLUDE_FILE
#define TRACE_INCLUDE_FILE my_driver_trace
#undef TRACE_INCLUDE_PATH
#define TRACE_INCLUDE_PATH .
#include <trace/define_trace.h>

8.2 LTTng 与内核 Tracepoints 对比

维度ftrace TracepointsLTTng
缓冲区全局环形缓冲区(per-cpu)per-channel + per-cpu buffer
元数据编译期生成(events/)运行时 TTL/JSON 序列化
用户态支持有限(需 uST 插件)原生(lttng-ust)
控制接口procfs/tracefs命令行客户端(lttng)
性能开销~25ns (无 probe)~10ns (无 probe)
分布式追踪trace-cmd listen中继 daemon + 会话管理
数据导出text/raw/xmlCTF (Common Trace Format)

8.3 内核 Tracepoint 演进趋势

  • BPF 取代 reader 阶段:通过 BPF_MAP_TYPE_RING_BUF 替代传统 read() 流,实现零拷贝采集
  • Telemetry 框架合并:2024 年内核社区讨论将 eBPF/extrace/tracepoint 整合为统一的 telemetry 子系统
  • 硬实时支持:RT 内核中 tracepoints 已经实现不可抢占区最小化,保证 preempt-rt 延迟确定性

九、总结与工程检查清单

Tracepoints 作为 Linux 内核静态探针的三大支柱之一(另两个是 kprobe 和 perf event),具有不可替代的工程价值:

  • 零成本基线:未启用时仅一条条件分支 + 5 字节 NOP,对生产性能影响可忽略
  • ABI 稳定:面向用户的 tracepoint API 保证跨内核小版本兼容,适合监控探针长期维护
  • 生态完整:toolchain(trace-cmd/kernelshark)+ 可视化(CTF/perf)+ 编程(eBPF)形成闭环

生产诊断检查清单:

  1. 确认目标系统 tracefs 挂载:mount -t debugfs none /sys/kernel/debug
  2. 使用 trace-cmd check 验证所有依赖(libtracefs/libtraceevent)
  3. 首选 tracepoint 而非 kprobe 获取函数级数据(稳定性更好)
  4. 使用 hist trigger 做聚合统计,避免 userland 后处理开销
  5. 高负载环境优先使用 BPF ring buffer 而非 perf buffer
  6. 将自定义 tracepoint 与 sysfs/proc 数据源结合构建全栈可观测性
  7. 保留 snapshot buffer,捕获偶发性延迟事件而非完整 trace

通过将 Tracepoints 与 eBPF、perf 等工具组合使用,我们可以构建一套覆盖"宏观聚合统计 + 微观事件追踪"的全栈诊断体系,为生产环境中的偶发性延迟问题提供从发现问题到定位根因的完整链路。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部