土法炼钢兴趣小组的算法知识备份

【eBPF 内核实现深度拆解】蹦床(Trampoline)与 fentry / fexit:零开销内核追踪

文章导航

分类入口
kernelebpf
标签入口
#ebpf#fentry#fexit#trampoline#bpf_trampoline#kprobe#struct_ops#fentry-fexit#linux-kernel

目录

用 kprobe 追踪 tcp_rcv_established() ——每条 TCP 包到达都触发一次 int3 断点、进入异常处理、切换到 BPF 程序、再 iret 回来。内核文档 Documentation/bpf/bpf_devel_QA.rst 和社区 benchmark 给出的固定开销量级约为 100–300ns(不含 BPF 程序本身);在高吞吐服务器上,每秒数百万次调用的函数被 kprobe 挂载后,CPU 占用可能显著上升。这不是理论上的”可能有影响”——在生产环境中,不恰当的 kprobe 挂载曾导致丢包和延迟抖动。(以下性能数字均引用该文档及社区测试,非本站实测。)

fentry/fexit(Linux 5.5+)通过 BPF 蹦床(trampoline)机制,把 “中断→上下文切换→BPF→恢复” 替换为 “call→蹦床→BPF→ret”。同一来源给出的 fentry 固定开销量级约为 10–20ns,相对 kprobe 约一个数量级(~10x)。本文将 fentry/fexit 的 “零开销” 完整拆开:bpf_trampoline 的内核数据结构、arch_prepare_bpf_trampoline() 如何生成架构相关的栈帧代码、struct_ops 如何复用蹦床机制、以及什么情况下蹦床的开销并非为零。

一、kprobe 的开销来自哪里

kprobe 的机制决定了它必须走异常处理路径。在函数入口处,kprobe 将目标指令的第一个字节替换为 int3(x86 的断点指令,单字节 0xCC):

/* Linux 6.6: arch/x86/kernel/kprobes/core.c */
/* kprobe 使用 text_poke 将目标地址的首字节替换为 0xCC */
int __kprobes arch_arm_kprobe(struct kprobe *p)
{
    text_poke(p->addr, ((unsigned char []){INT3_INSN_OPCODE}), 1);
    ...
}

当 CPU 执行流到达被替换的 int3 时,触发完整的异常处理链路:目标函数入口 -> int3 陷阱(trap gate)-> do_int3() (arch/x86/kernel/traps.c) -> kprobe_int3_handler() -> pre_handler(BPF kprobe 程序)-> post_handler(可选)-> iret 返回原函数继续。 每一步都有不可忽略的成本:

合计:不包含 BPF 程序本身的执行,kprobe 的固定开销约 80–130 个周期(bpf_devel_QA.rst 给出的量级约 100–300ns @ 3GHz,非本站实测)。如果挂钩点是每条包路径都要经过的函数(如 __netif_receive_skb_core()),这足够将软中断处理时间撑大一个数量级。

kprobe 的另一个问题是它修改的指令必须保存并模拟执行。对于 PC 相对寻址指令(如 x86 的 jmpcall),kprobe 需要模拟其行为——模拟错误会导致内核崩溃。

二、fentry 的机制:nop sled 替换

fentry 的机制完全不同。它不靠异常路径,而是直接利用内核编译时预留的 nop sled

2.1 编译期的 nop sled 预留

当内核编译时启用了 CONFIG_FTRACECONFIG_FUNCTION_TRACER,编译器为每个内核函数生成入口 nop 指令。这通过 GCC/Clang 的 -fpatchable-function-entry=N 参数实现:

/* 编译参数(x86-64):
 * -fpatchable-function-entry=5,5
 * 效果:在函数入口放置 5 个 nop(每个 1 字节),
 *       后面紧跟一条 5 字节 nop(作为原始入口标记)
 */

编译后,每个内核函数的开始是这样的(x86-64):

