Skip to content

Latest commit

 

History

History
2409 lines (1922 loc) · 95.9 KB

File metadata and controls

2409 lines (1922 loc) · 95.9 KB

Linux 追踪与可观测性子系统深度解析

基于 Linux Kernel 源码深度分析(内核版本 6.x) 核心路径:kernel/trace/ | kernel/kprobes.c | include/linux/tracepoint.h include/linux/kprobes.h | include/linux/perf_event.h | include/linux/uprobes.h


目录

  1. 追踪子系统全景架构
  2. ftrace 框架深度解析
  3. ring buffer:无锁环形缓冲区实现
  4. kprobe 与 kretprobe:内核动态探针
  5. tracepoint:静态追踪点机制
  6. perf events 框架
  7. uprobe:用户态动态探针
  8. eBPF 追踪程序
  9. 动态调试(dyndbg)
  10. 系统整体可观测性工具链
  11. 子系统协作与数据流
  12. 生产实践与调优建议

1. 追踪子系统全景架构

1.1 四大支柱概述

Linux 追踪与可观测性体系经历了 20 年的演进,由四个相互正交又彼此协作的核心子系统组成。每个子系统有其独特的插桩机制和数据路径,但最终都汇聚到统一的 ring buffer 存储层和用户空间接口。

╔══════════════════════════════════════════════════════════════════════════╗
║                         用 户 空 间 工 具 层                              ║
║                                                                          ║
║  ┌─────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌──────────────┐  ║
║  │  perf   │ │ bpftrace │ │ trace-cmd │ │ systemtap│ │  BCC / BPF   │  ║
║  └────┬────┘ └─────┬────┘ └─────┬─────┘ └────┬─────┘ └──────┬───────┘  ║
╚═══════╪═════════════╪═══════════╪════════════╪══════════════╪══════════╝
        │             │           │            │              │
        ▼             ▼           ▼            ▼              ▼
╔══════════════════════════════════════════════════════════════════════════╗
║                         内 核 接 口 层                                    ║
║                                                                          ║
║  /sys/kernel/tracing/    perf_event_open()    /sys/kernel/debug/        ║
║  tracefs 虚拟文件系统      系统调用              debugfs 文件系统          ║
╚═══╤════════════════════════════════╤═══════════════════╤════════════════╝
    │                                │                   │
    ▼                                ▼                   ▼
╔═══════════╗  ╔═══════════╗  ╔═══════════╗  ╔════════════════════════╗
║   ftrace  ║  ║  kprobe   ║  ║  uprobe   ║  ║    perf_events (PMU)  ║
║  function ║  ║ kretprobe ║  ║ uretprobe ║  ║  hardware / software  ║
║  graph    ║  ║           ║  ║           ║  ║  tracepoint events    ║
╚═══╤═══════╝  ╚═════╤═════╝  ╚═════╤═════╝  ╚══════════╤═════════════╝
    │                │               │                    │
    ▼                ▼               ▼                    ▼
╔══════════════════════════════════════════════════════════════════════════╗
║                      tracepoint(静态插桩层)                             ║
║              DEFINE_TRACE / DECLARE_TRACE 宏展开生成                     ║
║              static_key 零开销门控,未激活时完全无代价                    ║
╚═══════════════════════════════╤══════════════════════════════════════════╝
                                │
                                ▼
╔══════════════════════════════════════════════════════════════════════════╗
║                  ring buffer(per-CPU 无锁环形缓冲区)                    ║
║         kernel/trace/ring_buffer.c  —  lock-free, NMI-safe             ║
╚══════════════════════════════════════════════════════════════════════════╝

1.2 各子系统定位对比

子系统 插桩时机 开销 灵活性 典型场景
ftrace 编译期(mcount/fentry 桩) 极低 函数调用追踪、延迟分析
tracepoint 编译期(静态插桩点) 近零(未启用时) 预定义事件、稳定 ABI
kprobe 运行时(断点替换) 低~中 极高 任意内核地址探测
uprobe 运行时(用户空间断点) 用户程序函数追踪
perf events 硬件 PMU / 软件计数器 硬件级 性能计数、采样分析
eBPF 运行时(JIT 编译附着) 极高 可编程追踪逻辑

1.3 历史演进脉络

  • 2008:Steven Rostedt 引入 ftrace(kernel/trace/),同年 ring buffer 重写
  • 2009:kprobe 正式进入主线(IBM 贡献,最初 2002 年原型)
  • 2012:uprobe 进入主线(kernel/uprobes.c
  • 2012:perf 子系统成熟,perf_event_open() 系统调用稳定
  • 2014:tracepoint 与 eBPF 的集成开始
  • 2015:eBPF 进入主线(kernel/bpf/),kprobe BPF 程序支持
  • 2020BPF_MAP_TYPE_RINGBUF 新 ring buffer 接口引入
  • 2022:fentry/fexit BPF 程序,性能大幅提升

2. ftrace 框架深度解析

2.1 核心数据结构

ftrace 的顶层数据结构是 struct trace_array,每个追踪实例(instance)对应一个,定义在 kernel/trace/trace.h:331

// kernel/trace/trace.h:331
struct trace_array {
    struct list_head    list;        // 链入全局 ftrace_trace_arrays
    char                *name;       // 实例名(NULL 表示全局实例)
    struct array_buffer array_buffer; // 主 ring buffer 封装
    struct array_buffer snapshot_buffer; // 快照 buffer(CONFIG_TRACER_SNAPSHOT)

    /* 内存映射 ring buffer 相关 */
    unsigned int        mapped;
    unsigned long       range_addr_start;
    unsigned long       range_addr_size;

    /* PID 过滤 */
    struct trace_pid_list __rcu *filtered_pids;
    struct trace_pid_list __rcu *filtered_no_pids;

    /* 函数追踪器相关 */
    struct ftrace_ops   *ops;
    struct trace_pid_list __rcu *function_pids;
    struct trace_pid_list __rcu *function_no_pids;

    /* 当前追踪器 */
    struct tracer       *current_trace;
    u64                 trace_flags;

    /* tracefs 目录项 */
    struct dentry       *dir;
    struct dentry       *options;
    struct eventfs_inode *event_dir;
    ...
    bool ring_buffer_expanded;
};

array_buffer 是 ring buffer 的封装,定义在 kernel/trace/trace.h:217

// kernel/trace/trace.h:217
struct array_buffer {
    struct trace_array      *tr;
    struct trace_buffer     *buffer;     // 指向实际的 ring buffer
    struct trace_array_cpu __percpu *data; // per-CPU 统计数据
    u64                     time_start;
    int                     cpu;
};

每个 CPU 的追踪状态由 struct trace_array_cpu 维护(kernel/trace/trace.h:191):

// kernel/trace/trace.h:191
struct trace_array_cpu {
    local_t         disabled;          // 禁用计数(原子)

    unsigned long   entries;           // 已记录条目数
    unsigned long   saved_latency;
    unsigned long   critical_start;
    unsigned long   critical_end;
    unsigned long   skipped_entries;   // 因 buffer 满跳过的条目数
    u64             preempt_timestamp;
    pid_t           pid;
    kuid_t          uid;
    char            comm[TASK_COMM_LEN];

    int             ftrace_ignore_pid; // PID 过滤控制
    bool            ignore_pid;
};

2.2 追踪类型枚举

kernel/trace/trace.h:37 定义了所有追踪事件的类型:

// kernel/trace/trace.h:37
enum trace_type {
    __TRACE_FIRST_TYPE = 0,

    TRACE_FN,           // 函数调用(function tracer)
    TRACE_CTX,          // 上下文切换
    TRACE_WAKE,         // 唤醒事件
    TRACE_STACK,        // 栈追踪
    TRACE_PRINT,        // trace_printk() 输出
    TRACE_BPRINT,       // 二进制 printk
    TRACE_MMIO_RW,      // MMIO 读写
    TRACE_MMIO_MAP,     // MMIO 映射
    TRACE_BRANCH,       // 分支预测追踪
    TRACE_GRAPH_RET,    // function_graph 返回
    TRACE_GRAPH_ENT,    // function_graph 进入
    TRACE_USER_STACK,   // 用户栈追踪
    TRACE_BLK,          // 块设备 I/O
    TRACE_BPUTS,        // BPF 字符串输出
    TRACE_HWLAT,        // 硬件延迟
    TRACE_OSNOISE,      // OS 噪声
    TRACE_TIMERLAT,     // 定时器延迟
    TRACE_RAW_DATA,     // 原始数据
    TRACE_FUNC_REPEATS, // 连续重复函数调用(压缩记录)

    __TRACE_LAST_TYPE,
};

2.3 mcount/fentry 插桩机制

ftrace 的核心魔法在于编译期插桩。GCC/Clang 在编译内核时,通过 -pg(mcount)或 -mfentry(fentry)选项,在每个函数入口插入一条 call __fentry__ 指令。

                    函数编译输出(x86-64,使用 fentry)
    ┌─────────────────────────────────────────────────────────┐
    │  <some_kernel_func>:                                    │
    │    0:  e8 00 00 00 00    call   <__fentry__>    ◄── 插桩点│
    │    5:  55                push   %rbp                    │
    │    6:  48 89 e5          mov    %rsp,%rbp               │
    │    ...                                                  │
    └─────────────────────────────────────────────────────────┘

                    ftrace 未激活时(动态 NOP 替换)
    ┌─────────────────────────────────────────────────────────┐
    │  <some_kernel_func>:                                    │
    │    0:  0f 1f 44 00 00    nop (5 bytes)         ◄── NOP  │
    │    5:  55                push   %rbp                    │
    │    ...                                                  │
    └─────────────────────────────────────────────────────────┘

                    ftrace 激活后(NOP 改回 call)
    ┌─────────────────────────────────────────────────────────┐
    │  <some_kernel_func>:                                    │
    │    0:  e8 XX XX XX XX    call   <ftrace_caller>  ◄── 重激活│
    │    5:  55                push   %rbp                    │
    │    ...                                                  │
    └─────────────────────────────────────────────────────────┘

CONFIG_DYNAMIC_FTRACE 是关键:系统启动时,内核扫描 __mcount_loc section 中记录的所有插桩位置,将 call __fentry__ 批量替换为 5 字节 NOP,实现零开销。当用户激活某个追踪器时,再用原子指令将对应位置改回 call

x86 的 ftrace_regs 机制include/linux/ftrace_regs.h):

被调函数的 pt_regs 寄存器状态会被保存到 struct ftrace_regs,供 handler 访问和修改参数。这是 live patch(kpatch)实现函数替换的基础。

2.4 function_graph tracer

function_graph 追踪器在函数入口和出口都插入钩子,实现调用图记录:

trace_type 对应:
  TRACE_GRAPH_ENT  → kernel/trace/trace.h:50  (进入)
  TRACE_GRAPH_RET  → kernel/trace/trace.h:49  (返回)

数据结构:
  struct fgraph_entry { unsigned long func; }    // 记录进入的函数地址
  struct fgraph_ret   { unsigned long func;
                        unsigned long overrun;
                        unsigned long long calltime;
                        unsigned long long rettime; }

