4. kprobe 参数配置:struct kprobe 成员详解
kprobe 的核心数据结构是 struct kprobe,它定义了探针的插入位置、回调函数以及行为控制。正确配置这些成员是驱动性能调优的第一步。本章将逐一解析关键成员,并结合实战场景说明配置要点。
4.1 struct kprobe 核心成员一览
下表列出了 struct kprobe 中最常用的成员及其作用:
| 成员名称 | 类型 | 说明 | 配置要求 |
|---|---|---|---|
symbol_name |
const char * |
被探测的内核函数名称 | 必须指定,或与 offset 配合使用 |
offset |
unsigned long |
函数内的偏移字节数 | 可选,默认为 0(函数入口) |
pre_handler |
kprobe_pre_handler_t |
指令执行前调用的回调 | 至少实现一个处理函数 |
post_handler |
kprobe_post_handler_t |
指令执行后调用的回调 | 可选,用于采集返回值 |
fault_handler |
kprobe_fault_handler_t |
处理回调中发生的异常 | 强烈建议实现,防止系统崩溃 |
addr |
kprobe_opcode_t * |
直接指定探测地址 | 与 symbol_name 二选一 |
实战建议:在性能调优场景中,优先使用
symbol_name + offset 组合,避免直接操作 addr,以保持内核版本兼容性。
4.2 symbol_name 与 offset:精准定位探测点
symbol_name 指定要探测的内核函数名称,offset 则允许你在函数内部偏移指定字节数后插入探针。两者配合可以实现从函数入口到内部任意指令位置的精确探测。
关键规则:
- 如果只设置
symbol_name,探针默认插入函数第一条指令(偏移 0)。 - 设置
offset时,必须确保偏移位置是一条指令的起始地址,否则注册会失败。 - 对于内联函数或静态函数,
symbol_name可能无法直接解析,此时需使用addr或通过/proc/kallsyms确认符号是否存在。
以下是一个典型的配置示例:
static struct kprobe kp = {
.symbol_name = "tcp_v4_connect",
.offset = 0x10, // 在函数入口后第16字节处插入探针
.pre_handler = my_pre_handler,
.post_handler = my_post_handler,
.fault_handler = my_fault_handler,
};
注意:偏移值
0x10 是十六进制,表示 16 字节。不同内核版本、不同编译选项下,同一函数的指令布局可能不同,建议先通过反汇编确认偏移位置的指令类型。
4.3 pre_handler 与 post_handler:采集调用频次与参数
pre_handler 在探测指令执行前被调用,post_handler 在指令执行后被调用。两者是性能数据采集的核心入口。
4.3.1 pre_handler:捕获输入参数
pre_handler 的原型为:
int (*pre_handler)(struct kprobe *p, struct pt_regs *regs);
通过 regs 可以读取函数参数。在 x86_64 架构下,前 6 个参数分别位于 rdi、rsi、rdx、rcx、r8、r9 寄存器中。
性能调优场景:统计
tcp_v4_connect 的调用频次,并记录目标 IP 地址(第二个参数)。
static int my_pre_handler(struct kprobe *p, struct pt_regs *regs)
{
struct sockaddr *uaddr = (struct sockaddr *)regs->si;
// 记录调用计数
atomic64_inc(&call_count);
// 记录目标 IP(仅示例,实际需做地址校验)
printk("connect to %pISc\n", uaddr);
return 0;
}
4.3.2 post_handler:捕获返回值
post_handler 的原型为:
void (*post_handler)(struct kprobe *p, struct pt_regs *regs, unsigned long flags);
返回值保存在 regs->ax(x86_64)中。通过 post_handler 可以统计函数执行成功/失败的比例。
static void my_post_handler(struct kprobe *p, struct pt_regs *regs,
unsigned long flags)
{
long ret = regs->ax;
if (ret < 0)
atomic64_inc(&fail_count);
else
atomic64_inc(&success_count);
}
4.4 fault_handler:保障系统稳定性
回调函数中可能访问无效内存或触发异常,fault_handler 用于捕获这些异常并决定是否继续执行。其原型为:
int (*fault_handler)(struct kprobe *p, struct pt_regs *regs, int trapnr);
- 返回 0:表示异常已处理,kprobe 继续执行原始指令。
- 返回非 0:表示异常未处理,kprobe 将异常传播给原始处理流程。
必须实现 fault_handler 的场景:
- 在 pre_handler 中访问用户态指针(如
copy_from_user的替代场景)。 - 在 pre_handler 中执行复杂计算或内存分配。
- 探测点位于频繁调用的热路径上,异常可能导致系统不稳定。
一个安全的 fault_handler 实现示例:
static int my_fault_handler(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
// 记录异常信息
printk("kprobe fault at IP: %lx, trap: %d\n", regs->ip, trapnr);
// 返回 0 表示异常已处理,继续执行
return 0;
}
4.5 成员配置关系与注册流程
下图展示了 struct kprobe 各成员在注册和回调过程中的协作关系:
4.6 成员配置实战对比
下表总结了不同配置组合的适用场景:
| 配置组合 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
symbol_name 仅入口 |
统计函数调用频次、捕获所有参数 | 配置简单,兼容性好 | 无法探测函数内部逻辑 |
symbol_name + offset |
探测函数内部特定指令位置 | 灵活定位,可绕过函数头 | 需反汇编确认偏移,版本敏感 |
addr 直接指定 |
探测非符号地址(如模块内) | 可探测任意指令地址 | 地址随版本变化,维护成本高 |
仅实现 pre_handler |
只关心输入参数和调用频次 | 开销最小,适合高频探测 | 无法获取返回值 |
实现 pre + post |
需要完整记录调用输入输出 | 数据完整,可分析执行结果 | 额外开销,需注意性能影响 |
实现 fault_handler |
探测点可能触发异常 | 保障系统稳定性 | 增加代码复杂度 |
4.7 性能调优中的配置策略
在性能调优场景下,配置 struct kprobe 应遵循以下原则:
- 最小化回调逻辑:pre_handler 和 post_handler 中只做必要的计数和参数记录,避免复杂计算或内存分配。
- 优先使用 atomic 操作:统计调用频次时使用
atomic64_inc而非锁保护的自增。 - 按需实现 fault_handler:如果回调中访问用户态指针或执行可能失败的操作,务必实现 fault_handler。
- 合理选择 offset:在函数内部偏移探测时,优先选择不会频繁跳过的指令位置,避免漏统计。
实战技巧:在调试阶段,可以在 pre_handler 中打印
regs->ip 确认探测点实际地址,与预期偏移对比验证。