/* 假设内核函数 kfree() 的编译后代码 */
<kfree>:
    nop                    /* fentry trampoline 的 call 目标 */
    nop
    nop
    nop
    nop
    /* --- 原始函数入口 --- */
    endbr64                /* CET 标记 */
    push   %rbp            /* 正常函数体开始 */
    mov    %rsp,%rbp
    ...

nop 序列替代了传统的 mcount/fentry 调用点。Linux 5.5+ 使用这 5 字节 nop 作为 BPF fentry 的挂载点。

2.2 挂载过程:从 nop 到 call

fentry/fexit 程序类型为 BPF_PROG_TYPE_TRACING,通过 bpf_link(如 BPF_TRACE_FENTRY / BPF_TRACE_FEXIT)或 libbpf 的 bpf_program__attach_fentry() 挂载。挂载时内核调用 bpf_trampoline_link_prog()BPF_RAW_TRACEPOINT_OPEN 用于 raw tracepoint,不用于 fentry):

/* Linux 6.6: kernel/bpf/trampoline.c */
int bpf_trampoline_link_prog(struct bpf_tramp_link *link,
                              struct bpf_trampoline *tr)
{
    enum bpf_tramp_prog_type kind;
    int err = 0;
    ...
    /* 分配 trampoline 镜像内存 */
    if (!tr->cur_image) {
        err = bpf_trampoline_update(tr, true);
        ...
    }
    ...
}

bpf_trampoline_update() 是核心。它将生成的蹦床代码通过 arch_prepare_bpf_trampoline() 写入 trampoline 镜像,然后通过 text_poke_bp() 将原始函数的 nop 替换为 call <trampoline>

