基于 Linux Kernel 源码深度分析(内核版本 6.x) 核心路径:
kernel/trace/|kernel/kprobes.c|include/linux/tracepoint.hinclude/linux/kprobes.h|include/linux/perf_event.h|include/linux/uprobes.h
- 追踪子系统全景架构
- ftrace 框架深度解析
- ring buffer:无锁环形缓冲区实现
- kprobe 与 kretprobe:内核动态探针
- tracepoint:静态追踪点机制
- perf events 框架
- uprobe:用户态动态探针
- eBPF 追踪程序
- 动态调试(dyndbg)
- 系统整体可观测性工具链
- 子系统协作与数据流
- 生产实践与调优建议
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 ║
╚══════════════════════════════════════════════════════════════════════════╝
| 子系统 | 插桩时机 | 开销 | 灵活性 | 典型场景 |
|---|---|---|---|---|
| ftrace | 编译期(mcount/fentry 桩) | 极低 | 中 | 函数调用追踪、延迟分析 |
| tracepoint | 编译期(静态插桩点) | 近零(未启用时) | 低 | 预定义事件、稳定 ABI |
| kprobe | 运行时(断点替换) | 低~中 | 极高 | 任意内核地址探测 |
| uprobe | 运行时(用户空间断点) | 中 | 高 | 用户程序函数追踪 |
| perf events | 硬件 PMU / 软件计数器 | 硬件级 | 高 | 性能计数、采样分析 |
| eBPF | 运行时(JIT 编译附着) | 低 | 极高 | 可编程追踪逻辑 |
- 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 程序支持 - 2020:
BPF_MAP_TYPE_RINGBUF新 ring buffer 接口引入 - 2022:fentry/fexit BPF 程序,性能大幅提升
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;
};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,
};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)实现函数替换的基础。
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 */
用户通过 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_ 函数
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 # 读取快照数据
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_array(kernel/trace/trace.h:331),拥有独立的 ring buffer、过滤器和追踪器状态。
所有追踪记录都通过 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; // 返回地址
};kernel/trace/ring_buffer.c 实现了一个满足以下约束的高性能环形缓冲区:
- NMI 安全:可在 NMI 上下文写入,不能使用任何锁
- per-CPU:每个 CPU 独立 buffer,写入无竞争
- 多读者:支持多个并发读者
- 溢出策略:覆盖模式(circular)或丢弃模式(discard)
- 时间戳压缩:delta 时间戳节省空间
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(读取位置) │
└─────────────────────────────────────────────────────────────────┘
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 偏移数组
};每个 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): 绝对时间戳
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)与写者页分离,通过页指针交换实现
读者获取数据的流程:
┌──────────────────────────────────────────────────────────────┐
│ 每个 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 循环(被写者复用) │
└──────────────────────────────────────────────────────────────┘
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 // 探针位于函数入口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)
};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)。
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
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;
}某些内核函数不能被探测(会导致递归或系统崩溃),通过 __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 处理程序等。
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 上下文中使用
};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
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 被引用
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 调用开销
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 阶段)
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
...
};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);
};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)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)
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; // 当前节流周期的中断次数
};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
当 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 暴露物理地址
与 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 - 记录大小 │
└─────────────────────────────────────────────────────────────┘
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
};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;
};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 到原指令之后的正确地址 │
└───────────────────────────────────────────────────────────────┘
内核引入了复杂的生命周期管理来保证 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; // 缓存行对齐每个进程的 mm_struct 中有 uprobes_state(include/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,用于存放原指令副本。每个线程在此区域有独立的槽位,保证并发探针的独立执行。
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 程序
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")
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(引用计数归零时自动清理) │
└──────────────────────────────────────────────────────────────┘
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() 消费
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);
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)
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),无需手动计算寄存器偏移
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 要求)// 源码中的使用:
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,内核启动时作为数组统一处理。
通过 /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 显示调用栈
- 取消对应标志
= 设置为指定值(清除其他)
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/wakeupinclude/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/controlperf 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):函数及其调用的子函数总耗时占比
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();
}'
火焰图(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
/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
/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 内存、模块卸载后等)
- 大小 = 物理内存大小(但不是物理快照)
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.datSystemTap vs eBPF 对比:
SystemTap(老技术):
┌───────────────────────────────────────────────────────┐
│ .stp 脚本 → 翻译为 C 代码 → GCC 编译 → 内核模块 .ko │
│ 模块插入内核(insmod)→ 执行追踪 → 移除模块 │
│ │
│ 优势:表达能力强,接近 C 语言,可调用内核函数 │
│ 劣势:需要内核头文件/debuginfo,编译慢,有崩溃风险 │
└───────────────────────────────────────────────────────┘
eBPF(现代):
┌───────────────────────────────────────────────────────┐
│ .c 文件(受限 C)→ Clang 编译 → BPF 字节码 │
│ verifier 验证 → JIT 编译 → 注入内核 │
│ │
│ 优势:安全(verifier 保证),快速加载,无崩溃风险 │
│ 现代内核原生支持,CO-RE 跨版本兼容 │
│ 劣势:受限 C(无法调用任意函数),程序大小限制 │
└───────────────────────────────────────────────────────┘
追踪事件产生到用户空间消费的完整路径:
┌─────────────────────────────────────────────────────────────────────┐
│ 内核事件产生层 │
│ │
│ [硬件 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
子系统集成关系图:
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 事件(采样时执行)
各追踪机制开销对比(由低到高):
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(视脚本复杂度)
说明:上述数值依赖于具体硬件和工作负载
决策流程图:
需要追踪内核行为?
│
├─ 是否已有 tracepoint?
│ └─ 是 → 优先使用 tracepoint
│ 性能最好,ABI 稳定
│
├─ 需要任意位置追踪?
│ └─ 是 → 使用 kprobe
│ 考虑 KPROBE_FLAG_FTRACE 优化
│
├─ 需要追踪用户程序?
│ └─ 是 → 使用 uprobe
│
├─ 需要复杂过滤/聚合逻辑?
│ └─ 是 → 使用 eBPF
│ BPF maps 在内核中聚合,减少用户空间开销
│
└─ 需要函数调用图?
└─ 是 → ftrace function_graph tracer
# 查看当前 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// 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 探测
...
}采样频率权衡:
低频(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 # 系统自动维持目标频率
# 查看可用事件
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典型性能问题诊断流程:
问题: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 火焰图,显示阻塞原因
生产环境追踪安全准则:
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_array、array_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 kprobe、struct kretprobe |
include/linux/tracepoint.h |
DECLARE_TRACE、DEFINE_TRACE 宏 |
include/linux/tracepoint-defs.h |
struct tracepoint、struct tracepoint_func |
include/linux/perf_event.h |
struct perf_event、struct pmu |
include/linux/uprobes.h |
struct uprobe_consumer、struct 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 分析生成