function_graph 的输出示例:

 0)               |  do_sys_open() {
 0)               |    getname() {
 0)   1.234 us    |      kmem_cache_alloc();
 0)   0.890 us    |      __alloc_pages();
 0)   3.456 us    |    } /* getname */
 0)               |    ...
 0)  45.678 us    |  } /* do_sys_open */

2.5 动态过滤:set_ftrace_filter

用户通过 tracefs 接口控制过滤,核心文件参考 kernel/trace/trace.c:4746

/sys/kernel/tracing/
├── available_tracers      # 可用追踪器列表
├── current_tracer         # 当前活动的追踪器
├── set_ftrace_filter      # 白名单过滤(只追踪这些函数)
├── set_ftrace_notrace     # 黑名单过滤(不追踪这些函数)
├── set_ftrace_pid         # 只追踪特定 PID
├── set_graph_function     # function_graph 起始点
├── trace                  # 读取追踪数据
├── trace_pipe             # 流式读取(消费式)
├── tracing_on             # 追踪开关
├── trace_clock            # 时钟源选择
└── instances/             # 独立追踪实例(多租户)
    └── my_instance/
        ├── trace
        ├── set_ftrace_filter
        └── ...

过滤机制实现:

写入 set_ftrace_filter 时的内核路径:
  tracefs_write → ftrace_filter_write()
               → ftrace_set_filter()          # kernel/trace/fprobe.c:309
               → ftrace_hash_move_and_update_ops()
               → ftrace_update_code()         # 批量 NOP/call 替换

支持 glob 匹配:
  echo 'tcp_*' > set_ftrace_filter     # 匹配所有 tcp_ 前缀函数
  echo '!schedule' >> set_ftrace_filter # 排除 schedule
  echo 'vfs_*:mod:ext4' > set_ftrace_filter # 仅 ext4 模块的 vfs_ 函数

2.6 tracefs 接口详解

tracefs 是独立于 debugfs 的虚拟文件系统,挂载点 /sys/kernel/tracing(或旧版 /sys/kernel/debug/tracing):

tracefs 关键控制文件语义:

tracing_on:
  echo 1 > tracing_on    # 开启追踪
  echo 0 > tracing_on    # 暂停追踪(不清除 buffer)

trace_clock:
  local   - 每 CPU 本地时钟,最快,无法跨 CPU 比较
  global  - 全局时钟,有锁竞争
  counter - 单调递增计数器,用于相对顺序
  uptime  - 系统运行时间(jiffies)
  perf    - perf 时钟源

buffer_size_kb:
  echo 4096 > buffer_size_kb  # 设置每 CPU buffer 大小为 4MB

snapshot:
  echo 1 > snapshot      # 触发快照(保存当前 buffer 到 snapshot_buffer)
  cat snapshot           # 读取快照数据

2.7 多实例(instances)机制

Linux 3.x 引入 instances 机制,允许多个独立的追踪会话并发运行,互不干扰:

mkdir /sys/kernel/tracing/instances/net_debug
echo 1 > /sys/kernel/tracing/instances/net_debug/tracing_on
echo 'tcp_*' > /sys/kernel/tracing/instances/net_debug/set_ftrace_filter
echo function > /sys/kernel/tracing/instances/net_debug/current_tracer
cat /sys/kernel/tracing/instances/net_debug/trace

每个 instance 对应一个独立的 struct trace_arraykernel/trace/trace.h:331),拥有独立的 ring buffer、过滤器和追踪器状态。

2.8 FTRACE_ENTRY 宏与追踪记录格式

所有追踪记录都通过 FTRACE_ENTRY 宏定义(kernel/trace/trace.h:108):

// kernel/trace/trace.h:108
#define FTRACE_ENTRY(name, struct_name, id, tstruct, print)  \
    struct struct_name {                                      \
        struct trace_entry  ent;   // 公共头部(type + time + pid + cpu)
        tstruct                    // 具体字段
    }

kprobe 的追踪记录头定义(kernel/trace/trace.h:156):

// kernel/trace/trace.h:156
struct kprobe_trace_entry_head {
    struct trace_entry  ent;
    unsigned long       ip;    // 探针地址
};

// kernel/trace/trace.h:165
struct kretprobe_trace_entry_head {
    struct trace_entry  ent;
    unsigned long       func;     // 被探测函数地址
    unsigned long       ret_ip;   // 返回地址
};

3. ring buffer:无锁环形缓冲区实现

3.1 设计目标与约束

kernel/trace/ring_buffer.c 实现了一个满足以下约束的高性能环形缓冲区:

  • NMI 安全:可在 NMI 上下文写入,不能使用任何锁
  • per-CPU:每个 CPU 独立 buffer,写入无竞争
  • 多读者:支持多个并发读者
  • 溢出策略:覆盖模式(circular)或丢弃模式(discard)
  • 时间戳压缩:delta 时间戳节省空间

3.2 物理结构:子缓冲区(subbuf)链

ring_buffer 物理组织(per-CPU):

  ┌─────────────────────────────────────────────────────────────────┐
  │                    struct trace_buffer                          │
  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐            │
  │  │  CPU 0 buf  │  │  CPU 1 buf  │  │  CPU N buf  │   ...      │
  │  └──────┬──────┘  └─────────────┘  └─────────────┘            │
  └─────────│───────────────────────────────────────────────────────┘
            │
            ▼  每个 CPU buffer 由多个 subbuf(页)组成
  ┌─────────────────────────────────────────────────────────────────┐
  │  subbuf 0    subbuf 1    subbuf 2    subbuf 3    subbuf N      │
  │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐  │
  │ │ header  │ │ header  │ │ header  │ │ header  │ │ header  │  │
  │ │ ─────── │ │ ─────── │ │ ─────── │ │ ─────── │ │ ─────── │  │
  │ │ event 1 │ │ event 4 │ │ event 7 │ │ event10 │ │ ...     │  │
  │ │ event 2 │ │ event 5 │ │ event 8 │ │         │ │         │  │
  │ │ event 3 │ │ event 6 │ │ event 9 │ │         │ │         │  │
  │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘  │
  │     ▲ tail(写入位置)                                          │
  │                                  ▲ head(读取位置)             │
  └─────────────────────────────────────────────────────────────────┘

3.3 ring buffer 元数据

kernel/trace/ring_buffer.c:51 定义了持久化元数据(用于 boot-time tracing):

// kernel/trace/ring_buffer.c:51
struct ring_buffer_meta {
    int         magic;          // RING_BUFFER_META_MAGIC = 0xBADFEED
    int         struct_sizes;
    unsigned long total_size;
    unsigned long buffers_offset;
};

// kernel/trace/ring_buffer.c:58
struct ring_buffer_cpu_meta {
    unsigned long   first_buffer;   // 第一个 subbuf 地址
    unsigned long   head_buffer;    // 读者当前位置
    unsigned long   commit_buffer;  // 最后提交位置
    __u32           subbuf_size;    // 单个 subbuf 大小
    __u32           nr_subbufs;     // subbuf 总数
    int             buffers[];      // subbuf 偏移数组
};

3.4 事件头部格式(压缩编码)

每个 ring buffer 事件都有一个紧凑的头部,由 ring_buffer_print_entry_header() 描述(kernel/trace/ring_buffer.c:70):

ring buffer 事件头格式(32 位 = 4 字节):

  ┌────────────────────────────────────────────────────────┐
  │  bits [4:0]   - type_len (5 bits)                     │
  │  bits [31:5]  - time_delta (27 bits)                  │
  └────────────────────────────────────────────────────────┘

  如果 type_len == RINGBUF_TYPE_TIME_EXTEND (29):
  ┌────────────────────────────────────────────────────────┐
  │  第一个 32 位:type=29 + time_delta 低 27 位           │
  │  第二个 32 位:time_delta 高 32 位                     │
  └────────────────────────────────────────────────────────┘

  如果 type_len == RINGBUF_TYPE_TIME_STAMP (30):
  ┌────────────────────────────────────────────────────────┐
  │  完整 64 位绝对时间戳(59 位有效,5 MSB 保留)          │
  │  kernel/trace/ring_buffer.c:44: #define TS_MSB (0xf8ULL << 56) │
  └────────────────────────────────────────────────────────┘

type_len 语义:
  0              : 数据事件(长度编码在 time_delta 字段中)
  1~28           : 数据长度(以 4 字节为单位,最大 28*4=112 字节)
  RINGBUF_TYPE_PADDING (29): 填充
  RINGBUF_TYPE_TIME_EXTEND (30): 时间扩展
  RINGBUF_TYPE_TIME_STAMP (31): 绝对时间戳

3.5 无锁写入机制

ring buffer 的写入使用 local_t(per-CPU 原子变量,基于 asm/local.h),避免使用全局锁:

写入流程(NMI 安全,kernel/trace/ring_buffer.c 实现):

  1. local_add(&cpu_buffer->tail, event_size)
     获取写入位置,使用 cmpxchg 保证原子性

  2. 检查是否越过 subbuf 边界
     - 是:分配新 subbuf(或覆盖最旧 subbuf)
     - 否:在当前 subbuf 内写入

  3. 写入事件头(type_len + time_delta)

  4. 写入事件数据

  5. 提交:local_set(&cpu_buffer->commit, new_tail)

  关键特性:
  - 写入期间不需要任何锁(lock-free,但需要禁止抢占)
  - 在中断或 NMI 中可以安全调用
  - 读者页(reader page)与写者页分离,通过页指针交换实现

3.6 读者页机制

读者获取数据的流程:

  ┌──────────────────────────────────────────────────────────────┐
  │  每个 CPU buffer 有一个专用的 reader page                    │
  │                                                              │
  │  ring buffer 页链:[subbuf0] → [subbuf1] → [subbuf2] → ...  │
  │                                    ▲                         │
  │                              tail(写入端)                   │
  │                                                              │
  │  reader page(专用):                                       │
  │  ┌──────────────┐                                           │
  │  │  reader page │ ← 读者独占此页,writer 永不写入            │
  │  └──────────────┘                                           │
  │                                                              │
  │  当 reader page 读完后:                                     │
  │  1. 通过 cmpxchg 将 reader page 与 head subbuf 指针交换     │
  │  2. 旧 head subbuf 成为新 reader page                       │
  │  3. 旧 reader page 加入 ring buffer 循环(被写者复用)       │
  └──────────────────────────────────────────────────────────────┘

4. kprobe 与 kretprobe:内核动态探针

4.1 kprobe 结构体解析

include/linux/kprobes.h:59 定义了核心结构:

// include/linux/kprobes.h:59
struct kprobe {
    struct hlist_node hlist;    // 哈希表链接(kprobe_table)

    /* 支持同一地址多 handler:聚合 kprobe */
    struct list_head list;

    unsigned long nmissed;      // 因不可重入等原因错过的命中次数

    kprobe_opcode_t *addr;      // 探针位置(内核虚拟地址)
    const char *symbol_name;    // 符号名(可替代 addr)
    unsigned int offset;        // 符号偏移量

    /* 回调函数 */
    kprobe_pre_handler_t  pre_handler;   // 探针触发前调用
    kprobe_post_handler_t post_handler;  // 探针触发后调用(单步后)