text_poke_bp 是 x86 的安全代码修改机制——它用一个三步流程确保多核安全:

  1. 在目标地址写入一条 int3(一个字节,原子操作),停止其他 CPU 在此执行
  2. 修改 int3 后面的字节(剩余的 4 字节地址)
  3. int3 替换为 call 的首字节(0xE8
/* Linux 6.6: kernel/bpf/trampoline.c */
static int bpf_trampoline_update(struct bpf_trampoline *tr, bool lock_direct_mutex)
{
    void *trampoline = NULL;
    int err;
    ...
    /* 生成蹦床代码 */
    err = arch_prepare_bpf_trampoline(im, im->image, im->image + PAGE_SIZE,
                                      &tr->func.model, tr->fops,
                                      fentry_cnt, fexit_cnt,
                                      &tr->func.addr);
    ...
    /* 通过 text_poke_bp 替换 nop -> call */
    err = bpf_arch_text_poke(tr->func.addr, BPF_MOD_CALL,
                             NULL, im->image + image_off);
    ...
}

替换完成后,内核函数的执行流变为:

sequenceDiagram
    participant K as 内核函数
    participant T as 蹦床 (Trampoline)
    participant B as BPF 程序

    Note over K: 原始函数入口
    K->>T: call <trampoline><br/>(原 nop 被替换)
    T->>T: 保存 caller-saved 寄存器<br/>(rdi,rsi,rdx,rcx,r8,r9,rax)
    T->>T: 分配 BPF 栈帧
    T->>T: 构造 ctx (参数结构体)
    T->>B: call BPF fentry 程序
    B-->>T: return
    T->>T: 恢复寄存器
    T->>T: 恢复栈帧
    T-->>K: ret<br/>返回原始函数体

    Note over K: 原始函数体继续执行

与 kprobe 的对比——kprobe 的执行路径:

sequenceDiagram
    participant K as 内核函数
    participant CPU as CPU 异常处理
    participant KP as kprobe handler
    participant B as BPF 程序

    Note over K: 原始函数入口
    K->>CPU: int3 断点触发
    CPU->>CPU: 压栈 SS/RSP/RFLAGS/CS/RIP<br/>(40 字节)
    CPU->>CPU: 加载 IDT 条目
    CPU->>CPU: 切换内核栈
    CPU->>KP: do_int3() → kprobe_int3_handler()
    KP->>KP: RCU 锁定
    KP->>B: call BPF kprobe 程序
    B-->>KP: return
    KP->>KP: RCU 解锁
    KP->>CPU: iret
    CPU->>CPU: 恢复上下文
    CPU-->>K: 跳回原始指令

    Note over K: 原始函数体继续执行

fentry 的执行流用文字表示:原始函数入口 -> call <trampoline> -> 蹦床保存寄存器 -> call BPF fentry 程序 -> 蹦床恢复寄存器 -> ret -> 原始函数体继续执行。

对于 fexit,蹦床在函数返回前执行。内核在函数的 ret 指令位置生成额外的蹦床代码:原始函数体执行完毕 -> ret 之前,蹦床在栈帧中插入出口代码 -> 蹦床保存寄存器并捕获返回值 -> call BPF fexit 程序 -> 蹦床恢复寄存器 -> ret 返回原始调用者。

三、bpf_trampoline 数据结构

蹦床的核心数据结构定义在 include/linux/bpf.h

/* Linux 6.6: include/linux/bpf.h */
struct bpf_trampoline {
    struct hlist_node hlist;         /* 按目标函数地址 hash */
    struct ftrace_ops *fops;         /* ftrace ops(共享 ftrace 基础设施)*/
    struct mutex mutex;
    spinlock_t lock;
    refcount_t refcnt;
    u32 key;                         /* hash key(目标函数地址)*/
    struct bpf_tramp_image *cur_image;
    struct bpf_tramp_links *progs_hlist[BPF_TRAMP_MAX];
    /* prog 按类型组织:
     * [BPF_TRAMP_FENTRY] 链表
     * [BPF_TRAMP_FEXIT]  链表
     * [BPF_TRAMP_MODIFY_RETURN] 链表
     */
    struct bpf_func_model func;
    struct module *mod;
    ...
};

3.1 关键字段解析

progs_hlist:每个蹦床可以挂载多个 BPF 程序,按类型分组。BPF_TRAMP_FENTRY(值为 0)和 BPF_TRAMP_FEXIT(值为 1)分别存储入口和出口程序的链表:

/* Linux 6.6: include/linux/bpf.h */
enum bpf_tramp_prog_type {
    BPF_TRAMP_FENTRY,
    BPF_TRAMP_FEXIT,
    BPF_TRAMP_MODIFY_RETURN,
    BPF_TRAMP_MAX,
};

bpf_func_model:描述目标函数的签名(参数个数、类型、是否有返回值),蹦床生成器用它来决定保存/恢复哪些寄存器和如何传递参数:

/* Linux 6.6: include/linux/bpf.h */
struct bpf_func_model {
    u8 ret_type;        /* 返回值类型 */
    u8 nr_args;         /* 参数个数(最多 6 个) */
    u8 arg_size[6];     /* 每个参数是否为 64-bit */
    u8 ret_size;        /* 返回值大小 */
};

bpf_tramp_image:蹦床的可执行代码镜像。每个蹦床有一块独立的内存页,包含生成的 trampoline 指令:

/* Linux 6.6: include/linux/bpf.h */
struct bpf_tramp_image {
    void *image;            /* 可执行镜像地址 */
    struct bpf_ksym ksym;   /* kallsyms 项(/proc/kallsyms 可见)*/
    struct percpu_ref pcref;
    void *ip_after_call;    /* 返回地址(fexit 使用)*/
    void *ip_epilogue;      /* epilogue 地址 */
    ...
};

3.2 蹦床的生命周期

蹦床的生命周期通过以下函数调用链管理:

所有活跃的蹦床存储在一个全局 hash 表 trampoline_table 中,以目标函数地址为 key:

/* Linux 6.6: kernel/bpf/trampoline.c */
static struct hlist_head trampoline_table[TRAMPOLINE_TABLE_SIZE];

四、arch_prepare_bpf_trampoline():x86-64 栈帧构造

arch_prepare_bpf_trampoline() 是蹦床的最核心实现——它生成实际的 x86 机器码序列。这个函数位于架构相关代码中:

/* Linux 6.6: arch/x86/kernel/bpf_jit.c(其他架构见 arch/*/kernel/bpf_jit.c)*/
int arch_prepare_bpf_trampoline(struct bpf_tramp_image *im, void *image,
                                void *image_end,
                                const struct bpf_func_model *model, u32 flags,
                                struct bpf_tramp_links *tlinks,
                                void *orig_call)

4.1 蹦床的栈帧布局

蹦床在目标函数栈帧的 “上方” 分配自己的栈空间。完整布局从高地址到低地址依次为:

  1. 调用者的栈帧
  2. 返回地址(原始 call 压栈)
  3. 保存的 callee-saved 寄存器(rbxrbpr12--r15)——蹦床保存
  4. 保存的 caller-saved 寄存器(rdirsirdxrcxr8r9rax)——蹦床保存(fentry)
  5. BPF 程序栈(512 字节 BPF 运行时栈)——RSP 指向这里

4.2 生成的 x86 指令序列

arch_prepare_bpf_trampoline() 生成的代码大致如下(简化表示):

/* === 蹦床入口(fentry) === */
/* 第 1 步:为蹦床分配栈空间 */
push   %rbp
mov    %rsp, %rbp
sub    $STACK_SIZE, %rsp         /* 分配蹦床栈帧 */

/* 第 2 步:保存 caller-saved 寄存器 */
/* 将传入目标函数的参数保存到蹦床栈上 */
mov    %rdi, SAVE_RDI(%rsp)
mov    %rsi, SAVE_RSI(%rsp)
mov    %rdx, SAVE_RDX(%rsp)
mov    %rcx, SAVE_RCX(%rsp)
mov    %r8,  SAVE_R8(%rsp)
mov    %r9,  SAVE_R9(%rsp)

/* 第 3 步:构造 BPF 程序上下文(ctx)*/
/* RDI = ctx 指针(指向包含参数的结构体) */
lea    CTX_SLOT(%rsp), %rdi

/* 第 4 步:调用 BPF fentry 程序 */
call   <bpf_prog_fentry_addr>

/* 第 5 步:恢复寄存器 */
mov    SAVE_RDI(%rsp), %rdi
mov    SAVE_RSI(%rsp), %rsi
mov    SAVE_RDX(%rsp), %rdx
mov    SAVE_RCX(%rsp), %rcx
mov    SAVE_R8(%rsp), %r8
mov    SAVE_R9(%rsp), %r9

/* 第 6 步:恢复栈帧并返回 */
leave
ret                                  /* 回到原始函数体 */

对于 fexit,蹦床的入口代码更复杂——它需要修改原始函数的返回路径。fexit 蹦床通过修改函数栈帧来插入出口钩子:

/* === 蹦床入口(fexit 部分)=== */
/* 保存原始返回地址 */
mov    8(%rbp), %rax                /* 读取原始返回地址 */
mov    %rax, SAVED_RET_ADDR(%rsp)

/* 将返回地址替换为 fexit 蹦床的地址 */
lea    FEXIT_TRAMPOLINE(%rip), %rax
mov    %rax, 8(%rbp)                /* 覆盖返回地址 */

/* 继续执行原始函数(函数的 ret 将跳到 FEXIT_TRAMPOLINE)*/

/* === FEXIT_TRAMPOLINE(函数 ret 后到达这里)=== */
/* 第 1 步:保存返回值(在 %rax 中)*/
mov    %rax, SAVED_RAX(%rsp)

/* 第 2 步:保存 caller-saved 寄存器 */
mov    %rdi, SAVE_RDI(%rsp)
...

/* 第 3 步:构造 ctx(包含返回值和参数)*/
lea    CTX_SLOT(%rsp), %rdi

/* 第 4 步:调用 BPF fexit 程序 */
call   <bpf_prog_fexit_addr>