    kprobe_opcode_t opcode;              // 被替换的原始字节
    struct arch_specific_insn ainsn;     // 原指令副本(用于单步执行)

    u32 flags;                           // 状态标志
};

状态标志定义(include/linux/kprobes.h:97):

// include/linux/kprobes.h:97
#define KPROBE_FLAG_GONE        1  // 探针已被移除
#define KPROBE_FLAG_DISABLED    2  // 探针临时禁用
#define KPROBE_FLAG_OPTIMIZED   4  // 已优化(跳转替换而非断点)
#define KPROBE_FLAG_FTRACE      8  // 通过 ftrace 实现(性能更好)
#define KPROBE_FLAG_ON_FUNC_ENTRY 16  // 探针位于函数入口

4.2 kretprobe 结构体

include/linux/kprobes.h:146

// include/linux/kprobes.h:146
struct kretprobe {
    struct kprobe kp;               // 嵌入的 kprobe(在函数入口)
    kretprobe_handler_t handler;    // 返回时调用的 handler
    kretprobe_handler_t entry_handler; // 进入时调用(可选)
    int maxactive;                  // 最大并发实例数
    int nmissed;                    // 因 maxactive 不足错过的次数
    size_t data_size;               // 每实例私有数据大小
    struct rethook *rh;             // 返回钩子(CONFIG_KRETPROBE_ON_RETHOOK)
};

// include/linux/kprobes.h:162
struct kretprobe_instance {
    struct rethook_node node;       // 返回地址保存
    char data[];                    // 用户私有数据(大小 = kretprobe.data_size)
};

4.3 x86 断点替换机制(int3)

kprobe 的工作原理(x86 架构):

原始指令替换为 int3(0xCC):

  探针安装前:
  ┌───────────────────────────────────┐
  │  0x... :  48 89 e5    mov %rsp,%rbp  ← 原始指令(3 字节)│
  └───────────────────────────────────┘

  探针安装后(arch_arm_kprobe):
  ┌───────────────────────────────────┐
  │  0x... :  CC          int3        ← 断点指令(1 字节)   │
  │           89 e5       (剩余字节保留)                    │
  └───────────────────────────────────┘

  int3 触发流程:
  ┌────────────────────────────────────────────────────────────┐
  │  1. CPU 产生 #BP 异常(中断向量 3)                        │
  │  2. 内核 do_int3() / exc_int3() 处理异常                   │
  │  3. 检查 kprobe_table 哈希表,找到对应 kprobe              │
  │  4. 调用 kprobe.pre_handler()(用户 handler)              │
  │  5. 设置 TF(Trap Flag),启动单步模式                      │
  │  6. 从 kprobe.ainsn 执行原始指令副本                       │
  │  7. 产生 #DB 单步异常                                      │
  │  8. 调用 kprobe.post_handler()                             │
  │  9. 清除 TF,恢复正常执行                                  │
  └────────────────────────────────────────────────────────────┘

优化路径(Optimized kprobes)

对于支持的架构,kprobe 可以从 int3(断点)优化为 jmp 指令,避免双重异常:

优化后:
  ┌────────────────────────────────────────────────────────────┐
  │  0x... :  e9 XX XX XX XX    jmp <kprobe_optimized_handler> │
  │              ↓                                             │
  │           执行 pre_handler                                 │
  │           执行原始指令(内联)                              │
  │           执行 post_handler                                │
  │           jmp <原指令后继地址>                             │
  └────────────────────────────────────────────────────────────┘
  标志:KPROBE_FLAG_OPTIMIZED (4)

ftrace-based kprobe

当探针位于函数入口且 ftrace 已有 nop 桩时,使用 ftrace 机制代替 int3,开销更低。标志:KPROBE_FLAG_FTRACE (8)include/linux/kprobes.h:104)。

4.4 register_kprobe() 完整流程

kernel/kprobes.c:1708

// kernel/kprobes.c:1708
int register_kprobe(struct kprobe *p)
{
    // 1. 符号解析:将 symbol_name + offset 转换为虚拟地址
    addr = _kprobe_addr(p->addr, p->symbol_name, p->offset, &on_func_entry);

    // 2. 重复注册检查
    ret = warn_kprobe_rereg(p);

    // 3. 清除用户传入的无效标志
    p->flags &= KPROBE_FLAG_DISABLED;
    if (on_func_entry)
        p->flags |= KPROBE_FLAG_ON_FUNC_ENTRY;

    p->nmissed = 0;
    INIT_LIST_HEAD(&p->list);

    // 4. 地址安全检查(黑名单、不可探测区域)
    ret = check_kprobe_address_safe(p, &probed_mod);

    // 5. 核心注册(加锁)
    ret = __register_kprobe(p);
    ...
}

__register_kprobe() 核心逻辑(kernel/kprobes.c:1671):

// kernel/kprobes.c:1671
static int __register_kprobe(struct kprobe *p)
{
    guard(mutex)(&kprobe_mutex);

    // 检查同一地址是否已有 kprobe(聚合)
    old_p = get_kprobe(p->addr);
    if (old_p)
        return register_aggr_kprobe(old_p, p);

    // 准备指令副本(arch_prepare_kprobe)
    scoped_guard(cpus_read_lock) {
        guard(mutex)(&text_mutex);   // 防止 text 段并发修改
        ret = prepare_kprobe(p);
    }

    // 加入哈希表(RCU 安全)
    INIT_HLIST_NODE(&p->hlist);
    hlist_add_head_rcu(&p->hlist,
        &kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]);

    // 武装探针(写入 int3)
    if (!kprobes_all_disarmed && !kprobe_disabled(p))
        ret = arm_kprobe(p);

    // 尝试优化(异步,workqueue 执行)
    try_to_optimize_kprobe(p);
    return 0;
}
register_kprobe() 调用链示意图:

  register_kprobe()
    ├── _kprobe_addr()           符号 → 地址解析
    ├── check_kprobe_address_safe()  黑名单检查
    └── __register_kprobe()
          ├── get_kprobe()       查哈希表(同地址聚合)
          ├── prepare_kprobe()
          │     └── arch_prepare_kprobe()  保存原指令,准备副本
          ├── hlist_add_head_rcu()  加入 kprobe_table
          ├── arm_kprobe()
          │     └── arch_arm_kprobe()  写入 0xCC(int3)
          └── try_to_optimize_kprobe()  加入优化 workqueue

4.5 kretprobe 返回值拦截机制

kretprobe 通过拦截函数的返回地址实现返回值追踪:

kretprobe 工作原理:

  函数入口被 kprobe 探针拦截时:
  ┌──────────────────────────────────────────────────────────────┐
  │  1. pre_handler 被调用(kretprobe_trampoline 注册为 handler)│
  │  2. 分配 kretprobe_instance(从 objpool 获取)               │
  │  3. 保存真实返回地址到 ri->ret_addr                          │
  │  4. 将栈上返回地址替换为 kretprobe_trampoline                │
  └──────────────────────────────────────────────────────────────┘

  函数返回时(ret 指令跳到 trampoline):
  ┌──────────────────────────────────────────────────────────────┐
  │  1. 跳入 __kretprobe_trampoline()                           │
  │  2. 调用 kretprobe_trampoline_handler()                     │
  │  3. 从链表找到对应 kretprobe_instance                       │
  │  4. 调用用户 kretprobe.handler()(可读取返回值)             │
  │  5. 恢复真实返回地址,跳回调用者                             │
  └──────────────────────────────────────────────────────────────┘

  include/linux/kprobes.h:213 - kretprobe_trampoline_handler()
  include/linux/kprobes.h:218 - kretprobe_trampoline_addr()

entry_handler 的用途:在函数进入时保存状态(如参数),在返回时读取,实现参数 + 返回值的关联追踪:

// 典型 kretprobe 使用模式
struct my_data {
    ktime_t entry_stamp;
    u64     arg0;          // 保存第一个参数
};

static int entry_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
    struct my_data *data = (struct my_data *)ri->data;
    data->entry_stamp = ktime_get();
    data->arg0 = regs->di;  // x86-64 第一个参数
    return 0;
}

static int ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
    struct my_data *data = (struct my_data *)ri->data;
    s64 delta = ktime_to_ns(ktime_sub(ktime_get(), data->entry_stamp));
    printk("func(arg0=%llu) took %lld ns, ret=%lu\n",
           data->arg0, delta, regs_return_value(regs));
    return 0;
}

4.6 kprobe 黑名单

某些内核函数不能被探测(会导致递归或系统崩溃),通过 __kprobe_blacklist section 标记(include/linux/kprobes.h:267):

// include/linux/kprobes.h:267
extern unsigned long __start_kprobe_blacklist[];
extern unsigned long __stop_kprobe_blacklist[];

// 使用 nokprobe_inline 或 __kprobes 属性标记不可探测函数

黑名单包括:kprobe 自身的处理函数、中断处理关键路径、NMI 处理程序等。


5. tracepoint:静态追踪点机制

5.1 struct tracepoint 结构体

include/linux/tracepoint-defs.h:39

// include/linux/tracepoint-defs.h:39
struct tracepoint {
    const char *name;                      // tracepoint 名称字符串
    struct static_key_false key;           // 静态分支 key(零开销门控)
    struct static_call_key *static_call_key;  // 静态调用 key
    void *static_call_tramp;               // 静态调用 trampoline
    void *iterator;                        // probe 函数迭代器
    void *probestub;                       // 空 probe stub(未启用时调用)
    struct tracepoint_func __rcu *funcs;   // 已注册的 probe 函数数组(RCU)
    struct tracepoint_ext *ext;            // 扩展(regfunc/unregfunc)
};

// include/linux/tracepoint-defs.h:26
struct tracepoint_func {
    void *func;    // probe 函数指针
    void *data;    // probe 私有数据
    int   prio;    // 优先级(多个 probe 时的调用顺序)
};

struct tracepoint_ext 允许注册/注销回调:

// include/linux/tracepoint-defs.h:32
struct tracepoint_ext {
    int (*regfunc)(void);     // 第一个 probe 注册时调用
    void (*unregfunc)(void);  // 最后一个 probe 注销时调用
    unsigned int faultable:1; // 是否允许在可 fault 上下文中使用
};

5.2 DECLARE_TRACE 宏展开详解

include/linux/tracepoint.h:245