/* 第 5 步:恢复寄存器和返回值 */
mov    SAVED_RAX(%rsp), %rax
mov    SAVE_RDI(%rsp), %rdi
...

/* 第 6 步:跳转到真正的返回地址 */
jmp    *SAVED_RET_ADDR(%rsp)

4.3 多程序执行

单个蹦床可以挂载多个 BPF 程序——多个 fentry 程序和多个 fexit 程序。arch_prepare_bpf_trampoline() 为每个挂载的程序生成一个单独的 call 指令:

/* 有 3 个 fentry 程序时的生成代码 */
/* 保存寄存器... */

/* 第 1 个 fentry 程序 */
lea    CTX_SLOT(%rsp), %rdi
call   <bpf_prog_1>

/* 第 2 个 fentry 程序 */
lea    CTX_SLOT(%rsp), %rdi
call   <bpf_prog_2>

/* 第 3 个 fentry 程序 */
lea    CTX_SLOT(%rsp), %rdi
call   <bpf_prog_3>

/* 恢复寄存器... */
ret

程序按挂载顺序依序执行。每个 fentry 程序可以看到相同的上下文(原始函数参数)。

五、性能特征与开销分析

5.1 fentry vs kprobe 的性能对比

下表对比了 fentry 和 kprobe 在 x86-64 上的单次调用固定开销(不含 BPF 程序本身的执行时间;数据引用 Documentation/bpf/bpf_devel_QA.rst 及社区 benchmark,非本站实测):

指标 kprobe (optimized) fentry 倍率
CPU 周期(固定开销) ~300–500 ~30–50 ~10x
时间(ns,固定开销) ~100–170ns ~10–17ns ~10x
上下文切换 异常处理 + iret 无(直接 call) N/A
中断禁用影响 在中断上下文中可拒绝 probe 无影响 N/A
挂载/卸载耗时 ~μs 级(text_poke 修改 1 字节) ~μs 级(text_poke 修改 5 字节) 相近

实测数据(引用自内核源码 Documentation/bpf/bpf_devel_QA.rst 讨论和社区 benchmark):

5.2 开销”并不为零”的情况

蹦床的零开销是相对 kprobe 而言的。以下情况中,fentry 的开销仍然不可忽略:

高频率调用: 如果目标函数每秒被调用 10M 次,每次 15ns 的蹦床开销 = 150ms CPU 时间/秒 = 15% CPU 占用。在这种情况下,即使是 fentry 也可能需要采样或节流。

多程序串行执行: 每个额外的 fentry/fexit 程序都会增加一次 call 指令 + 上下文构造开销(~5–10ns 每个)。挂载 10 个 fentry 程序后,总开销为 ~50–100ns,接近 kprobe 的开销范围。

fexit 的附加开销: fexit 需要修改栈帧中的返回地址,在函数返回时触发蹦床。这涉及两次栈帧操作(saved return address + restore),额外开销约 ~10–15ns。

cache 效应: 蹦床的代码镜像通常不在 CPU L1 指令缓存中。蹦床调用导致 L1-I cache miss 的成本(~20–30 cycles)可能超过蹦床代码本身的执行时间。在频繁调用的热点函数上,蹦床代码通常会被缓存在 L1 中;但刚挂载或长时间未调用的蹦床可能完全从 cache 中掉出。

六、struct_ops 与蹦床的协作

struct_ops 复用蹦床机制来重定向内核函数指针到 BPF 程序。两者共享相同的 arch_prepare_bpf_trampoline() 基础设施。

6.1 struct_ops 的蹦床模型

struct_ops 的目标不是追踪(tracing),而是替换——BPF 程序完全替代原有的内核函数。例如,tcp_congestion_ops->cong_avoid 被一个 BPF 程序替代后,每次拥塞避免逻辑都执行 BPF 程序而不是内核函数。

/* Linux 6.6: kernel/bpf/bpf_struct_ops.c */
static int bpf_struct_ops_link_update(struct bpf_link *link, ...)
{
    struct bpf_struct_ops_link *st_link;
    ...
    /* 为每个 struct_ops 成员(函数指针)创建蹦床 */
    for (i = 0; i < st_ops->cfi_stubs_size; i++) {
        ...
        err = bpf_trampoline_link_prog(&st_link->tramp_links[i], ...);
    }
    ...
}

struct_ops 蹦床的关键差异:

6.2 BTF 驱动的类型验证

struct_ops 的蹦床生成依赖 BTF 类型信息来验证函数签名匹配:

/* Linux 6.6: kernel/bpf/bpf_struct_ops.c */
static int bpf_struct_ops_map_update_elem(struct bpf_map *map, void *key,
                                          void *value, u64 flags)
{
    ...
    /* 对每个 struct_ops 成员,走 verifier 验证 BPF 程序是否符合签名 */
    for_each_member(i, t) {
        ...
        prog = *(struct bpf_prog **)(udata + moff);
        if (prog) {
            err = bpf_struct_ops_prepare_trampoline(st_map, prog, ...);
        }
    }
    ...
}

七、fentry 的可观测性

7.1 查看活跃的蹦床

# 查看所有蹦床(通过 kallsyms)
grep bpf_trampoline /proc/kallsyms | head -20

# bpftool 查看 fentry/fexit 挂载的程序
bpftool prog list | grep -A2 fentry
bpftool prog list | grep -A2 fexit

7.2 bpftrace 使用 fentry

# fentry: 在 tcp_connect 入口执行
bpftrace -e 'fentry:tcp_connect { printf("pid=%d\n", pid); }'

# 对比 kprobe 版本
bpftrace -e 'kprobe:tcp_connect { printf("pid=%d\n", pid); }'

# 比较两者的调用次数和开销
bpftrace -e '
fentry:tcp_connect { @fentry[tid] = nsecs; }
kprobe:tcp_connect  { @kprobe[tid] = nsecs; }
'

7.3 蹦床是否活跃

# 检查某个函数是否有蹦床挂载
bpftool btf dump file /sys/kernel/btf/vmlinux | grep -A5 'FUNC.*tcp_connect'

# 查看蹦床的 JIT 镜像
bpftool prog dump xlated id <BPF_PROG_ID> | head -50

八、限制与适用边界

8.1 fentry 的限制

必须依赖 BTF: fentry/fexit 只能挂载到 BTF 描述的函数上。内核中所有启用了 ftrace 的函数都有 BTF 信息,但内核模块中的函数、动态生成的代码、以及某些用汇编编写的函数可能没有 BTF 覆盖。

不能修改参数: 与 kprobe / fmod_ret 程序可通过 bpf_override_return() 修改返回值不同,fentry 程序不能修改传入的寄存器值。bpf_override_return() 用于 kprobe 和 BPF_MODIFY_RETURN(fmod_ret)挂钩点,与 map 操作无关。fentry 的限制来自蹦床在调用 BPF 程序后恢复所有寄存器到原始状态。

不支持函数返回值篡改(一般情况下): fentry 在函数入口执行,看不到返回值。fexit 可以观察返回值但不能修改它(除非使用 BPF_MODIFY_RETURN,这是 5.8+ 的独立特性,需要 BPF_TRAMP_MODIFY_RETURN 标志)。

8.2 与 kprobe 的可选对比