// include/linux/tracepoint.h:245
#define __DECLARE_TRACE_COMMON(name, proto, args, data_proto)       \
    extern int __traceiter_##name(data_proto);                      \
    DECLARE_STATIC_CALL(tp_func_##name, __traceiter_##name);        \
    extern struct tracepoint __tracepoint_##name;                   \
    extern void rust_do_trace_##name(proto);                        \
    /* 注册/注销辅助函数 */                                          \
    static inline int                                               \
    register_trace_##name(void (*probe)(data_proto), void *data)    \
    {                                                               \
        return tracepoint_probe_register(&__tracepoint_##name,      \
                                        (void *)probe, data);       \
    }                                                               \
    static inline int                                               \
    unregister_trace_##name(void (*probe)(data_proto), void *data)  \
    {                                                               \
        return tracepoint_probe_unregister(&__tracepoint_##name,    \
                                          (void *)probe, data);     \
    }                                                               \
    /* 快速检查是否有 probe 注册 */                                  \
    static inline bool trace_##name##_enabled(void)                 \
    {                                                               \
        return static_branch_unlikely(&__tracepoint_##name.key);    \
    }

调用点宏展开(include/linux/tracepoint.h:279):

// include/linux/tracepoint.h:289
static inline void trace_##name(proto)
{
    if (static_branch_unlikely(&__tracepoint_##name.key))
        __do_trace_##name(args);
    if (IS_ENABLED(CONFIG_LOCKDEP) && (cond)) {
        WARN_ONCE(!rcu_is_watching(), "RCU not watching for tracepoint");
    }
}

展开后的实际调用点代码:

调用点汇编(未激活时):

  trace_sched_switch(preempt, prev, next)
  ↓ 展开为:
  if (static_branch_unlikely(&__tracepoint_sched_switch.key))
      __do_trace_sched_switch(preempt, prev, next)

  ↓ 汇编(static_key 未激活时):
  ; jmp 指令(5 字节 NOP 等价)
  ; 整个 if 分支被跳过,完全无函数调用开销

  ↓ 汇编(static_key 激活后):
  ; 条件不再跳过,进入 probe 调用路径
  call __traceiter_sched_switch

5.3 DEFINE_TRACE 宏:静态数据生成

include/linux/tracepoint.h:326__DEFINE_TRACE_EXT 宏在 .c 文件中实例化 tracepoint:

// include/linux/tracepoint.h:332-342(简化)
struct tracepoint __tracepoint_##_name __used
__section("__tracepoints") = {
    .name        = __tpstrtab_##_name,      // 字符串,在 __tracepoints_strings section
    .key         = STATIC_KEY_FALSE_INIT,   // 初始禁用
    .static_call_key = &STATIC_CALL_KEY(tp_func_##_name),
    .static_call_tramp = STATIC_CALL_TRAMP_ADDR(tp_func_##_name),
    .iterator    = &__traceiter_##_name,    // probe 迭代函数
    .probestub   = &__probestub_##_name,    // 空 stub
    .funcs       = NULL,                    // 初始无 probe
    .ext         = _ext,
};
__TRACEPOINT_ENTRY(_name);  // 在 __tracepoints_ptrs section 记录指针

ELF section 布局

ELF 文件中的 tracepoint 相关 sections:

  __tracepoints_strings:  所有 tracepoint 名称字符串
  __tracepoints:          所有 struct tracepoint 实例
  __tracepoints_ptrs:     指向 __tracepoints 的指针数组
                          (用于内核遍历所有 tracepoint)
  __tracepoint_check:     仅编译期使用,验证 tracepoint 被引用

5.4 static key 零开销机制

struct static_key_false key 基于 Linux jump label 机制:

static_key 工作原理(x86-64):

  未激活时:
  ┌──────────────────────────────────────────────────────────┐
  │  if (static_branch_unlikely(&key)):                     │
  │      jmp <skip_label>     ← 直接跳过(E9 XX XX XX XX)  │
  │  ... probe 调用代码 ...                                  │
  │  <skip_label>:                                          │
  └──────────────────────────────────────────────────────────┘

  激活后(static_key_enable() 修改指令):
  ┌──────────────────────────────────────────────────────────┐
  │  if (static_branch_unlikely(&key)):                     │
  │      nop nop nop nop nop  ← NOP(0F 1F 44 00 00)      │
  │  ... probe 调用代码 ...   ← 现在会被执行                │
  │  <skip_label>:                                          │
  └──────────────────────────────────────────────────────────┘

  关键:指令修改通过 text_poke_bp() 实现,利用 int3 保证原子性
  修改过程:int3 → 写新指令 → IPI 同步 → 移除 int3

  未激活成本:1 条 jmp 指令(已预测跳转,~0 周期)
  激活成本:1 条 nop + probe 调用开销

5.5 trace events:tracepoint 的格式化层

include/trace/events/ 下的头文件定义了内核预置的 trace event。以 sched_switch 为例:

// include/trace/events/sched.h(简化)
TRACE_EVENT(sched_switch,
    TP_PROTO(bool preempt,
             struct task_struct *prev,
             struct task_struct *next,
             unsigned int prev_state),

    TP_ARGS(preempt, prev, next, prev_state),

    TP_STRUCT__entry(
        __array(char,   prev_comm,  TASK_COMM_LEN)
        __field(pid_t,  prev_pid)
        __field(int,    prev_prio)
        __field(long,   prev_state)
        __array(char,   next_comm,  TASK_COMM_LEN)
        __field(pid_t,  next_pid)
        __field(int,    next_prio)
    ),

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

    TP_printk("prev_comm=%s prev_pid=%d ... next_comm=%s next_pid=%d",
              __entry->prev_comm, __entry->prev_pid, ...)
);

TRACE_EVENT 宏通过多次 include 同一头文件(每次 #define TRACE_EVENT 为不同含义)实现:

TRACE_EVENT 宏的 6 次展开:

  第 1 次:定义 struct trace_event_raw_<name>(ring buffer 中的数据格式)
  第 2 次:定义 trace_<name>() 调用点函数
  第 3 次:定义 perf_trace_<name>() perf 路径
  第 4 次:定义 __ftrace_<name>() ftrace 格式化输出
  第 5 次:定义 event_<name> trace_event_call 实例
  第 6 次:注册 event(__init 阶段)

6. perf events 框架

6.1 struct perf_event 核心结构

include/linux/perf_event.h:765

// include/linux/perf_event.h:765
struct perf_event {
    /* 链表管理 */
    struct list_head        event_entry;    // 链入 perf_event_context
    struct list_head        sibling_list;   // 事件组内的兄弟事件
    struct list_head        active_list;    // 活跃事件链表

    /* 分组(group leader/member) */
    struct perf_event       *group_leader;  // 组长事件
    struct pmu              *pmu;           // 所属 PMU
    void                    *pmu_private;   // PMU 私有数据

    enum perf_event_state   state;          // ACTIVE/INACTIVE/OFF/ERROR
    local64_t               count;          // 当前计数值
    atomic64_t              child_count;    // 子事件累计值

    u64                     total_time_enabled;  // 总启用时间(ns)
    u64                     total_time_running;  // 总运行时间(ns)
    u64                     tstamp;

    struct perf_event_attr  attr;           // 用户配置(来自 syscall)
    struct hw_perf_event    hw;             // 硬件 PMU 相关数据

    struct perf_event_context *ctx;         // 所属上下文(task/cpu)
    struct perf_event_pmu_context *pmu_ctx;

    /* ring buffer */
    struct perf_buffer      *rb;            // 输出 ring buffer
    struct list_head        rb_entry;

    /* 轮询/通知 */
    wait_queue_head_t       waitq;
    struct fasync_struct    *fasync;

    /* 延迟工作(NMI 上下文的工作卸载) */
    unsigned int            pending_wakeup;
    unsigned int            pending_kill;
    struct irq_work         pending_irq;
    struct irq_work         pending_disable_irq;
    struct callback_head    pending_task;

    atomic_t                event_limit;

    /* 所有者 */
    struct task_struct      *owner;

    /* 子事件(fork 继承) */
    struct mutex            child_mutex;
    struct list_head        child_list;
    struct perf_event       *parent;

    int cpu;         // 绑定的 CPU(-1 表示任意)
    int oncpu;       // 当前调度到的 CPU
    ...
};

6.2 struct pmu 接口

include/linux/perf_event.h:330 定义了 PMU 的操作接口:

// include/linux/perf_event.h:330
struct pmu {
    struct list_head    entry;       // 全局 PMU 链表
    const char          *name;       // PMU 名称
    int                 type;        // PMU 类型 ID

    int                 capabilities; // 能力标志
    unsigned int        scope;        // PMU 作用域(core/die/pkg/sys)

    /* 核心操作 */
    int  (*event_init) (struct perf_event *event);  // 初始化事件
    int  (*add)        (struct perf_event *event, int flags); // 加入 PMU
    void (*del)        (struct perf_event *event, int flags); // 从 PMU 移除
    void (*start)      (struct perf_event *event, int flags); // 启动计数
    void (*stop)       (struct perf_event *event, int flags); // 停止计数
    void (*read)       (struct perf_event *event);            // 读取计数值

    /* 可选操作 */
    void (*pmu_enable) (struct pmu *pmu);   // 批量启用 PMU
    void (*pmu_disable)(struct pmu *pmu);   // 批量禁用 PMU

    /* 事务(批量 add/del) */
    void (*start_txn) (struct pmu *pmu, unsigned int txn_flags);
    int  (*commit_txn)(struct pmu *pmu);
    void (*cancel_txn)(struct pmu *pmu);

    /* AUX area(Intel PT / ARM SPE) */
    void *(*setup_aux)(struct perf_event *event, void **pages,
                       int nr_pages, bool overwrite);
    void (*free_aux)(void *aux);
    long (*snapshot_aux)(struct perf_event *event,
                         struct perf_output_handle *handle,
                         unsigned long size);

    /* 地址过滤 */
    int  (*addr_filters_validate)(struct list_head *filters);
    void (*addr_filters_sync)    (struct perf_event *event);
};

6.3 PMU 能力标志

include/linux/perf_event.h:297

// include/linux/perf_event.h:297
#define PERF_PMU_CAP_NO_INTERRUPT    0x0001  // 不产生中断(只能轮询)
#define PERF_PMU_CAP_NO_NMI          0x0002  // 不使用 NMI
#define PERF_PMU_CAP_AUX_NO_SG      0x0004  // AUX buffer 不支持 scatter-gather
#define PERF_PMU_CAP_EXTENDED_REGS  0x0008  // 支持扩展寄存器读取
#define PERF_PMU_CAP_EXCLUSIVE      0x0010  // 独占使用(不可与其他 PMU 共存)
#define PERF_PMU_CAP_ITRACE         0x0020  // 指令追踪能力(Intel PT)
#define PERF_PMU_CAP_NO_EXCLUDE     0x0040  // 不支持 user/kernel 排除
#define PERF_PMU_CAP_AUX_OUTPUT     0x0080  // 支持 AUX output 模式
#define PERF_PMU_CAP_AUX_PAUSE      0x0200  // 支持 AUX 暂停/恢复
#define PERF_PMU_CAP_MEDIATED_VPMU  0x0800  // 虚拟化 PMU(VPMU)

6.4 三种事件类型

perf_event 类型体系(uapi/linux/perf_event.h):

  PERF_TYPE_HARDWARE(硬件事件):
    PERF_COUNT_HW_CPU_CYCLES          CPU 周期数
    PERF_COUNT_HW_INSTRUCTIONS        执行指令数
    PERF_COUNT_HW_CACHE_REFERENCES    缓存访问次数
    PERF_COUNT_HW_CACHE_MISSES        缓存缺失次数
    PERF_COUNT_HW_BRANCH_INSTRUCTIONS 分支指令数
    PERF_COUNT_HW_BRANCH_MISSES       分支预测失败数
    PERF_COUNT_HW_BUS_CYCLES          总线周期数
    PERF_COUNT_HW_STALLED_CYCLES_FRONTEND  前端停顿
    PERF_COUNT_HW_STALLED_CYCLES_BACKEND   后端停顿

  PERF_TYPE_SOFTWARE(软件事件):
    PERF_COUNT_SW_CPU_CLOCK           CPU 时钟
    PERF_COUNT_SW_TASK_CLOCK          任务时钟
    PERF_COUNT_SW_PAGE_FAULTS         页错误
    PERF_COUNT_SW_CONTEXT_SWITCHES    上下文切换
    PERF_COUNT_SW_CPU_MIGRATIONS      CPU 迁移
    PERF_COUNT_SW_PAGE_FAULTS_MIN     次缺页
    PERF_COUNT_SW_PAGE_FAULTS_MAJ     主缺页
    PERF_COUNT_SW_ALIGNMENT_FAULTS    对齐错误
    PERF_COUNT_SW_EMULATION_FAULTS    模拟错误
    PERF_COUNT_SW_DUMMY               虚拟事件(仅用于采样)

  PERF_TYPE_TRACEPOINT(tracepoint 事件):
    type = PERF_TYPE_TRACEPOINT
    config = tracepoint_id(来自 /sys/kernel/tracing/events/.../id)

6.5 hw_perf_event:硬件层数据

include/linux/perf_event.h:147

// include/linux/perf_event.h:147
struct hw_perf_event {
    union {
        struct { /* 硬件 PMU 事件 */
            u64     config;           // PMU 配置寄存器值
            u64     config1;          // 额外配置
            u64     last_tag;
            unsigned long event_base; // 事件计数器基址
            int     event_base_rdpmc; // rdpmc 索引
            int     idx;              // 计数器索引
            int     last_cpu;
            int     flags;

            struct hw_perf_event_extra extra_reg;   // 额外 MSR
            struct hw_perf_event_extra branch_reg;  // LBR 相关
        };
        struct { /* 软件事件 */
            struct hrtimer hrtimer;   // 软件事件的 hrtimer
        };
        struct { /* tracepoint 事件 */
            struct list_head tp_list;
        };
        struct { /* AUX / Intel PT */
            u64     aux_config;
            unsigned int aux_paused;
        };
    };

    int     state;         // PERF_HES_STOPPED / PERF_HES_UPTODATE
    local64_t prev_count;  // 上次读取的计数值

    u64 sample_period;     // 采样周期
    union {
        struct {
            u64     last_period;
            local64_t period_left;  // 当前周期剩余计数
        };
        struct {
            u64 saved_metric;  // Topdown 指标
            u64 saved_slots;
        };
    };

    u64 interrupts_seq;    // 中断序号(用于节流)
    u64 interrupts;        // 当前节流周期的中断次数
};

6.6 perf_event_open() 系统调用

perf_event_open() 是 perf 的核心系统调用(syscall NR 298 on x86-64):

// kernel/events/core.c
SYSCALL_DEFINE5(perf_event_open,
    struct perf_event_attr __user *, attr_uptr,
    pid_t,                           pid,
    int,                             cpu,
    int,                             group_fd,
    unsigned long,                   flags)

参数语义:

perf_event_attr 关键字段:

  .type       - 事件类型(HARDWARE/SOFTWARE/TRACEPOINT/...)
  .config     - 具体事件编号或 tracepoint id
  .sample_period - 采样周期(每 N 次事件采样一次)
  .sample_freq   - 采样频率(Hz,与 sample_period 互斥)
  .sample_type   - 采样内容(IP/TID/TIME/ADDR/CALLCHAIN/...)
  .read_format   - 读取格式
  .disabled      - 初始禁用
  .exclude_kernel - 不计数内核空间
  .exclude_user   - 不计数用户空间
  .mmap          - 启用 mmap 事件记录
  .comm          - 记录 comm 事件
  .freq          - 使用频率而非周期

pid/cpu 组合含义:
  pid=0,  cpu=-1  → 当前进程,所有 CPU
  pid=0,  cpu=N   → 当前进程,CPU N
  pid=-1, cpu=N   → 所有进程,CPU N(系统级)
  pid=N,  cpu=-1  → 进程 N,所有 CPU

6.7 overflow 采样与 PEBS

当 PMU 计数器溢出时,触发 PMI(Performance Monitoring Interrupt):

PMI 驱动的采样流程:

  硬件计数器  →  溢出  →  PMI 中断
                              │
                              ▼
                    perf_event_overflow()
                              │
                     ┌────────┴──────────┐
                     │                   │
               采集采样数据         触发 BPF 程序
               (IP, callchain,      (BPF_PROG_TYPE_PERF_EVENT)
                regs, stack...)
                     │
                     ▼
              写入 perf ring buffer
              (用户空间通过 mmap 读取)

PEBS(Precise Event Based Sampling,Intel 特性):
  - 硬件在指令边界准确记录 IP(非 PMI 时的 skid)
  - 写入专用 PEBS buffer(内存),再由软件收集
  - 支持 load/store 数据地址采样
  - 内核通过 PERF_SAMPLE_PHYS_ADDR 暴露物理地址

6.8 perf 输出 ring buffer(struct perf_buffer)

与 ftrace ring buffer 不同,perf 的输出 buffer 通过 mmap(2) 直接映射到用户空间:

perf mmap ring buffer 布局:

  ┌─────────────────────────────────────────────────────────────┐
  │  第 0 页:struct perf_event_mmap_page(控制页)             │
  │    .data_head  - 写指针(原子,由内核更新)                  │
  │    .data_tail  - 读指针(由用户空间更新)                    │
  │    .data_offset, .data_size - 数据区偏移和大小              │
  │    .index      - rdpmc 索引(用于 userspace 读计数器)       │
  ├─────────────────────────────────────────────────────────────┤
  │  第 1..N 页:实际采样数据(power-of-2 大小)                │
  │  每条记录以 struct perf_event_header 开头:                  │
  │    .type   - PERF_RECORD_SAMPLE / MMAP / COMM / ...        │
  │    .misc   - CPUMODE(USER/KERNEL/HYPERVISOR)              │
  │    .size   - 记录大小                                       │
  └─────────────────────────────────────────────────────────────┘

7. uprobe:用户态动态探针

7.1 uprobe 核心结构

include/linux/uprobes.h:45

// include/linux/uprobes.h:45
struct uprobe_consumer {
    /* 命中时调用 */
    int (*handler)(struct uprobe_consumer *self,
                   struct pt_regs *regs, __u64 *data);
    /* 返回时调用 */
    int (*ret_handler)(struct uprobe_consumer *self,
                       unsigned long func,
                       struct pt_regs *regs, __u64 *data);
    /* 过滤:返回 true 则对此 mm 生效 */
    bool (*filter)(struct uprobe_consumer *self, struct mm_struct *mm);

    struct list_head cons_node;
    __u64 id;       // 注册后由内核分配的唯一 ID
};

7.2 uprobe_task 与执行状态机

include/linux/uprobes.h:125

// include/linux/uprobes.h:125
struct uprobe_task {
    enum uprobe_task_state  state;       // RUNNING/SSTEP/SSTEP_ACK/...

    unsigned int            depth;       // 嵌套 uretprobe 深度
    struct return_instance  *return_instances; // uretprobe 栈

    struct return_instance  *ri_pool;    // 预分配的 ri 对象池
    struct timer_list       ri_timer;    // SRCU 超时定时器
    seqcount_t              ri_seqcount; // 序列号(并发保护)

    union {
        struct {
            struct arch_uprobe_task autask;
            unsigned long           vaddr;  // 探针虚拟地址
        };
        struct {
            struct callback_head    dup_xol_work;
            unsigned long           dup_xol_addr;
        };
    };

    struct uprobe       *active_uprobe;
    unsigned long       xol_vaddr;      // XOL 执行地址(out-of-line)
    bool                signal_denied;
    struct arch_uprobe  *auprobe;
};

7.3 uprobe 插桩机制详解

uprobe 的核心思想是在用户空间的指令位置写入断点指令(x86: 0xCC int3),触发后内核单步执行原指令(XOL - execute out-of-line):

uprobe 安装流程:

  1. 注册:uprobe_register(inode, offset, consumer)
     │
     ├── 查找/创建 struct uprobe(以 inode+offset 为 key,红黑树存储)
     │
     ├── 添加 consumer 到 uprobe->consumers 链表
     │
     └── install_breakpoint()
           │
           ├── 找到该 inode 在各进程中的映射(遍历 rmap)
           │
           └── 对每个映射 VMA 调用 set_swbp()
                 │
                 └── 在对应页写入 0xCC(int3)
                     (通过 COW 机制不影响文件原始内容)

  执行时触发:
  ┌───────────────────────────────────────────────────────────────┐
  │  用户程序执行到 0xCC → #BP 异常                              │
  │         │                                                     │
  │         ▼ arch_uprobe_exception_notify()                      │
  │  uprobe_pre_sstep_notifier() → 识别为 uprobe                  │
  │         │                                                     │
  │         ▼ 找到 uprobe 和 uprobe_task                          │
  │  调用所有匹配的 consumer->handler()                           │
  │         │                                                     │
  │         ▼ 准备单步执行                                        │
  │  将原指令复制到 XOL 槽(xol_area 中的专用 vaddr)             │
  │  修改 RIP 指向 XOL 槽                                         │
  │  设置 TF(Trap Flag)                                         │
  │         │                                                     │
  │         ▼ 返回用户空间,单步执行 XOL 中的原指令               │
  │  产生 #DB 异常 → uprobe_post_sstep_notifier()                │
  │  恢复 RIP 到原指令之后的正确地址                              │
  └───────────────────────────────────────────────────────────────┘

7.4 hybrid-lifetime uprobe(hprobe)

内核引入了复杂的生命周期管理来保证 uretprobe 的安全性(include/linux/uprobes.h:75):

// include/linux/uprobes.h:75
enum hprobe_state {
    HPROBE_LEASED,    // SRCU 保护的 uprobe(临时)
    HPROBE_STABLE,    // 引用计数保护的 uprobe(稳定)
    HPROBE_GONE,      // uprobe 已失效(SRCU 超时 + refcount 失败)
    HPROBE_CONSUMED,  // 已被 uretprobe handler "消费"
};

// include/linux/uprobes.h:116
struct hprobe {
    enum hprobe_state   state;
    int                 srcu_idx;    // SRCU 锁的 cookie
    struct uprobe       *uprobe;     // 实际的 uprobe 指针
};

return_instance 链(include/linux/uprobes.h:159):

// include/linux/uprobes.h:159
struct return_instance {
    struct hprobe       hprobe;         // hybrid 生命周期管理
    unsigned long       func;           // 被探测函数地址
    unsigned long       stack;          // 保存的栈指针
    unsigned long       orig_ret_vaddr; // 原始返回地址
    bool                chained;        // 是否嵌套
    int                 cons_cnt;       // 关联的 consumer 数量

    struct return_instance *next;       // 链接(作为栈)
    struct rcu_head     rcu;
    struct return_consumer consumer;    // 单 consumer 时内联
    struct return_consumer *extra_consumers; // 多 consumer 时动态分配
} ____cacheline_aligned;               // 缓存行对齐

7.5 uprobes_state 与 xol_area

每个进程的 mm_struct 中有 uprobes_stateinclude/linux/uprobes.h:187):

// include/linux/uprobes.h:187
struct uprobes_state {
    struct xol_area     *xol_area;     // XOL 执行区域
#ifdef CONFIG_X86_64
    struct hlist_head   head_tramps;   // trampoline 哈希表
#endif
};

XOL area 是在用户空间高地址区域分配的一个特殊 vma,用于存放原指令副本。每个线程在此区域有独立的槽位,保证并发探针的独立执行。

7.6 uprobe 与 BPF 结合

BPF_PROG_TYPE_KPROBE 实际上同时支持 kprobe 和 uprobe 附着(通过 perf_event 接口):

uprobe + BPF 的数据路径:

  用户程序触发 int3
    → uprobe handler(内核)
    → bpf_prog_run()  执行附着的 BPF 程序
    → BPF 程序通过 helpers 访问 pt_regs / 内存
    → bpf_perf_event_output() 输出到 perf ring buffer
    → 用户空间 BPF reader 消费

注册 uprobe BPF 程序:

通过 perf_event_open() + PERF_TYPE_PROBE:
  attr.type = perf_type_id("uprobe");  // 动态分配
  attr.config1 = (u64)(path);          // 目标文件路径
  attr.config2 = offset;               // 文件偏移

然后通过 ioctl(fd, PERF_EVENT_IOC_SET_BPF, prog_fd) 附着 BPF 程序

8. eBPF 追踪程序

8.1 BPF 程序类型(追踪相关)

include/uapi/linux/bpf.h:1063

// include/uapi/linux/bpf.h:1063(相关类型)
BPF_PROG_TYPE_KPROBE,       // kprobe/kretprobe/uprobe/uretprobe
BPF_PROG_TYPE_TRACEPOINT,   // tracepoint(静态追踪点)
BPF_PROG_TYPE_PERF_EVENT,   // perf PMU 事件

// 更新的类型
BPF_PROG_TYPE_TRACING,      // fentry/fexit/iter(基于 BTF)
BPF_PROG_TYPE_RAW_TRACEPOINT,        // 原始 tracepoint(无格式化)
BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE, // 可写参数的 raw tracepoint

各类型特点对比:

BPF 追踪程序类型对比:

  BPF_PROG_TYPE_KPROBE:
    附着点:任意内核函数(kprobe/kretprobe)或用户函数(uprobe)
    参数:struct pt_regs *ctx
    读取参数:PT_REGS_PARM1(ctx) 等宏
    限制:需要手动处理指针解引用

  BPF_PROG_TYPE_TRACEPOINT:
    附着点:tracepoint(如 sched:sched_switch)
    参数:tracepoint 特定的结构体
    优势:参数格式稳定,类型安全
    示例:trace_sched_switch 的参数 = TP_STRUCT__entry 的字段

  BPF_PROG_TYPE_PERF_EVENT:
    附着点:perf PMU 事件(hardware/software/tracepoint)
    用途:sampling + 计数
    特殊能力:访问 perf_sample_data

  BPF_PROG_TYPE_TRACING (fentry/fexit):
    附着点:内核函数入口/出口(基于 BTF 类型信息)
    参数:直接访问函数参数(有 BTF 保证类型安全)
    优势:比 kprobe 开销更低(基于 ftrace fentry)
    示例:SEC("fentry/tcp_sendmsg")

8.2 BPF 程序附着机制

BPF 程序生命周期:

  ┌──────────────────────────────────────────────────────────────┐
  │  1. bpf(BPF_PROG_LOAD, ...)    加载 BPF 字节码到内核        │
  │                                验证器(verifier)安全检查    │
  │                                JIT 编译为原生机器码          │
  │                                返回 prog_fd                  │
  │                                                              │
  │  2. 附着到追踪点:                                           │
  │     方式 A:perf_event_open() + ioctl SET_BPF               │
  │     方式 B:bpf(BPF_LINK_CREATE, attach_type=...)           │
  │            → 创建 struct bpf_link                           │
  │                                                              │
  │  3. 追踪事件触发时:                                         │
  │     kprobe/tracepoint/... → bpf_prog_run(prog, ctx)         │
  │                                                              │
  │  4. BPF 程序输出数据:                                       │
  │     bpf_perf_event_output() → perf ring buffer              │
  │     bpf_ringbuf_output()   → BPF ring buffer(更新接口)     │
  │                                                              │
  │  5. 关闭 fd → detach(引用计数归零时自动清理)               │
  └──────────────────────────────────────────────────────────────┘

8.3 bpf_perf_event_output()

include/uapi/linux/bpf.h:2459

long bpf_perf_event_output(void *ctx, struct bpf_map *map, u64 flags,
                            void *data, u64 size)

参数说明:
  ctx   - BPF 程序上下文
  map   - BPF_MAP_TYPE_PERF_EVENT_ARRAY 类型的 map
  flags - BPF_F_CURRENT_CPU(当前 CPU 的 perf buffer)
  data  - 要输出的数据指针
  size  - 数据大小

工作原理:
  1. 从 map 找到当前 CPU 的 perf_event fd
  2. 将 data 写入 perf ring buffer
  3. 唤醒等待的用户空间读者
  4. 用户空间通过 perf_buffer__poll() 消费

8.4 BPF_MAP_TYPE_RINGBUF:新接口

include/uapi/linux/bpf.h:1041 引入的新式 ring buffer,相比 perf event array 有以下优势:

BPF ring buffer vs perf event array 对比:

  perf event array(旧):
    - 每 CPU 独立 buffer,需要 CPU 数量个 fd
    - 可能丢失数据(多 CPU 同时写且某 CPU buffer 满)
    - 用户空间按 CPU 轮询

  BPF ring buffer(新,BPF_MAP_TYPE_RINGBUF):
    - 单个共享 buffer(所有 CPU 写同一个)
    - 通过 spinlock 保证顺序(适合低频事件)
    - 支持 reserve + commit/discard 模式
    - 用户空间单个 mmap 消费所有数据

使用方式:
  // BPF 程序端
  struct {
      __uint(type, BPF_MAP_TYPE_RINGBUF);
      __uint(max_entries, 1 << 24);   // 16MB
  } rb SEC(".maps");

  // reserve(不立即提交)
  struct event *e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
  if (!e) return 0;
  e->pid = bpf_get_current_pid_tgid();
  bpf_ringbuf_submit(e, 0);          // 或 bpf_ringbuf_discard(e, 0)

  // 用户空间端(libbpf)
  rb = ring_buffer__new(map_fd, handle_event, NULL, NULL);
  ring_buffer__poll(rb, timeout_ms);

8.5 BPF verifier 与安全保证

BPF 验证器是 eBPF 安全的核心,确保加载的程序不会破坏内核:

BPF verifier 检查项(kernel/bpf/verifier.c):

  1. 程序大小限制(BPF_MAXINSNS = 1M 条指令)
  2. 循环检测(有界循环,最大迭代次数)
  3. 指针验证:
     - 追踪所有寄存器的类型(scalar/ptr/map_value/...)
     - 防止越界访问
     - 防止非法指针泄漏到用户空间
  4. Helper 调用验证:
     - 参数类型检查
     - 返回值类型追踪
  5. 栈深度限制(MAX_BPF_STACK = 512 字节)
  6. Sleepable 程序的特殊规则
     (允许调用 bpf_copy_from_user 等可能睡眠的 helper)

8.6 BTF 与 CO-RE

BTF(BPF Type Format)和 CO-RE(Compile Once - Run Everywhere)使 BPF 程序可以跨内核版本运行:

CO-RE 工作原理:

  编译时:
    Clang 将类型访问编译为 BTF 注解的重定位记录
    记录:使用了 task_struct 的 comm 字段,在 task_struct 中偏移为 X

  加载时(libbpf):
    1. 读取目标内核的 BTF(/sys/kernel/btf/vmlinux)
    2. 找到实际的 task_struct.comm 偏移
    3. 重写 BPF 程序中的偏移值

  结果:
    同一个 BPF 程序在不同内核版本上都能正确访问结构体字段
    无需重新编译

bpftrace 语言基于 BTF:
  bpftrace -e 'kprobe:do_sys_open { printf("file: %s\n", str(arg1)); }'
  # arg1 对应 openat() 的第二个参数(filename),无需手动计算寄存器偏移

9. 动态调试(dyndbg)

9.1 _ddebug 结构体

include/linux/dynamic_debug.h:16

// include/linux/dynamic_debug.h:16
struct _ddebug {
    const char  *modname;    // 模块名
    const char  *function;   // 函数名
    const char  *filename;   // 源文件名
    const char  *format;     // 格式字符串
    unsigned int lineno:18;  // 行号(最大 262143)
    unsigned int class_id:6; // 类别 ID(用于分组)

    /* 运行时控制标志(可动态修改) */
    unsigned int flags:8;
    // _DPRINTK_FLAGS_PRINT         (1<<0) 实际打印
    // _DPRINTK_FLAGS_INCL_MODNAME  (1<<1) 包含模块名
    // _DPRINTK_FLAGS_INCL_FUNCNAME (1<<2) 包含函数名
    // _DPRINTK_FLAGS_INCL_LINENO   (1<<3) 包含行号
    // _DPRINTK_FLAGS_INCL_TID      (1<<4) 包含线程 ID
    // _DPRINTK_FLAGS_INCL_SOURCENAME (1<<5) 包含源文件名
    // _DPRINTK_FLAGS_INCL_STACK    (1<<6) 包含栈追踪

#ifdef CONFIG_JUMP_LABEL
    union {
        struct static_key_true  dd_key_true;  // 默认启用(DEBUG 定义时)
        struct static_key_false dd_key_false; // 默认禁用
    } key;
#endif
} __attribute__((aligned(8)));  // 8 字节对齐(static_key 要求)

9.2 pr_debug 宏展开

// 源码中的使用:
pr_debug("got packet from %pI4\n", &src_ip);

// 展开为(简化):
{
    static struct _ddebug __attribute__((aligned(8))) descriptor = {
        .modname  = KBUILD_MODNAME,
        .function = __func__,
        .filename = __FILE__,
        .format   = "got packet from %pI4\n",
        .lineno   = __LINE__,
        .flags    = _DPRINTK_FLAGS_DEFAULT,  // 0(或 _PRINT 如果 DEBUG 已定义)
    };
    // 检查 static_key(未启用时无开销)
    if (static_branch_unlikely(&descriptor.key.dd_key_false))
        __dynamic_pr_debug(&descriptor, "got packet from %pI4\n", &src_ip);
}

所有 _ddebug 结构体被放入 __dyndbg ELF section,内核启动时作为数组统一处理。

9.3 dyndbg 控制接口

通过 /sys/kernel/debug/dynamic_debug/control 文件动态启用/禁用调试输出:

# 启用特定模块的所有调试输出
echo 'module ext4 +p' > /sys/kernel/debug/dynamic_debug/control

# 启用特定文件的调试输出,并显示函数名和行号
echo 'file fs/ext4/inode.c +pfl' > /sys/kernel/debug/dynamic_debug/control

# 启用特定函数的调试,并显示线程 ID
echo 'func ext4_write_begin +pt' > /sys/kernel/debug/dynamic_debug/control

# 启用包含特定格式串的调试输出
echo 'format "got packet" +p' > /sys/kernel/debug/dynamic_debug/control

# 禁用
echo 'module ext4 -p' > /sys/kernel/debug/dynamic_debug/control

# 查看所有 dyndbg callsite
cat /sys/kernel/debug/dynamic_debug/control | head -20

标志字符含义:

+p  启用打印(_DPRINTK_FLAGS_PRINT)
+f  显示函数名
+l  显示行号
+m  显示模块名
+t  显示线程 ID
+s  显示源文件名
+S  显示调用栈
-   取消对应标志
=   设置为指定值(清除其他)

9.4 dev_dbg() 与设备调试

dev_dbg()pr_debug() 的设备版本,支持按设备动态控制:

// 基本使用
dev_dbg(&pdev->dev, "DMA transfer complete, len=%zu\n", len);

// 设备动态调试控制
echo 'module i2c-hid +p' > /sys/kernel/debug/dynamic_debug/control

// 或通过 sysfs
echo 'dynamic_debug/module=i2c-hid,format="DMA" flags=+p' \
    > /sys/bus/i2c/devices/0-0010/power/wakeup

9.5 dyndbg 分类系统(class_id)

include/linux/dynamic_debug.h:62 定义了分类系统:

// include/linux/dynamic_debug.h:62
enum class_map_type {
    DD_CLASS_TYPE_DISJOINT_BITS,   // 独立位,每位代表一个类别(drm.debug 风格)
    DD_CLASS_TYPE_LEVEL_NUM,       // 数值级别,0-N,N 启用 0..N-1
    DD_CLASS_TYPE_DISJOINT_NAMES,  // 命名独立类别(CSV 输入)
    DD_CLASS_TYPE_LEVEL_NAMES,     // 命名级别类别
};

DRM 驱动使用此分类:

// drivers/gpu/drm/drm_print.c
DEFINE_DYNAMIC_DEBUG_CLASSES(drm_debug_classes, ...
    { DRM_UT_CORE,    "core" },
    { DRM_UT_DRIVER,  "driver" },
    { DRM_UT_KMS,     "kms" },
    { DRM_UT_PRIME,   "prime" },
    ...
)

// 启用 DRM KMS 类别调试
echo 'module drm class kms +p' > /sys/kernel/debug/dynamic_debug/control

10. 系统整体可观测性工具链

10.1 perf record/report/stat 原理

perf stat(计数模式):

perf stat ls -la 的内部流程:

  1. fork + execve ls
  2. 对子进程 perf_event_open(),创建各类计数事件
     - cpu-cycles, instructions, cache-misses, ...
  3. 子进程执行期间,PMU 自动计数
  4. 子进程退出后:
     read(event_fd) → 获取最终计数值
  5. 格式化输出统计数据

输出示例:
  Performance counter stats for 'ls -la':

    5,234,567   cpu-cycles              # 3.21 GHz
    8,456,123   instructions            # 1.62 insn per cycle
      123,456   cache-misses            # 0.15% of all cache refs
       23,456   branch-misses           # 0.45% of all branches
    1.632 ms    elapsed

perf record(采样模式):

perf record -g -F 99 ls 的流程:

  1. perf_event_open() 以 99Hz 频率采样
     attr.sample_type = PERF_SAMPLE_IP | PERF_SAMPLE_CALLCHAIN
     attr.sample_freq = 99  (±1% 抖动防止频率锁定)

  2. mmap ring buffer,后台线程持续读取
     每个采样包含:
       IP(指令指针)
       callchain(调用栈,通过 DWARF 或 frame pointer 展开)
       PID/TID、CPU、时间戳

  3. 保存到 perf.data(二进制格式)

  4. 同时记录:
     - PERF_RECORD_COMM(进程名变化)
     - PERF_RECORD_MMAP(内存映射变化,用于符号解析)
     - PERF_RECORD_FORK/EXIT(进程生命周期)

perf report(分析模式):

perf report 读取 perf.data 流程:

  1. 读取 MMAP 记录,重建符号表
  2. 加载 /proc/kallsyms(内核符号)
  3. 加载 .debug 文件或 debuginfo 包(DWARF 信息)
  4. 对每个采样:
     - IP → 符号名(函数名)
     - callchain → 调用栈符号化
  5. 按函数聚合(overhead = 该函数命中次数 / 总采样数)
  6. 展示热点函数和调用关系

关键概念:
  overhead (self):函数本身耗时占比
  overhead (children):函数及其调用的子函数总耗时占比

10.2 bpftrace 脚本语言

bpftrace 是构建在 BPF 和 kprobe/tracepoint 之上的高级追踪语言:

bpftrace 执行流程:

  bpftrace script.bt
    │
    ├── 解析脚本(AST 生成)
    │
    ├── 语义分析(类型推断,BTF 查询)
    │
    ├── 代码生成(LLVM IR → BPF 字节码)
    │
    ├── 加载 BPF 程序到内核
    │   ├── kprobe/kretprobe
    │   ├── tracepoint
    │   ├── uprobe/uretprobe
    │   └── profile/interval(基于 perf event)
    │
    └── 事件循环(poll BPF maps 和 ring buffer)

典型 bpftrace 脚本示例:

# 追踪所有进程打开文件(kprobe 方式)
bpftrace -e '
kprobe:do_sys_openat2 {
    printf("PID %d (%s) opened: %s\n",
           pid, comm, str(arg1));
}'

# 追踪系统调用延迟(分布直方图)
bpftrace -e '
tracepoint:syscalls:sys_enter_read {
    @start[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_read
/@start[tid]/ {
    @latency = hist(nsecs - @start[tid]);
    delete(@start[tid]);
}'

# 基于采样的 CPU 火焰图数据收集
bpftrace -e '
profile:hz:99 {
    @[kstack] = count();
}
interval:s:10 {
    print(@);
    exit();
}'

10.3 火焰图生成原理

火焰图(Flame Graph)由 Brendan Gregg 发明,直观展示 CPU 时间分布:

火焰图生成流程:

  阶段 1:数据采集(perf record 或 bpftrace)
  ┌──────────────────────────────────────────────────────────────┐
  │  采样 1: main → tcp_write → skb_copy_datagram               │
  │  采样 2: main → tcp_write → tcp_transmit_skb                │
  │  采样 3: main → socket_write → tcp_write → ...              │
  │  采样 4: idle → do_idle → cpuidle_enter                     │
  │  ...(数千个采样)                                           │
  └──────────────────────────────────────────────────────────────┘

  阶段 2:栈折叠(perf script | stackcollapse-perf.pl)
  ┌──────────────────────────────────────────────────────────────┐
  │  main;tcp_write;skb_copy_datagram  42                       │
  │  main;tcp_write;tcp_transmit_skb   31                       │
  │  main;socket_write;tcp_write       12                       │
  │  idle;do_idle;cpuidle_enter        156                      │
  └──────────────────────────────────────────────────────────────┘

  阶段 3:SVG 渲染(flamegraph.pl)
  ┌──────────────────────────────────────────────────────────────┐
  │  ████████████████ idle ████████████████████████████         │
  │  ████████████████████████████████ main ████████████         │
  │             ████████ socket_write ██                        │
  │  ████████████████████ tcp_write █████████████               │
  │  ████ tcp_transmit █████ skb_copy █████ ...                 │
  └──────────────────────────────────────────────────────────────┘

  解读规则:
  - X 轴:函数占用的总 CPU 时间比例(宽度 = 频率)
  - Y 轴:调用深度(越高 = 越深的调用栈)
  - 颜色:随机(用于区分,无特殊含义,暖色=CPU,冷色=内存 等变体)
  - 顶部函数:实际消耗 CPU 的函数(宽顶 = 热点)

  off-CPU 火焰图:
  - 采集线程阻塞时的调用栈(追踪睡眠延迟)
  - 使用 bpftrace 的 offcputime.bt 或 BCC 的 offcputime

10.4 /proc/kallsyms 符号解析

/proc/kallsyms 是内核所有已知符号的映射表:

/proc/kallsyms 格式:

  ffffffff811234ab T tcp_sendmsg
  ffffffff81124560 T tcp_recvmsg
  ffffffff81ab1234 t __schedule   (小写 t = 模块局部符号)
  ffffffffc0012345 T ext4_write_begin [ext4]  (模块函数)

生成机制:
  1. 内核编译时,nm vmlinux 导出所有符号及地址
  2. scripts/kallsyms 生成 .S 汇编文件
  3. 链接进内核的 __ksymtab 和 kallsyms_* 数组

权限控制:
  - root 可见全部地址
  - 非 root 默认地址显示为 0(kptr_restrict=1)
  - kptr_restrict=2 时连 root 也看不到(高安全环境)

使用示例:
  # 查找函数地址
  grep "T tcp_sendmsg" /proc/kallsyms

  # perf/bpftrace 用于 IP → 符号名的反向查找
  addr2line -e /proc/kcore 0xffffffff811234ab

10.5 /proc/kcore 内核内存读取

/proc/kcore 以 ELF core dump 格式暴露内核虚拟地址空间,用于调试器访问内核状态:

/proc/kcore 使用:

  # 查看内核版本字符串(在内核符号表中找到地址后)
  gdb /usr/lib/debug/boot/vmlinux-$(uname -r) /proc/kcore
  (gdb) x/s &linux_banner

  # crash 工具使用 kcore
  crash /usr/lib/debug/vmlinux /proc/kcore

限制:
  - 需要 CAP_SYS_RAWIO 权限
  - 只读访问(防止内核内存损坏)
  - 部分地址不可读(I/O 内存、模块卸载后等)
  - 大小 = 物理内存大小(但不是物理快照)

10.6 trace-cmd:ftrace 的前端

trace-cmd 封装了 tracefs 接口,提供类似 perf 的命令行体验:

# 记录函数调用
trace-cmd record -p function_graph -g tcp_sendmsg sleep 1
trace-cmd report

# 记录 tracepoint
trace-cmd record -e sched:sched_switch sleep 5
trace-cmd report | head -50

# 实时查看(streaming)
trace-cmd stream -e net:netif_receive_skb

# Kernelshark GUI 分析
kernelshark trace.dat

10.7 SystemTap 对比 eBPF

SystemTap vs eBPF 对比:

  SystemTap(老技术):
  ┌───────────────────────────────────────────────────────┐
  │  .stp 脚本 → 翻译为 C 代码 → GCC 编译 → 内核模块 .ko │
  │  模块插入内核(insmod)→ 执行追踪 → 移除模块          │
  │                                                       │
  │  优势:表达能力强,接近 C 语言,可调用内核函数        │
  │  劣势:需要内核头文件/debuginfo,编译慢,有崩溃风险   │
  └───────────────────────────────────────────────────────┘

  eBPF(现代):
  ┌───────────────────────────────────────────────────────┐
  │  .c 文件(受限 C)→ Clang 编译 → BPF 字节码           │
  │  verifier 验证 → JIT 编译 → 注入内核                  │
  │                                                       │
  │  优势:安全(verifier 保证),快速加载,无崩溃风险     │
  │        现代内核原生支持,CO-RE 跨版本兼容              │
  │  劣势:受限 C(无法调用任意函数),程序大小限制        │
  └───────────────────────────────────────────────────────┘

11. 子系统协作与数据流

11.1 完整数据流图

追踪事件产生到用户空间消费的完整路径:

  ┌─────────────────────────────────────────────────────────────────────┐
  │                    内核事件产生层                                     │
  │                                                                     │
  │  [硬件 PMU]  [软件计数器]  [tracepoint]  [kprobe int3]  [uprobe]   │
  │       │            │            │              │            │       │
  └───────┼────────────┼────────────┼──────────────┼────────────┼───────┘
          │            │            │              │            │
          ▼            ▼            ▼              ▼            ▼
  ┌─────────────────────────────────────────────────────────────────────┐
  │                    BPF 程序层(可选)                                 │
  │  BPF 程序可附着到上述任意事件源                                       │
  │  执行自定义逻辑:过滤、聚合、计数、关联                               │
  └─────────────────────────┬───────────────────────────────────────────┘
                            │
          ┌─────────────────┴──────────────────┐
          │                                    │
          ▼                                    ▼
  ┌───────────────────┐              ┌──────────────────────┐
  │  ftrace ring buf  │              │  perf ring buffer    │
  │  (per-CPU)        │              │  (mmap 到用户空间)   │
  │  kernel/trace/    │              │  kernel/events/      │
  │  ring_buffer.c    │              │  core.c              │
  └─────────┬─────────┘              └──────────┬───────────┘
            │                                   │
            ▼                                   ▼
  /sys/kernel/tracing/trace          perf.data 文件 / mmap
            │                                   │
            ▼                                   ▼
  trace-cmd / cat trace              perf report / bpftrace

11.2 追踪子系统间的集成关系

子系统集成关系图:

  ftrace ←──────────────────── kprobe(KPROBE_FLAG_FTRACE)
    │                              │
    │  function tracer              │
    │  (mcount/fentry)              │ 当 kprobe 位于函数入口时
    ▼                              │ 复用 ftrace 机制提高效率
  ftrace_ops                       │
    │                              ▼
    │                          kprobe_ftrace_handler()
    │
    ▼
  perf_event ←──────────────── tracepoint
    │                          perf_trace_<name>() 作为 probe 注册
    │                          tracepoint 事件通过 perf 输出
    ▼
  perf ring buffer
    │
    ├── BPF 程序附着(PROG_TYPE_PERF_EVENT)
    │   在 PMI handler 中执行
    │
    └── 用户空间 mmap 消费

  eBPF ←── 统一附着到:
    ├── kprobe/kretprobe(通过 perf_event)
    ├── uprobe/uretprobe(通过 perf_event)
    ├── tracepoint(通过 perf_event 或 raw_tp)
    ├── fentry/fexit(通过 ftrace)
    └── perf PMU 事件(采样时执行)

11.3 追踪开销层次

各追踪机制开销对比(由低到高):

  1. 无任何追踪(baseline)
     └── 0 ns

  2. tracepoint 未激活
     └── ~0 ns(1 条已预测的 jmp 指令)

  3. tracepoint 激活但无数据
     └── ~10-20 ns(static_key + 函数调用开销)

  4. ftrace function tracer(激活状态)
     └── ~50-100 ns(per 函数调用)

  5. kprobe(int3 机制)
     └── ~500 ns - 1 µs(int3 异常处理开销)

  6. kprobe + BPF 程序(简单)
     └── ~1-3 µs(int3 + BPF JIT 执行)

  7. uprobe
     └── ~2-5 µs(信号传递 + ptrace 类开销)

  8. SystemTap(内核模块方式)
     └── ~1-10 µs(视脚本复杂度)

  说明:上述数值依赖于具体硬件和工作负载

12. 生产实践与调优建议

12.1 选择合适的追踪机制

决策流程图:

  需要追踪内核行为?
  │
  ├─ 是否已有 tracepoint?
  │   └─ 是 → 优先使用 tracepoint
  │           性能最好,ABI 稳定
  │
  ├─ 需要任意位置追踪?
  │   └─ 是 → 使用 kprobe
  │           考虑 KPROBE_FLAG_FTRACE 优化
  │
  ├─ 需要追踪用户程序?
  │   └─ 是 → 使用 uprobe
  │
  ├─ 需要复杂过滤/聚合逻辑?
  │   └─ 是 → 使用 eBPF
  │           BPF maps 在内核中聚合,减少用户空间开销
  │
  └─ 需要函数调用图?
      └─ 是 → ftrace function_graph tracer

12.2 ring buffer 大小调优

# 查看当前 buffer 大小
cat /sys/kernel/tracing/buffer_size_kb

# 设置每 CPU buffer(根据事件频率调整)
echo 16384 > /sys/kernel/tracing/buffer_size_kb  # 16MB per CPU

# 对于高频事件,考虑使用 per_cpu_buffer_size_kb
echo 65536 > /sys/kernel/tracing/per_cpu_buffer_size_kb

# 使用 buffer_percent 控制水位告警
echo 50 > /sys/kernel/tracing/buffer_percent

12.3 kprobe 最佳实践

// 1. 优先指定 symbol_name 而非地址(可读性 + 模块地址无关)
struct kprobe kp = {
    .symbol_name = "tcp_sendmsg",
    .pre_handler  = handler_pre,
};

// 2. maxactive 设置(kretprobe 并发实例数)
struct kretprobe rp = {
    .kp.symbol_name = "vfs_read",
    .handler        = ret_handler,
    .entry_handler  = entry_handler,
    .data_size      = sizeof(struct my_data),
    .maxactive      = num_online_cpus() * 2,  // 适当冗余
};

// 3. 检查 nmissed(采样丢失监控)
if (rp.nmissed > 0)
    pr_warn("kretprobe missed %d instances\n", rp.nmissed);

// 4. 使用 __kprobes 属性防止在 handler 内部递归
static __kprobes int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
    // 此函数本身不会被 kprobe 探测
    ...
}

12.4 perf 采样频率选择

采样频率权衡:

  低频(10-99 Hz):
  ✓ 开销极低(< 0.1%)
  ✓ 适合长时间(小时级)持续监控
  ✗ 可能错过短暂热点(< 10ms 的函数)

  中频(99-999 Hz):
  ✓ 良好精度(统计显著性)
  ✓ 开销可接受(~0.1-1%)
  ✓ 大多数场景的推荐设置
  推荐:-F 99(避免整数 Hz 与时钟频率共振)

  高频(> 1000 Hz):
  ✓ 高精度,可发现短暂热点
  ✗ 开销显著(> 1%)
  ✗ ring buffer 可能溢出
  适用:短时间(< 60 秒)精确分析

  自适应频率:
  perf record -F 99 --freq          # 系统自动维持目标频率

12.5 tracepoint 事件过滤

# 查看可用事件
ls /sys/kernel/tracing/events/

# 启用特定子系统的所有事件
echo 1 > /sys/kernel/tracing/events/net/enable

# 启用单个事件
echo 1 > /sys/kernel/tracing/events/sched/sched_switch/enable

# 添加过滤条件(避免不必要的数据)
echo 'prev_pid == 1234' > /sys/kernel/tracing/events/sched/sched_switch/filter

# 触发器(条件触发 snapshot)
echo 'latency > 1000000:snapshot' > \
    /sys/kernel/tracing/events/irq/irq_handler_exit/trigger

12.6 调试复杂性能问题的工具组合

典型性能问题诊断流程:

  问题:Web 服务 P99 延迟偶发升高
  │
  ├── 第一步:宏观定位(perf stat)
  │   perf stat -p <pid> -I 1000
  │   观察:ipc、cache-miss-rate、context-switch 频率
  │
  ├── 第二步:热点函数(perf record + report)
  │   perf record -g -F 99 -p <pid> -- sleep 30
  │   perf report --stdio | head -50
  │   查看:哪些函数消耗了大量 CPU
  │
  ├── 第三步:系统调用分析(tracepoint)
  │   perf trace -p <pid> -s    # 类似 strace,但基于 tracepoint
  │   查看:系统调用分布和延迟
  │
  ├── 第四步:调度延迟(bpftrace)
  │   bpftrace -e '
  │     tracepoint:sched:sched_switch /args->next_pid == target/ {
  │         @wakeup_lat = hist(nsecs - @ts[args->next_pid]);
  │     }'
  │   查看:进程被唤醒后多久才真正运行
  │
  └── 第五步:锁竞争分析(BCC offwaketime)
      offwaketime -p <pid> 30
      生成 off-CPU 火焰图,显示阻塞原因

12.7 安全注意事项

生产环境追踪安全准则:

  1. kprobe 潜在风险:
     - 探测关键路径函数(调度器、中断处理)可能引发死锁
     - kprobe 自身在 kprobe handler 内不可被探测
     - 检查 kprobe_blacklist 确认目标函数可探测

  2. BPF verifier 的限制是安全保证:
     - 不要试图绕过 verifier(stable kernel 上 verifier 漏洞很少)
     - verifier 拒绝的程序通常有潜在安全/稳定问题

  3. 权限要求:
     - kprobe/tracepoint:CAP_SYS_ADMIN 或 kernel.perf_event_paranoid <= 1
     - perf_event_open:kernel.perf_event_paranoid 控制
     - BPF 加载:CAP_BPF 或 CAP_SYS_ADMIN
     - /proc/kallsyms:kernel.kptr_restrict

  4. ring buffer 大小:
     - 生产环境默认 4MB per CPU,高频场景适当增大
     - 过大的 ring buffer 可能影响内存压力

  5. 追踪关闭验证:
     - 追踪结束后确认清理:echo 0 > /sys/kernel/tracing/tracing_on
     - kprobe 注销:unregister_kprobe()
     - BPF link fd 关闭

附录:关键文件速查

文件 作用
kernel/trace/trace.h ftrace 核心数据结构(trace_arrayarray_buffer 等)
kernel/trace/trace.c ftrace 主实现(tracefs 接口、全局初始化)
kernel/trace/ring_buffer.c 无锁 per-CPU ring buffer 实现
kernel/trace/trace_functions.c function tracer 实现
kernel/trace/trace_events.c trace event 注册和管理
kernel/kprobes.c kprobe 核心实现(register_kprobe 等)
kernel/uprobes.c uprobe 核心实现
kernel/events/core.c perf_event_open() 及 perf 核心逻辑
kernel/bpf/verifier.c BPF 安全验证器
include/linux/kprobes.h struct kprobestruct kretprobe
include/linux/tracepoint.h DECLARE_TRACEDEFINE_TRACE
include/linux/tracepoint-defs.h struct tracepointstruct tracepoint_func
include/linux/perf_event.h struct perf_eventstruct pmu
include/linux/uprobes.h struct uprobe_consumerstruct uprobe_task
include/linux/dynamic_debug.h struct _ddebug
include/uapi/linux/bpf.h BPF 程序类型、map 类型、helper 定义
include/trace/events/ 内核预置 trace event 定义
arch/x86/kernel/kprobes/ x86 kprobe 架构实现(int3 替换)

由 Claude Code 分析生成