场景 推荐 原因
高频函数追踪(>100K calls/s) fentry 相对 kprobe 约 10x 更低开销(引用 bpf_devel_QA.rst
需要修改参数 kprobe fentry 不支持参数修改
追踪非 BTF 函数 kprobe fentry 需要 BTF
追踪函数返回值 fexit 比 kretprobe 开销低 5–10x
追踪内核模块函数 取决于是否导出 BTF 编译时 BTF 决定
perf_event 驱动的采样 kprobe(perf-based) fentry 暂不支持 perf-based 触发

8.3 内核版本兼容性

特性 最低内核版本 说明
fentry/fexit hook 5.5 BPF_PROG_TYPE_TRACING
BPF_MODIFY_RETURN 5.8 允许 fmod_ret 修改返回值
multi-fentry (单蹦床多程序) 5.10 一个蹦床执行多个 BPF 程序
BTF-based trampoline 5.5 替代 ftrace-based 实现
ARM64 trampoline 支持 5.11 ARM64 架构的蹦床实现

九、fentry/fexit 程序示例

9.1 最小 fentry 程序

/* SPDX-License-Identifier: GPL-2.0 */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

SEC("fentry/tcp_connect")
int BPF_PROG(tcp_connect_entry, struct sock *sk)
{
    /* 可以访问 sk 的所有字段 */
    bpf_printk("tcp_connect called\n");
    return 0;
}

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

9.2 fexit 捕获返回值

SEC("fexit/tcp_connect")
int BPF_PROG(tcp_connect_exit, struct sock *sk, int ret)
{
    /* ret 是 tcp_connect() 的返回值 */
    if (ret != 0)
        bpf_printk("tcp_connect failed: %d\n", ret);
    return 0;
}

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

9.3 加载并挂载

clang -O2 -target bpf -c fentry_example.bpf.c -o fentry_example.bpf.o
bpftool gen skeleton fentry_example.bpf.o > fentry_example.skel.h
# 在 C 程序中使用 skeleton 加载和挂载

十、总结

fentry/fexit 的蹦床机制是 eBPF 追踪基础设施的一次重要跃迁。它的核心思想——在目标函数的 nop sled 上直接放入 call 指令,绕过整个异常处理链路——将追踪的固定开销从 kprobe 的 ~100ns 量级降至 fentry 的 ~10ns 量级(引用 bpf_devel_QA.rst,非本站实测)。

理解蹦床的关键是三层抽象:

  1. bpf_trampoline 数据结构:管理蹦床的生命周期和程序挂载
  2. arch_prepare_bpf_trampoline():生成架构相关的汇编代码——保存/恢复寄存器、分配栈帧、调用 BPF 程序
  3. text_poke_bp():安全地将 nop 替换为 call,在多核系统上不中断执行

蹦床并非真正的零开销——在高频调用场景下,15ns 的累积仍然可观。但相对于 kprobe,它是一个数量级的改进,且避免了异常处理路径的不可预测性(中断禁用、IDT 缓存等)。

在 eBPF 系列中,本文与 第 16 篇(BPF 并发模型) 直接相关——蹦床的执行上下文决定了 BPF 程序在什么样的并发环境中运行。与 第 18 篇(sched_ext) 也有交集——sched_ext 通过 struct_ops 复用蹦床机制来将调度回调重定向到 BPF。

参考资料

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-06-12 · kernel / ebpf

【eBPF 内核实现深度拆解】从验证器到 JIT,从 BTF 到调度器

eBPF 内核虚拟机内部实现系统讲解:BPF 指令集与寄存器机器、验证器的抽象解释与状态裁剪、JIT 编译器后端、Map 各类型的并发与内存模型、helper 函数注册与类型检查、BTF 格式规范与 CO-RE 重定位引擎、libbpf 加载器工程、fentry/fexit 蹦床机制、sched_ext 调度器内核接口。面向想读懂 eBPF 内核源码、写生产级 BPF 程序的系统工程师。

2026-06-12 · kernel / ebpf

【eBPF 内核实现深度拆解】sched_ext 深度:用 BPF 写内核调度器

从 struct sched_ext_ops 的 10+ 回调语义出发,拆解 select_cpu/enqueue/dispatch/tick 等核心回调、scx_bpf_dispatch/scx_bpf_kick_cpu 等 kfunc 的内核实现、ext 调度类与 CFS/EEVDF 的共存策略(SCX_OPS_SWITCH_PARTIAL),以及 scx_layered 和 scx_rustland 的用户态调度器参考实现。


By .