eBPF 全景:原理、工具、应用场景与性能排查
eBPF 全景:原理、工具、应用场景与性能排查
eBPF 让用户把一小段程序交给 Linux 内核,内核先证明这段程序安全,再把它挂到某个事件上,事件发生时在内核态执行,结果通过 map 或缓冲区送回用户态。 全文分四部分:原理拆开这件事里的每个环节(指令集、验证器、JIT、程序类型与挂载、map、helper/kfunc、BTF/CO-RE); 工具讲 bpftrace、BCC、libbpf 的选型和用法;应用场景覆盖可观测性(内存泄漏、CPU、IO、用户态、网络)、网络数据面、安全、内核行为定制; 生产讲开销、限制、容器部署和与其他工具的对比。
适用范围:Linux x86-64,内核 5.x 起的特性为主线。特性标注了引入的主线内核版本,发行版可能回移植,以 bpftool feature probe 的结果为准。
用户态内存泄漏的完整排查流程见 memory-leak.md,本文只展开其中 eBPF 那一段。
实测说明:本文代码与命令均未实测,参数和格式以对应工具版本的 --help 为准。
网络章节中的负载均衡性能数字来自倪朋飞《eBPF 核心技术与实战》课程的 Docker 实验环境,只用于比较量级,不代表生产环境。
目录
| # | 部分 | 章节 | 主题 |
|---|---|---|---|
| 一 | 原理 | 定位:eBPF 解决什么问题 | 扩展内核的三种方式;发展历程 |
| 二 | 原理 | 整体架构与生命周期 | 五个组成模块;从源码到执行;内核对象与引用计数 |
| 三 | 原理 | 指令集与虚拟机 | 寄存器、指令格式、调用约定、与系统虚拟机的区别 |
| 四 | 原理 | 验证器 | 控制流检查、寄存器状态跟踪、上下文改写、特权分级 |
| 五 | 原理 | JIT 与执行环境 | JIT、执行上下文、递归保护、可睡眠程序 |
| 六 | 原理 | 程序类型与挂载机制 | 程序类型决定什么;各类钩子的实现原理;挂载接口演进 |
| 七 | 原理 | map:状态与通信 | 类型分组、并发语义、ring buffer、全局变量 |
| 八 | 原理 | helper、kfunc 与 BTF/CO-RE | 内核接口的稳定性分层;CO-RE 重定位过程 |
| 九 | 工具与开发 | 工具层次与选型 | bpftrace / BCC / libbpf / Go / Rust;bpftool |
| 十 | 工具与开发 | bpftrace 速成 | 语法、内置变量、聚合函数、常用一行脚本 |
| 十一 | 工具与开发 | libbpf + CO-RE:手写一个程序 | 内核态 C、skeleton、用户态加载器、常见报错 |
| 十二 | 应用场景 | 可观测性:诊断思路与案例 | 短时进程、丢包、偶发 RST、容器感知、云原生观测平台 |
| 十三 | 应用场景 | 内存泄漏排查 | memleak 原理与参数、bpftrace 自写版、内核泄漏 |
| 十四 | 应用场景 | CPU 性能分析 | on-CPU 火焰图、off-CPU、调度延迟、函数延迟、PMU |
| 十五 | 应用场景 | 内存子系统与 IO | 缺页、OOM、page cache、块设备延迟、文件系统慢请求 |
| 十六 | 应用场景 | 用户态程序跟踪 | uprobe C++ 函数、读参数、USDT、TLS 明文、解释型语言 |
| 十七 | 应用场景 | 网络:跟踪与数据面 | 连接/重传/丢包工具;收发包钩子、XDP、tc、socket 重定向、四层负载均衡、Cilium |
| 十八 | 应用场景 | 安全 | 分析、检测、阻断;LSM;TOCTOU;eBPF 自身的攻击面 |
| 十九 | 应用场景 | 内核行为定制 | struct_ops、TCP 拥塞控制、sched_ext、HID-BPF |
| 二十 | 生产与对比 | 生产环境:开销、限制与容器 | 各类探针开销、栈回溯、权限、容器部署、局限与应对 |
| 二十一 | 生产与对比 | 与 perf / ftrace / SystemTap 的比较 | |
| 二十二 | 生产与对比 | 面试速答 | |
| — | — | 附录:特性与内核版本、命令速查、参考资料 |
一、定位:eBPF 解决什么问题
扩展内核的三种方式
想让内核多做一件事(多统计一个指标、多一条转发规则、多一个安全检查),过去只有两条路;eBPF 是第三条。
| 修改内核源码并合入主线 | 内核模块 | eBPF | |
|---|---|---|---|
| 交付周期 | 以年计:提交、评审、发版、发行版跟进 | 天级,但要跟每个内核版本重编 | 分钟级:加载即生效 |
| 出错后果 | 整机 | 空指针即 panic | 验证器拒绝加载,或运行时 helper 返回错误 |
| 能力边界 | 任意 | 任意 | 只能在预留的钩子上、通过白名单接口做事 |
| 跨内核版本 | 不涉及 | 依赖内核头和导出符号,每版重编 | CO-RE:编译一次,按目标内核的 BTF 重定位 |
| 卸载 | 不能 | rmmod,引用不干净时卸不掉 |
关闭 fd / 删除 pin 文件,引用计数归零即释放 |
eBPF 的交换条件是:用能力上的限制换安全和可移植性。内核提供三样东西——钩子(在哪执行)、受限执行环境(验证器 + JIT)、状态存储(map);用户只提供逻辑。
发展历程
| 时间 | 事件 | 意义 |
|---|---|---|
| 1992 | McCanne 与 Jacobson 的 BSD Packet Filter 论文(1993 年 1 月 USENIX 冬季会议发表) | 在内核里跑用户给的过滤字节码,避免把每个包复制到用户态;tcpdump 的基础 |
| 1997 | Linux 2.1.75 引入 BPF(Linux Socket Filter) | |
| 2011 | Linux 3.0 为 BPF 加入 x86-64 JIT | 解释执行换成本机指令 |
| 2014 | Alexei Starovoitov 的 eBPF;3.18 加入 bpf() 系统调用和 map |
从"包过滤器"变成通用内核虚拟机 |
| 2015–2016 | 4.1 kprobe、tc;4.7 tracepoint;4.8 XDP;BCC 项目启动 | 进入跟踪和高性能网络 |
| 2017–2018 | 4.14 sockmap;4.18 BTF、AF_XDP | Cilium、Katran 上生产 |
| 2019–2020 | 5.2 指令上限 100 万;5.3 有界循环;5.5 fentry;5.7 LSM;5.8 ring buffer、CAP_BPF |
可写复杂程序;进入安全策略执行 |
| 2021 | eBPF 基金会成立;微软 eBPF for Windows | 跨操作系统 |
| 2024 | 6.12 合入 sched_ext | 用 eBPF 写 CPU 调度器 |
命名:现在内核社区说 BPF 就指扩展版;老的只有两个 32 位寄存器的版本称 cBPF(classic BPF)。seccomp 和 tcpdump -d 输出的仍是 cBPF,内核把 cBPF 在加载时转译成 eBPF 执行。
二、整体架构与生命周期
五个组成模块
| 模块 | 作用 |
|---|---|
| 验证器(verifier) | 加载时对字节码做静态分析,证明不会崩溃、不会越界、一定会结束 |
| JIT 编译器 | 把字节码翻译成本机指令 |
| 寄存器与栈 | 11 个 64 位寄存器 + 512 字节栈,是程序唯一的"私有内存" |
| helper / kfunc | 程序与内核其他子系统交互的唯一接口,可用集合由程序类型决定 |
| map | 键值存储,跨程序调用保存状态,也是内核态与用户态交换数据的通道 |
从源码到执行
flowchart LR
A["prog.bpf.c"] -->|"clang -target bpf -g -O2"| B["prog.bpf.o<br/>ELF: 字节码 + map 定义 + BTF"]
B --> C["加载器<br/>libbpf / BCC / bpftrace"]
C -->|"BPF_MAP_CREATE"| M[("map")]
C -->|"CO-RE 重定位后<br/>BPF_PROG_LOAD"| V["验证器"]
V -->|"拒绝:返回日志"| C
V -->|"通过"| J["JIT"]
J --> P["程序对象 prog fd"]
C -->|"挂载:perf_event / netlink /<br/>BPF_PROG_ATTACH / BPF_LINK_CREATE"| H["钩子"]
P -.-> H
H -->|"事件发生"| R["执行本机代码"]
R <-->|"helper"| M
M <-->|"bpf() 系统调用 / mmap"| U["用户态程序"]
关键点:
- 加载和挂载是两步。
BPF_PROG_LOAD只是让内核认可并编译这段代码,得到一个 prog fd;把它挂到钩子上才会被执行。 - 程序是事件驱动的。它不是一个常驻线程,没有自己的调度上下文,事件没发生就不占 CPU;一次执行通常几十到几百纳秒。
- map 只能由用户态通过系统调用创建。程序里引用 map 时,字节码中是一条特殊的 64 位立即数加载指令,携带 map fd;验证器把它替换成内核中 map 对象的地址。
用 strace 看一次加载
以 BCC 加载一个挂在 do_sys_openat2 上的 kprobe 程序为例,系统调用序列是:
sequenceDiagram
participant U as 用户态(BCC)
participant K as 内核
U->>K: bpf(BPF_PROG_LOAD, {prog_type=KPROBE, insns=[...], license="GPL"})
K-->>U: prog fd = 4(已通过验证并 JIT)
U->>K: 读 /sys/bus/event_source/devices/kprobe/type
K-->>U: "6"(kprobe 这个 PMU 的类型号)
U->>K: perf_event_open({type=6, config1=&"do_sys_openat2"})
K-->>U: perf fd = 5(内核在 do_sys_openat2 上注册 kprobe)
U->>K: ioctl(5, PERF_EVENT_IOC_SET_BPF, 4)
Note over K: 之后每次调用 do_sys_openat2,<br/>kprobe 处理函数执行 prog 4
复现命令:sudo strace -f -e trace=bpf,perf_event_open,ioctl,openat python3 hello.py。
内核对象与引用计数
prog、map、link、BTF 都是内核对象,用户态持有的是 fd。对象在引用计数归零时释放,引用来源有三种:
| 引用来源 | 说明 | 进程退出后 |
|---|---|---|
| 进程持有的 fd | 加载时返回 | fd 关闭,引用减一 |
| bpffs 中的 pin 文件 | BPF_OBJ_PIN 到 /sys/fs/bpf/xxx |
保留;rm 该文件才减一 |
| 挂载关系 | 钩子持有程序 | 取决于挂载方式,见下 |
挂载关系是否持有引用决定了"用户进程退出,程序还在不在":
| 挂载方式 | 进程退出后程序 |
|---|---|
| kprobe/uprobe/tracepoint(perf_event fd) | 随 perf fd 关闭自动卸载 |
| XDP / tc(netlink 方式) | 留在网卡上,需 ip link set dev X xdp off 或 tc filter del |
cgroup(BPF_PROG_ATTACH) |
留在 cgroup 上,需 detach |
bpf_link(5.7+,BPF_LINK_CREATE) |
link fd 关闭即卸载;pin 住 link 可以常驻 |
bpf_link 统一了语义:"谁持有 link 谁拥有这次挂载"。它还防止别的进程误替换同一钩子上的程序,这对 Cilium 这类常驻 agent 升级时的平滑替换很重要(BPF_LINK_UPDATE 原子替换程序)。
三、指令集与虚拟机
和系统虚拟机不是一回事
| KVM 等系统虚拟机 | eBPF 虚拟机 | |
|---|---|---|
| 指令集 | x86/ARM 完整指令集 | 约百条精简指令,无浮点,无特权指令 |
| 目的 | 模拟一台完整计算机 | 在内核里安全执行一段受限逻辑 |
| 执行 | 硬件虚拟化 | JIT 成宿主机指令直接跑在内核上下文里 |
| 内存 | 独立的客户机物理内存 | 只有寄存器、512 字节栈、map,访问其他内存必须经 helper |
| 运行时长 | 长期运行 | 每次触发执行一遍,必须在有限步内结束 |
"虚拟机"在这里的准确含义是:一套可以被静态验证、再被 JIT 的中间指令集。
寄存器
| 寄存器 | 用途 | x86-64 JIT 映射 |
|---|---|---|
r0 |
helper 返回值、程序返回值 | rax |
r1–r5 |
helper 调用参数;程序入口时 r1 = 上下文指针 |
rdi rsi rdx rcx r8 |
r6–r9 |
被调用者保存 | rbx r13 r14 r15 |
r10 |
只读栈帧指针 | rbp |
映射取自 arch/x86/net/bpf_jit_comp.c 的 reg2hex,实现相关,可能随版本变化。
设计意图:r1–r5 与 x86-64 System V ABI 的前 5 个参数寄存器一一对应,r0 对应返回值寄存器,所以调用 helper 在 JIT 后就是一条原生 call,没有参数搬运。代价是 helper 最多 5 个参数、只有一个返回值。
指令格式
每条指令定长 8 字节(include/uapi/linux/bpf.h 的 struct bpf_insn):
bit 0 8 12 16 32 64
┌────────┬───────┬───────┬───────────────┬───────────────────────────────┐
│ opcode │ dst │ src │ off (s16) │ imm (s32) │
│ 8 bit │ 4 bit │ 4 bit │ 跳转偏移/内存偏移 │ 立即数 │
└────────┴───────┴───────┴───────────────┴───────────────────────────────┘
- 格式接近 x86-64 的寄存器-寄存器/寄存器-内存形式,JIT 基本是一条对一条(或一条对几条)翻译
- 程序大小上限:5.2 之前 4096 条,5.2 起特权程序 100 万条
- opcode 低 3 位是指令类:
LD、LDX、ST、STX、ALU、JMP、JMP32、ALU64 - 唯一的 16 字节指令是
ld_imm64(加载 64 位立即数),占两个槽位。所以bpftool prog dump xlated输出的指令编号会跳一格;map 引用也用这条指令表达 - 例:
(b7) r1 = 33中0xb7=ALU64 | MOV | K(把立即数写入 64 位寄存器);(85) call bpf_trace_printk中0x85=JMP | CALL
查看一个已加载程序的字节码和 JIT 结果:
函数调用的三种形态
| 形态 | 引入 | 语义 | 限制 |
|---|---|---|---|
| helper 调用 | 3.18 | 调内核提供的固定函数 | 可用集合由程序类型决定 |
| BPF-to-BPF 调用 | 4.16 | 程序内部的子函数调用,会返回 | 调用深度 8 层;所有帧的栈合计 512 字节 |
| tail call | 4.2 | bpf_tail_call(ctx, &prog_array, idx) 跳到另一个程序,不返回,复用当前栈 |
最多 33 次(5.17 前为 32) |
tail call 的用途:把大程序拆成流水线(Cilium 按协议分派到不同的处理程序),以及不停机替换其中一段(更新 PROG_ARRAY 中的一项即可)。
四、验证器
验证器(kernel/bpf/verifier.c)是 eBPF 能进内核的前提。它要在加载时、不运行程序的前提下,证明程序对所有输入都满足:
- 一定结束(没有无界循环)
- 不读未初始化的寄存器和栈
- 每次内存访问都在合法对象的边界内
- 只调用该程序类型允许的 helper,参数类型匹配
- 不向用户态泄露内核指针(非特权模式)
第一遍:控制流检查
对指令做深度优先遍历,构建控制流图:
- 存在从未到达的指令 → 拒绝(
unreachable insn) - 跳转目标越界 → 拒绝
- 5.3 之前:存在回边(循环)→ 拒绝,循环只能靠编译器
#pragma unroll展开 - 5.3 起:允许回边,交给第二遍证明循环有界
第二遍:沿所有路径模拟执行
验证器为每个寄存器和每个栈槽维护一个抽象状态,逐条指令推演,遇到分支两边都走:
| 状态字段 | 含义 |
|---|---|
| 类型 | NOT_INIT、SCALAR_VALUE、PTR_TO_CTX、PTR_TO_STACK、PTR_TO_MAP_VALUE、PTR_TO_MAP_VALUE_OR_NULL、PTR_TO_PACKET、PTR_TO_PACKET_END、PTR_TO_BTF_ID 等 |
| 取值范围 | 有符号/无符号的最小最大值(smin/smax/umin/umax) |
| 已知位 | tnum:哪些位已知为 0/1、哪些未知,用于跟踪 &、>> 之后的精确范围 |
| 指针偏移 | 指针相对对象起点的偏移(常量部分 + 变量范围) |
两个典型例子。
例 1:map 查找必须判空。
struct val* v = ; // r0 的类型: PTR_TO_MAP_VALUE_OR_NULL
v->cnt++; // 拒绝: invalid mem access 'map_value_or_null'
struct val* v = ;
if return 0; // 在"非空"分支上,r0 被改标为 PTR_TO_MAP_VALUE
v->cnt++; // 通过,且偏移 < value_size 已被证明
例 2:报文访问必须先比较 data_end。
void* data = ctx->data; // PTR_TO_PACKET, off=0, range=0
void* data_end = ctx->data_end; // PTR_TO_PACKET_END
struct ethhdr* eth = data;
if return XDP_DROP;
// 在不越界的分支上,验证器把 data 的可访问范围记为 14 字节
// 之后读 eth->h_proto(偏移 12,宽 2)才能通过
验证器不知道包有多长,它只认"你已经和 data_end 比过"。这就是所有 XDP/tc 程序里到处是边界检查的原因。
验证器拒绝时返回的是一段模拟执行日志(libbpf 会打印出来),最后几行是原因,例如 invalid mem access 'map_value_or_null'(忘了判空)、back-edge from insn X to Y(5.3 前的循环)、R1 offset is outside of the packet(报文越界)。常见报错对照见第十一章。
复杂度控制
- 路径数随分支指数增长。验证器在跳转目标处保存已验证过的状态,新到达的状态如果被某个旧状态"包含"(范围更窄、类型相同),就剪掉这条路径(state pruning)
- 上限:已处理指令数 100 万条(5.2 起,
BPF_COMPLEXITY_LIMIT_INSNS);超过报BPF program is too large - 循环写法演进:
#pragma unroll(任何版本)→ 有界循环(5.3)→bpf_loop()helper(5.17,回调形式,循环体只验证一次)→ open-coded 迭代器bpf_for(6.4)
上下文访问改写
每种程序类型给程序看的上下文结构(如 struct __sk_buff、struct xdp_md、struct bpf_sock_ops)是 UAPI 里的"影子结构",字段固定;内核里真实的结构(struct sk_buff、struct xdp_buff)字段和布局随版本变化。
验证器在通过后执行 convert_ctx_accesses:把"读 __sk_buff 偏移 X"改写成"读 sk_buff 真实偏移 Y"。所以:
- 网络程序访问上下文字段不需要 CO-RE,天然跨版本
bpftool prog dump xlated看到的是改写后的指令,和 clang 输出的不一样
特权分级
| 能力 | 需要的权限(5.8 起) |
|---|---|
| 加载任意类型程序 | CAP_BPF |
| 跟踪类程序、读内核内存 | CAP_BPF + CAP_PERFMON |
| 网络类程序(XDP、tc 等) | CAP_BPF + CAP_NET_ADMIN |
| 非特权加载(socket filter 等少数类型) | 受 kernel.unprivileged_bpf_disabled 控制,主流发行版默认禁止 |
5.8 之前统一要求 CAP_SYS_ADMIN。非特权模式下验证器额外做:禁止指针与标量互转、禁止把指针写进 map、对指针运算插入 Spectre 防护掩码。
验证器证明不了什么
- 逻辑正确性:它只证明安全,不证明程序做了对的事。XDP 程序改错了 MAC 地址照样通过
- 可证明但被拒绝:clang 优化可能把边界检查和访问拆到验证器跟丢的形式(例如先把偏移存到另一个寄存器再比较)。常见应对是
__always_inline、把长度与常量&一下收窄范围、调整代码顺序 - 验证器自身的漏洞:历史上多个本地提权 CVE 出自验证器的范围推演错误(如 CVE-2021-3490、CVE-2021-31440)。这是发行版默认关闭非特权 eBPF 的主要原因
五、JIT 与执行环境
JIT
| 配置 | 作用 |
|---|---|
net.core.bpf_jit_enable=1 |
开启 JIT(主流发行版默认) |
CONFIG_BPF_JIT_ALWAYS_ON=y |
编译时去掉解释器,防止 Spectre v2 利用解释器做 gadget |
net.core.bpf_jit_harden=1/2 |
常量盲化:把立即数拆成两个数异或,防 JIT spray(1 只对非特权,2 对所有) |
net.core.bpf_jit_kallsyms=1 |
JIT 后的程序出现在 /proc/kallsyms,perf 能解析到 bpf_prog_<tag>_<name> |
主流 64 位架构(x86-64、arm64、riscv64、s390x、ppc64、loongarch 等)都有 JIT。
执行上下文
程序在触发它的那个内核执行流里同步运行,没有自己的线程:
| 钩子 | 运行在 |
|---|---|
| syscall tracepoint、大部分 kprobe | 触发系统调用的进程上下文 |
| XDP、tc ingress | 软中断(NAPI poll) |
| perf_event 采样 | 硬件 PMU 溢出的 NMI 上下文 |
| uprobe | 被跟踪进程陷入内核后的上下文 |
共同约束:
- 执行期间持有 RCU 读锁、禁止迁移 CPU。per-CPU map 正是依赖"一次执行不会换 CPU"
- 不能睡眠:不能缺页、不能拿可睡眠的锁。
bpf_probe_read_user遇到缺页不会触发换入,而是直接返回错误并把目标缓冲区清零——这就是为什么读用户态字符串偶尔拿到空值 - 5.10 起的可睡眠程序(
SEC("lsm.s/...")、SEC("fentry.s/...")、SEC("uprobe.s/..."))可以调用bpf_copy_from_user这类会缺页的 helper,只允许挂在本身可睡眠的钩子上
递归保护
一个 kprobe 程序里调用的 helper 如果本身又触发了 kprobe,会无限递归。内核用 per-CPU 计数 bpf_prog_active(及每个程序的 active 计数)防止同一 CPU 上重入:重入时直接跳过执行,计入 bpftool prog show 输出的 recursion_misses。结论:高频钩子上的事件可能被静默丢掉,统计类工具要看这个计数。
六、程序类型与挂载机制
程序类型决定四件事
bpf_prog_type(5.13 有 30 种,之后持续增加)决定:
| 决定项 | 例子 |
|---|---|
上下文 r1 指向什么 |
kprobe → struct pt_regs;XDP → struct xdp_md;tc → struct __sk_buff |
| 能调用哪些 helper | 只有 kprobe 类能调 bpf_override_return;只有网络类能调 bpf_redirect |
| 能挂到哪里 | XDP 只能挂网卡;cgroup_skb 只能挂 cgroup |
| 返回值的含义 | XDP:动作码;tc:TC_ACT_*;cgroup:1 放行 / 0 拒绝;kprobe:被忽略 |
libbpf 用 ELF 段名(SEC("..."))推断程序类型和挂载点,所以段名不是随意取的。
三大类
跟踪类:从内核和应用中提取状态,不改变系统行为。
| 类型 | 段名示例 | 上下文 | 挂载点 |
|---|---|---|---|
KPROBE |
kprobe/tcp_v4_connect、uprobe//usr/bin/bash:readline、usdt/... |
pt_regs |
任意内核/用户函数 |
TRACEPOINT |
tracepoint/syscalls/sys_enter_execve |
tracepoint 格式化后的结构 | 静态跟踪点 |
RAW_TRACEPOINT |
raw_tp/sched_switch |
bpf_raw_tracepoint_args(u64 args[]) |
静态跟踪点,不格式化 |
TRACING |
fentry/、fexit/、tp_btf/、fmod_ret/、iter/ |
BTF 类型化的原始参数 | 内核函数入口/出口、跟踪点、迭代器 |
PERF_EVENT |
perf_event |
bpf_perf_event_data |
定时采样、PMU 计数器 |
网络类:处理和控制数据包。
| 类型 | 段名示例 | 上下文 | 挂载点 |
|---|---|---|---|
XDP |
xdp |
xdp_md |
网卡驱动收包路径 |
SCHED_CLS |
tc、tcx/ingress |
__sk_buff |
tc 入向/出向 |
SOCKET_FILTER |
socket |
__sk_buff |
单个 socket(SO_ATTACH_BPF) |
SOCK_OPS |
sockops |
bpf_sock_ops |
cgroup:TCP 连接建立、RTO、状态变化等事件 |
SK_SKB / SK_MSG |
sk_skb/stream_verdict、sk_msg |
__sk_buff / sk_msg_md |
sockmap:socket 间重定向 |
CGROUP_SKB |
cgroup_skb/ingress |
__sk_buff |
cgroup 内进程收发的包 |
CGROUP_SOCK_ADDR |
cgroup/connect4、cgroup/sendmsg4 |
bpf_sock_addr |
cgroup 内进程的 connect/bind/sendmsg |
SK_REUSEPORT |
sk_reuseport |
sk_reuseport_md |
在 SO_REUSEPORT 组里选 socket |
FLOW_DISSECTOR |
flow_dissector |
__sk_buff |
替换内核的包头解析 |
其他类:安全控制与内核扩展。
| 类型 | 用途 |
|---|---|
LSM |
挂到 LSM 钩子,返回负错误码即拒绝操作(5.7) |
CGROUP_DEVICE |
控制 cgroup 内进程能访问哪些设备(cgroup v2 的设备控制就是它) |
CGROUP_SYSCTL / CGROUP_SOCKOPT |
控制 sysctl 读写、setsockopt |
STRUCT_OPS |
用 BPF 实现内核的一组函数指针(TCP 拥塞控制、sched_ext) |
EXT |
替换另一个 BPF 程序中的某个函数(freplace),用于程序热替换 |
SYSCALL |
在内核里执行一系列 bpf() 操作(轻量加载器) |
同一个事件,为什么上下文各不相同
以内核函数 __set_task_comm()(进程改名)为例,它内部调用了 tracepoint trace_task_rename()。可以用三种方式跟踪,拿到的参数完全不同:
| 方式 | 上下文 | 原因 |
|---|---|---|
kprobe/__set_task_comm |
struct pt_regs* |
kprobe 通过断点(或跳转)进入内核的异常/ftrace 路径,内核把当时的寄存器保存成 pt_regs。按 x86-64 调用约定,第一个参数在 regs->di,要自己解释 |
tracepoint/task/task_rename |
按 /sys/kernel/tracing/events/task/task_rename/format 布局的结构 |
tracepoint 由 TRACE_EVENT 宏定义,调用时先把参数按 TP_STRUCT__entry 复制成一条记录,程序拿到的就是这条记录(pid、oldcomm、newcomm…) |
raw_tp/task_rename |
bpf_raw_tracepoint_args,即 u64 args[] |
跳过格式化,直接给 TP_PROTO 里的原始参数(task_struct*、const char*),开销更低,能拿到整个 task_struct |
tp_btf/task_rename / fentry/__set_task_comm |
带 BTF 类型的原始参数 | 验证器知道参数类型,程序可以直接解引用 task->pid,无需 bpf_probe_read_kernel |
写 eBPF 程序的难点不在语法,而在知道内核里有哪些钩子、每个钩子能拿到什么。
跟踪钩子的实现原理与开销
| 钩子 | 实现 | 未启用时的开销 | 触发一次的开销(量级) |
|---|---|---|---|
| tracepoint | 编译进内核的调用点,由 static key 控制:未启用时是一条 nop,启用时改写成跳转 |
一条 nop | 低 |
| kprobe | 在目标指令处写入 int3;可优化时替换为 jmp 到跳板(optprobe);在函数入口且内核启用 ftrace 时走 ftrace 路径 |
0 | 百纳秒级 |
| fentry/fexit(5.5) | 函数入口有编译器插入的 5 字节 nop(-mfentry),改写成 call 到 BPF trampoline,trampoline 直接调用 BPF 程序 |
一条 nop | 低于 kprobe(无异常处理、无 pt_regs 保存) |
| uprobe | 在用户进程代码页写入 int3(写时复制出该进程私有的页),命中后陷入内核执行程序,再在一个"越界执行槽"(XOL)里单步执行被替换的原指令 |
0 | 微秒级(两次内核态/用户态切换) |
| USDT | 应用在源码里用 DTRACE_PROBE 等宏埋点,编译成一条 nop,位置和参数描述记在 ELF 的 .note.stapsdt 段;启用时按 uprobe 方式改成 int3 |
一条 nop | 同 uprobe |
| perf_event | PMU 计数溢出触发 NMI,或定时器触发 | 0 | 与采样频率成正比 |
由此得到的选型规则:
- 有 tracepoint 用 tracepoint(稳定 ABI);没有再用 kprobe;新内核用 fentry 代替 kprobe
- kprobe 挂不到被内联的函数;编译器生成的
.isra.0、.constprop.0等后缀函数要用带后缀的名字挂 - uprobe 不要挂在
malloc、锁这类每秒百万次的函数上,开销会叠加到被跟踪进程的每一次调用上 - 用户态优先 USDT,其次 uprobe
各类探针开销的具体量级和估算方法见第二十章。
查可用的钩子
|
|
|
挂载接口的演进
| 接口 | 适用 | 说明 |
|---|---|---|
perf_event_open + ioctl(PERF_EVENT_IOC_SET_BPF) |
kprobe、uprobe、tracepoint、perf_event | 最早的方式,见第二章的时序图 |
BPF_RAW_TRACEPOINT_OPEN |
raw_tp、fentry/fexit、LSM | 返回一个 fd,关闭即卸载 |
netlink(ip link / tc) |
XDP、tc | 挂在网络设备上,不随进程退出 |
BPF_PROG_ATTACH |
cgroup 类、sockmap 类、flow dissector | 挂在 cgroup / map / netns 上 |
BPF_LINK_CREATE(5.7 起逐步覆盖各类型) |
几乎所有类型 | 统一为 link 对象;kprobe_multi(5.18)一次挂上千个函数;tcx(6.6)替代 tc 的 netlink 挂载 |
七、map:状态与通信
eBPF 程序每次执行都从空的寄存器和栈开始,跨调用的一切状态都在 map 里。map 同时是用户态与程序、程序与程序之间的共享内存。
类型分组
| 用途 | 类型 | 要点 |
|---|---|---|
| 通用键值 | HASH、LRU_HASH、ARRAY |
ARRAY 预分配、按下标访问、不能删除元素;LRU_HASH 满了淘汰最久未用的,适合连接表 |
| 无竞争计数 | PERCPU_HASH、PERCPU_ARRAY |
每个 CPU 一份值,写不需要原子操作;用户态读到的是 N 份,自己求和。也常用作超过 512 字节的"临时堆" |
| 事件输出 | PERF_EVENT_ARRAY、RINGBUF(5.8) |
向用户态推送逐条事件 |
| 调用栈 | STACK_TRACE |
bpf_get_stackid() 存栈并返回 id,相同栈去重 |
| 程序组合 | PROG_ARRAY、ARRAY_OF_MAPS、HASH_OF_MAPS |
tail call 跳转表;map 嵌套(按租户/按 CPU 换内层 map) |
| 网络转发 | DEVMAP、CPUMAP、XSKMAP |
XDP_REDIRECT 的目标:另一个网卡 / 另一个 CPU / AF_XDP socket |
| socket | SOCKMAP、SOCKHASH |
存 socket 引用,用于 sk_msg/sk_skb 重定向 |
| 路由匹配 | LPM_TRIE |
最长前缀匹配,CIDR 规则 |
| cgroup 判断 | CGROUP_ARRAY |
配合 bpf_current_task_under_cgroup() 判断进程是否属于某个 cgroup |
| 对象附属存储 | TASK_STORAGE、SK_STORAGE、INODE_STORAGE、CGRP_STORAGE |
把数据挂在内核对象上,对象销毁时自动回收,无需自己按 pid 做 key 再清理 |
| 共享内存 | ARENA(6.9) |
程序与用户态共享的一块稀疏内存,程序里可以用普通指针构建数据结构 |
| 队列 | QUEUE、STACK、BLOOM_FILTER |
并发语义
bpf_map_lookup_elem返回的是指向 map 内部值的指针,不是拷贝。多个 CPU 同时拿到同一个指针再v->cnt++,就是普通的数据竞争,会丢计数- 三种解法:用
PERCPU_*类型(首选);用__sync_fetch_and_add(&v->cnt, 1)(编译成 BPF 原子指令);值里嵌struct bpf_spin_lock配合bpf_spin_lock()/bpf_spin_unlock()(锁内不能调用其他 helper) bpf_map_update_elem对哈希表是整元素替换(RCU),读者要么看到旧值要么看到新值- 哈希表默认预分配全部元素(
max_entries个),因为程序可能运行在 NMI 等不能分配内存的上下文里;BPF_F_NO_PREALLOC关闭预分配,省内存但不能用于 NMI 上下文的程序
perf buffer 与 ring buffer
PERF_EVENT_ARRAY(perf buffer) |
RINGBUF(5.8) |
|
|---|---|---|
| 结构 | 每个 CPU 一个环形缓冲区 | 所有 CPU 共享一个 |
| 顺序 | 跨 CPU 无序,用户态要按时间戳重排 | 按提交顺序 |
| 内存 | 按最坏情况给每个 CPU 分配 | 共享一份,利用率高 |
| 写法 | 先在栈上构造事件再 bpf_perf_event_output 拷贝 |
bpf_ringbuf_reserve 拿到缓冲区指针直接写,再 submit,少一次拷贝 |
| 满了 | 丢事件,用户态收到 lost 计数 | reserve 返回 NULL,程序自己决定 |
新程序优先用 ring buffer。两者都会丢事件,统计类需求应先在内核里聚合进 map,只把结果交给用户态(例如直方图只传桶计数)。
全局变量
5.2 起,C 代码里的全局变量被 clang 放进 .bss、.data、.rodata 段,libbpf 把每个段创建成一个单元素 ARRAY map:
- 用户态通过 skeleton 直接读写
skel->bss->counter(底层是 mmap) .rodata在加载前由用户态填好,加载时冻结。验证器把它当常量,不可达的分支会被当作死代码剪掉。常用来做"加载时配置":const volatile pid_t target_pid = 0;用户态设置后,if (target_pid && pid != target_pid)在未设置时整段消失
八、helper、kfunc 与 BTF/CO-RE
内核接口的稳定性分层
| 接口 | 稳定性 | 说明 |
|---|---|---|
| helper | 稳定 UAPI,一旦加入不删除不改签名 | man bpf-helpers;数量二百多个;按程序类型开放 |
| kfunc(5.13) | 不保证稳定,可能随版本改变或删除 | 内核函数用 BTF 标注后允许 BPF 直接调用。新功能基本都以 kfunc 形式加入,避免 helper 表无限膨胀 |
| tracepoint 格式 | 事实上稳定 | 内核尽量不改已有 tracepoint 的字段 |
| kprobe 目标函数、内核结构体布局 | 不稳定 | 靠 CO-RE 适配 |
常用 helper:
| 类别 | helper |
|---|---|
| map | bpf_map_lookup_elem、bpf_map_update_elem、bpf_map_delete_elem |
| 读内存 | bpf_probe_read_kernel、bpf_probe_read_user、bpf_probe_read_*_str |
| 当前任务 | bpf_get_current_pid_tgid、bpf_get_current_uid_gid、bpf_get_current_comm、bpf_get_current_task_btf、bpf_get_current_cgroup_id |
| 时间 | bpf_ktime_get_ns、bpf_ktime_get_boot_ns |
| 栈 | bpf_get_stackid、bpf_get_stack |
| 输出 | bpf_ringbuf_output/reserve/submit、bpf_perf_event_output、bpf_printk(写 /sys/kernel/tracing/trace_pipe,仅调试) |
| 网络 | bpf_redirect、bpf_redirect_map、bpf_skb_store_bytes、bpf_l3_csum_replace、bpf_fib_lookup、bpf_sk_lookup_tcp |
| 干预 | bpf_send_signal(5.3)、bpf_override_return(4.16,需 CONFIG_BPF_KPROBE_OVERRIDE,只对标注了 ALLOW_ERROR_INJECTION 的函数有效)、bpf_probe_write_user |
bpf_probe_read_* 存在的原因:程序里的指针(比如从 pt_regs 里取出来的 task_struct*)对验证器来说只是一个整数,它无法证明这个地址可读。这组 helper 在内部做异常保护的拷贝,读失败返回错误而不是崩溃。
BTF:内核自带的类型信息
BTF(BPF Type Format)是精简版的 DWARF。内核编译时开启 CONFIG_DEBUG_INFO_BTF=y(5.2 起支持;Ubuntu 20.10+、RHEL 8.2+ 等默认开启),所有内核类型会嵌入 vmlinux;5.4 起通过 /sys/kernel/btf/vmlinux 暴露给用户态。
BTF 的用途:
| 用途 | 说明 |
|---|---|
vmlinux.h |
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h,一个头文件替代所有内核头 |
| CO-RE 重定位 | 见下 |
| 类型化指针 | fentry / tp_btf / LSM 程序的参数是 PTR_TO_BTF_ID,验证器知道结构布局,允许直接解引用 |
| kfunc | 靠 BTF 描述函数签名 |
| 可读的 map | bpftool map dump 按结构体字段打印,而不是十六进制 |
没有 BTF 时(BCC 的做法):目标机器上必须装 linux-headers-$(uname -r) 和 LLVM,每次运行现场编译。问题是生产机常不允许装头文件、启动慢(秒级、占几百 MB 内存)、头文件和运行内核不一致时读错字段。
CO-RE:编译一次,到处运行
问题:程序要读 task->real_parent->tgid,而 tgid 在 task_struct 里的偏移在不同内核版本、不同编译配置下都不同。
pid_t ppid = ;
flowchart TD
A["编译期:clang 遇到 BPF_CORE_READ<br/>(__builtin_preserve_access_index)"] --> B["生成重定位记录<br/>类型 task_struct,访问路径 real_parent→tgid<br/>写入 .BTF.ext 段"]
B --> C["加载期:libbpf 读目标机 /sys/kernel/btf/vmlinux"]
C --> D["按结构名 + 字段名匹配<br/>算出目标内核上的真实偏移"]
D --> E["改写字节码里的偏移立即数"]
E --> F["BPF_PROG_LOAD"]
结果是一个几十 KB 的静态二进制,目标机不需要内核头文件和编译器。
CO-RE 还能处理:
- 字段在某些版本不存在:
bpf_core_field_exists(task->some_field),加载时变成常量 0/1,另一个分支被剪掉 - 字段改名:定义
struct task_struct___old { ... }(三下划线后缀会被忽略),分别尝试新旧两种布局 - 枚举值、类型大小变化:
bpf_core_enum_value、bpf_core_type_size
老内核没有内置 BTF:可以用 BTFHub 提供的按发行版内核预生成的 BTF 文件,加载时通过 libbpf 的 btf_custom_path 指定。
九、工具层次与选型
上手快 ◀───────────────────────────────────────────────▶ 控制强
bpftrace BCC 工具集 BCC Python API libbpf + CO-RE
一行脚本 现成的 100+ 命令 Python + 内嵌 C 纯 C,静态二进制
临时排查 日常性能分析 定制工具 产品级、长期运行
需要 LLVM 需要内核头 + LLVM 同左 只需 clang 编译一次
| 方式 | 内核态 | 用户态 | 编译时机 | 目标机依赖 | 适合 |
|---|---|---|---|---|---|
| bpftrace | DSL | 内置 | 运行时 | bpftrace(内含 LLVM) | 一行脚本排查、验证想法 |
| BCC | C(内嵌字符串) | Python / Lua / C++ | 运行时 | LLVM + 内核头 | 现成工具(execsnoop、biolatency…)、快速原型 |
| libbpf + CO-RE | C | C(skeleton) | 开发时一次 | 内核有 BTF | 产品级、常驻 agent、静态二进制分发 |
| cilium/ebpf | C | Go(纯 Go 加载器) | 开发时 | 同上 | Go 写的云原生组件(Cilium、Tetragon 等) |
| libbpf-rs | C | Rust | 开发时 | 同上 | Rust 用户态 |
| Aya | Rust | Rust | 开发时 | 同上 | 两端都用 Rust,不依赖 libbpf |
内核 samples/bpf、tools/testing/selftests/bpf |
C | C | — | — | 查某个特性的最小可用例子 |
BCC、bpftrace 的运行时编译要在目标机上带 LLVM(几十到上百 MB),启动要几秒;libbpf 程序开发时编译好,启动是毫秒级。
Ubuntu 上的包名:bpftrace、bpfcc-tools(命令带 -bpfcc 后缀,如 memleak-bpfcc)、libbpf-dev、linux-tools-$(uname -r)(含 bpftool)。
工作流:先用 bpftrace 或 BCC 验证"这个钩子能拿到我要的数据",再用 libbpf 写正式版本分发(第十一章)。
编译器:LLVM 3.7 起支持 BPF 后端,是事实标准;GCC 10 起也有 BPF 后端,大型项目使用较少。
bpftool:查看与管理
|
十、bpftrace 速成
程序结构
probe[,probe...] /filter/ { action }
# 三个基本元素:事件源、过滤条件、动作
探针写法
| 写法 | 含义 |
|---|---|
kprobe:tcp_sendmsg / kretprobe:tcp_sendmsg |
内核函数入口/返回 |
kfunc:tcp_sendmsg / kretfunc:... |
fentry/fexit,参数可以按名字取(args->sk) |
tracepoint:syscalls:sys_enter_read |
静态 tracepoint,参数 args->fd |
uprobe:/bin/bash:readline / uretprobe:... |
用户态函数;库可以写 uprobe:libc:malloc |
uprobe:/path/bin:0x1234 |
按偏移 |
usdt:/path/bin:provider:probe |
USDT |
profile:hz:99 |
每 CPU 每秒 99 次定时采样 |
interval:s:5 |
单 CPU 每 5 秒一次,用于周期性打印 |
software:page-faults:1 / hardware:cache-misses:1000000 |
perf 软件/硬件事件,数字是采样周期 |
BEGIN / END |
脚本开始/结束 |
watchpoint:0xaddr:8:w |
内存写监视点 |
iter:task |
遍历内核对象 |
通配符:kprobe:tcp_*、uprobe:libc:*alloc*。
内置变量
| 变量 | 含义 |
|---|---|
pid / tid |
进程 id(tgid)/ 线程 id |
comm |
进程名(16 字节) |
uid / gid / cpu / nsecs / elapsed |
用户、CPU 号、纳秒时间戳、脚本启动以来的纳秒 |
arg0..argN |
kprobe/uprobe 的参数(按寄存器) |
args |
tracepoint / kfunc / USDT 的结构化参数(args->filename) |
retval |
kretprobe/uretprobe 返回值 |
kstack / ustack |
内核/用户调用栈;ustack(6) 限制深度;ustack(perf) 换格式 |
func / probe |
当前函数名 / 探针名 |
curtask |
当前 task_struct*,可以 curtask->mm->... |
$1, $2 / str($1) |
命令行位置参数 |
cgroup |
当前 cgroup id |
聚合与函数
@name[key] = 聚合函数(值),脚本结束自动打印所有 map:
| 函数 | 作用 |
|---|---|
count() |
计数 |
sum(x) / avg(x) / min(x) / max(x) |
求和/平均/最小/最大 |
hist(x) |
2 的幂直方图 |
lhist(x, min, max, step) |
线性直方图 |
stats(x) |
count + avg + total |
delete(@m[k]) / clear(@m) / zero(@m) |
删除项 / 清空 / 归零 |
print(@m) / print(@m, top) |
立即打印(配合 interval) |
printf / time("%H:%M:%S ") / strftime |
输出 |
str(ptr) / str(ptr, len) |
读字符串 |
ksym(addr) / usym(addr) |
地址转符号 |
ntop(addr) |
IP 转字符串 |
system("cmd") / cat("file") / exit() |
需要 --unsafe |
作用域变量 $x 只在一个动作块内有效;@x 是全局 map。
常用一行脚本
# 谁在执行新程序
# 系统调用按进程计数
# read() 返回字节数分布
# 某内核函数的延迟直方图(入口记时间戳,返回相减)
# 磁盘 IO 大小
# 新建 TCP 连接
# 谁在调 malloc(按调用栈)
# CPU 采样火焰图原料
# 每 5 秒打印一次 top 5 然后清零
脚本文件
#!/usr/bin/env bpftrace
bpftrace 仓库的 tools/ 目录有几十个现成脚本(bashreadline.bt、biolatency.bt、runqlat.bt、tcpconnect.bt 等)。
十一、libbpf + CO-RE:手写一个程序
以"统计每个进程调 openat 的次数"为例。分内核态和用户态两个文件。
内核态:count.bpf.c
char LICENSE = "GPL"; // 部分 helper 只对 GPL 程序开放
counts ;
int
要点:
SEC("...")决定程序类型和挂载点,libbpf 按名字自动挂载- map 用
SEC(".maps")声明,libbpf 加载时自动创建 bpf_map_lookup_elem返回值必须判空,否则验证器拒绝- 读内核结构字段用
BPF_CORE_READ(task, mm, start_brk)宏,CO-RE 会重定位偏移;直接task->mm->start_brk在 tracepoint/kprobe 程序里不通过验证器(fentry/tp_btf 可以)
编译与生成 skeleton
-g 必须加,BTF 和 CO-RE 重定位信息来自 DWARF。skeleton 是一个头文件,把 ELF 对象嵌进去并生成 count_bpf__open() 等函数。
用户态:count.c
int
向用户态推事件:ring buffer
计数用 map 够了;要把每个事件(含时间戳、pid、文件名)送到用户态时用 ring buffer:
// 内核态
rb ;
;
int
// 用户态
static int
struct ring_buffer* rb = ;
while ;
ring buffer 与 perf buffer 的对比见第七章。
开发中的常见报错
| 现象 | 原因 |
|---|---|
invalid mem access 'map_value_or_null' |
lookup 返回值没判空 |
back-edge from insn |
循环无界;改用 #pragma unroll 或 bpf_loop()(5.17) |
R1 type=inv expected=fp |
给 helper 传了标量而不是栈指针 |
program too large / BPF program is too large |
超出指令上限,拆成 tail call |
failed to find BTF for extern |
目标机内核没有 BTF(CONFIG_DEBUG_INFO_BTF),需要 BTFHub 的外部 BTF |
libbpf: failed to find valid kernel BTF |
同上 |
挂载失败 ENOENT |
函数名不存在或被内联;kprobe 要查 /proc/kallsyms |
十二、可观测性:诊断思路与案例
为什么 eBPF 适合
| 需求 | 传统手段 | eBPF |
|---|---|---|
| 不改代码拿到内部状态 | 加日志、重新发版 | 动态挂到任意函数 |
| 高频事件统计 | 事件逐条拷到用户态(strace、perf record),开销大 |
在内核里聚合,只把直方图/计数交给用户态 |
| 抓瞬时现象 | 采样工具会漏掉短时进程、偶发丢包 | 事件驱动,每次发生都执行 |
| 跨层关联 | 用户态和内核态数据各自为政 | 同一个程序里同时拿到 pid、cgroup、内核栈、用户栈 |
通用数据流:
flowchart LR
E["钩子<br/>tracepoint/kprobe/uprobe"] --> P["BPF 程序<br/>过滤 + 聚合"]
P -->|"计数 / 直方图"| M[("HASH / PERCPU map")]
P -->|"逐条事件"| RB[("RINGBUF")]
M -->|"周期读取"| U["用户态:展示 / 导出指标"]
RB -->|"epoll 唤醒"| U
本章讲几个有代表性的诊断思路;具体工具见后面几章:内存泄漏(第十三章)、CPU(第十四章)、内存与 IO(第十五章)、用户态程序(第十六章)、网络(第十七章)。
短时进程:top 看不到的 CPU 消耗
现象:CPU 使用率高,top 里找不到占用高的进程。原因往往是大量运行几毫秒就退出的进程(脚本里循环调用外部命令、崩溃重启),top 按间隔采样 /proc,看不到它们。
思路:进程创建一定经过 execve,挂 tracepoint/syscalls/sys_enter_execve 和 sys_exit_execve:入口记录文件名和参数到以 tid 为 key 的 hash map,出口取出并附上返回值,经 ring buffer 发给用户态。BCC 的 execsnoop、bpftrace 的 execsnoop.bt 就是这个结构。
参数不能在入口直接发送的原因:入口时参数还是用户态指针,入口和出口分开处理可以拿到 execve 是否成功。
网络丢包:丢在内核哪一行
内核释放 sk_buff 有两个函数:kfree_skb(异常释放,丢包时调用)和 consume_skb(正常处理完释放)。跟踪 kfree_skb 的调用栈就能定位丢包点:
# 5.17+:tracepoint 带丢包原因枚举
# 旧内核:kprobe + 调用栈,限定进程
拿到类似 nf_hook_slow+0x... 的栈帧后,用内核源码里的 scripts/faddr2line vmlinux nf_hook_slow+0x48/0xd0 换算成源码行号(需要带调试信息的 vmlinux)。栈里出现 nf_hook_slow 基本说明包被 iptables/nftables 规则丢了。
服务网格中的偶发 RST
现象(来自一个生产案例的简化):客户端向 Kubernetes Pod 上传数据,连接偶发中断;抓包看到 Pod 发出了 TCP RST。Pod 注入了 Envoy sidecar,入站流量经 iptables DNAT 先到 Envoy,再由 Envoy 转给业务进程。
诊断:内核发 RST 的函数是 tcp_v4_send_reset()(收到不属于任何连接的包时回复)和 tcp_send_active_reset()(本端主动中止)。kprobe 挂这两个函数,打印触发时的 socket 状态、收到的包的四元组、内核栈。
发现:
- 发 RST 的 socket 处于
TCP_LISTEN,监听的是业务端口——即业务进程的监听 socket 收到了一个包 - 这个包的目的地址是业务端口,说明它没有经过 DNAT 转给 Envoy
根因:conntrack 在默认配置下把窗口外的乱序包标记为 INVALID,INVALID 包不做 NAT,于是这个包直接送到了业务进程的监听端口;监听 socket 找不到对应连接,回复 RST,客户端的连接被打断。解决方法是开启 net.netfilter.nf_conntrack_tcp_be_liberal=1,或丢弃 INVALID 状态的包。
这个案例说明:eBPF 的写法很固定,真正的门槛是知道该跟踪哪个内核函数。它要求理解网络包从驱动到 socket 经过的每个环节。
容器感知的跟踪
容器共享宿主机内核,内核态的跟踪天然覆盖所有容器;问题是区分事件来自哪个容器。
| 方法 | 做法 |
|---|---|
| 命名空间编号 | 从 task_struct->nsproxy 读 pid_ns_for_children->ns.inum(PID 命名空间)或 uts_ns->name.nodename(容器主机名) |
| cgroup id | bpf_get_current_cgroup_id(),用户态把 cgroup id 映射到容器 id / Pod 名(cgroup v2) |
| 过滤 | 把关心的 cgroup 放进 CGROUP_ARRAY,程序里用 bpf_current_task_under_cgroup() 判断 |
// 读当前进程所在 PID 命名空间的编号(CO-RE 写法)
struct task_struct* t = ;
unsigned int pidns = ;
跟踪容器内的用户态程序:容器的文件系统在另一个 mount 命名空间里,宿主机上通过 /proc/<宿主机 PID>/root/ 访问:
PID=
uprobe 按"文件 inode + 偏移"挂载,所以挂在容器镜像里的某个二进制上,会命中所有使用这个文件(同一镜像层)的进程。
云原生可观测平台
| 项目 | 做什么 | eBPF 的角色 |
|---|---|---|
| Pixie(CNCF) | K8s 内的请求级观测,自动解析 HTTP/gRPC/MySQL 等协议 | kprobe 挂 socket 读写 + uprobe 挂 TLS 库,无侵入抓请求 |
| Hubble(Cilium) | 服务间网络流量可视化、策略审计 | 复用 Cilium 数据面的 BPF 程序输出的流事件 |
| Parca、Pyroscope、OpenTelemetry eBPF Profiler | 全集群持续 CPU 剖析 | perf_event 定时采样 + 内核里做栈回溯(对无帧指针的程序用 .eh_frame 展开) |
| Grafana Beyla、DeepFlow | 无需 SDK 的自动埋点、分布式追踪 | uprobe/kprobe 关联请求与响应,生成 RED 指标和 trace |
| Inspektor Gadget | kubectl 插件形式的 BCC 类工具集 | 按 Pod/命名空间过滤的跟踪工具 |
共同点:零侵入——不改应用代码、不注入 SDK、不重启 Pod,部署一个 DaemonSet 就覆盖整个节点。
十三、内存泄漏排查
原理
用户态内存泄漏的 eBPF 排查法与 memory-leak.md 第九章相同:在分配/释放函数上挂探针,配对记录,定期输出"分配了还没释放"的调用栈。
uprobe:malloc ──▶ 记 size 到 @sizes[tid]
uretprobe:malloc ─▶ 拿返回地址 addr:@allocs[addr] = {size, stack_id, timestamp}
@stacks 是 STACK_TRACE map,同一个栈只存一份
uprobe:free ──▶ delete @allocs[addr]
用户态每 N 秒 ──▶ 遍历 @allocs,按 stack_id 汇总字节数,排序输出
它不判断可达性(LSan 的做法),只看"分配了、还没释放"。所以:
- 逻辑泄漏(可达但没用的缓存)能显现:那个栈的持有量单调涨
- 正常长期持有的对象(启动时分配的全局对象)也会出现在列表里,要看趋势而不是单次绝对值,
-o参数过滤掉年轻分配是为了去掉正常的短命对象
memleak(BCC)
输出格式:
[14:02:31] Top 10 stacks with outstanding allocations:
2048000 bytes in 2000 allocations from stack
operator new(unsigned long)+0x1c [libstdc++.so.6.0.33]
std::__cxx11::basic_string<...>::_M_create(unsigned long&, unsigned long)+0x2a [prog]
Cache::insert(std::string const&)+0x5b [prog]
handle_request(Request const&)+0x120 [prog]
main+0x38 [prog]
4096 bytes in 1 allocations from stack
...
判断方法:连续几轮里同一个栈的 bytes 单调上涨且与请求量成正比,就是泄漏点;稳定不变的是启动期的长期对象。
它挂的函数:malloc、calloc、realloc、posix_memalign、aligned_alloc、valloc、memalign、pvalloc、free;
-O 指定别的库;C++ 的 operator new 最终走 malloc,自动覆盖。
mmap 直接分配的内存不经过这些函数,要另挂 tracepoint:syscalls:sys_enter_mmap/munmap。
bpftrace 自写版
没有 BCC 时,bpftrace 能实现简化版(未实测):
#!/usr/bin/env bpftrace
&&
&&
bpftrace 不方便在 BPF 侧按栈汇总字节数(map 的 key 不能是另一个 map 的值),所以把地址级记录打出来在用户态聚合。高频分配的程序用 BCC 的 --combined-only。
只看"谁在大量分配"(不配对 free)更简单,也常常够用:
堆增长的直接证据:brk / mmap
分配器向内核要内存只有两条路,挂上它们看到的是"谁触发了堆增长":
内核内存泄漏
/proc/meminfo 里 Slab、SUnreclaim 持续涨、slabtop 里某个 cache 的对象数只增不减,是内核或驱动泄漏。
内核自带的 kmemleak(CONFIG_DEBUG_KMEMLEAK)做的是可达性扫描,和 LSan 一个思路,生产内核一般不开;eBPF 的方法不需要重新编译内核。
限制
- frame pointer:uprobe 的用户态栈回溯默认走帧指针链,被测程序和它调用的库要
-fno-omit-frame-pointer,否则栈只有一两层。Ubuntu 24.04 起系统库默认保留 frame pointer;Fedora 38 起同样 - 符号:strip 过的二进制只能显示地址;准备 debug 包或
debuginfod - 开销:每次
malloc/free各一次 uprobe 陷入,单次 1-3 微秒;每秒百万次分配的程序会慢数倍。先用-s采样、-z过滤小块、--combined-only - 短命进程:进程启动前挂不上,用
-c让工具拉起进程 - 静态链接的分配器:符号在主程序里,
-O ./prog - 不判断可达性:报告里的栈是"嫌疑人",不是"定罪"
十四、CPU 性能分析
CPU 问题分两类:on-CPU(在跑,跑什么)和 off-CPU(没在跑,在等什么)。eBPF 两边都能覆盖,而且聚合在内核里完成,不需要像 perf record 那样把每个样本写盘。
on-CPU:profile 与火焰图
原理:perf_event 定时器每 CPU 每秒触发 99 次(错开 100 Hz 的内核时钟避免锁相),每次触发时 BPF 程序取当前的用户态 + 内核态调用栈,存进 STACK_TRACE map 并计数。结束时输出"栈 → 次数"。
折叠格式每行是 comm;frame1;frame2;...;frameN count,直接喂给 FlameGraph:
bpftrace 版:
|
怎么读火焰图:横轴是样本占比(不是时间顺序),纵轴是栈深度,顶部是正在执行的函数。找最宽的顶部平台,那是 CPU 真正花在哪里;顺着往下看是谁调的它。
颜色默认随机,--color=java 之类只是按语言着色。
与 perf 的差别:perf record -F 99 -g 把每个样本(含完整栈)写进 perf.data,30 秒几百 MB;profile 在内核里按栈去重计数,输出只有几百 KB,长时间采样无压力。
off-CPU:offcputime
on-CPU 火焰图看不见"在等锁、等 IO、等网络"的时间。off-CPU 分析挂在调度器切换点:线程被切走时记时间戳,切回来时算差值,按栈累加。
bpftrace 版:
off-CPU 火焰图里宽的平台通常是 futex_wait(锁竞争)、io_schedule(磁盘)、tcp_recvmsg(等网络)、epoll_wait(空闲,正常)。
sched_switch 每秒可能几十万次,这个工具开销比 profile 大得多,用 -p 限定进程。
把 on-CPU 和 off-CPU 拼在一起就是墙钟时间的完整分解(Brendan Gregg 称为 hot/cold 火焰图)。
调度延迟:runqlat / runqlen / runqslower
线程"想跑但排队"的时间。CPU 饱和、cgroup 限流、错误的亲和性都会体现在这里。
原理:sched_wakeup 记时间戳,sched_switch 切到它时相减。典型输出是 usecs 直方图,健康系统集中在 0-32 微秒,出现毫秒级的长尾就是 CPU 不够。
函数级:funccount / funclatency / argdist
funclatency -F 按函数分开、-m 毫秒、-T 带时间戳。
硬件计数器:PMU
perf stat 看总量,eBPF 看"哪个栈"产生了这些事件。
中断与软中断
网络包处理时间大头在 softirq 里,不算在任何进程的 CPU 上,top 里表现为 si 高。
CPU 分析工具选择
| 症状 | 工具 |
|---|---|
| CPU 高,不知道跑什么 | profile → 火焰图 |
| CPU 不高但慢 | offcputime → 火焰图;看 futex / IO / 网络 |
| 延迟抖动 | runqlat(调度)、hardirqs/softirqs(中断)、cpudist(时间片) |
| 某个函数慢 | funclatency,再用 argdist 看参数分布 |
| 吞吐上不去 | llcstat、hardware:cache-misses 看内存访问 |
| 容器被限流 | runqlat --pidnss,配合 cpu.stat 的 nr_throttled |
十五、内存子系统与 IO
内存
大页相关:
块设备与文件系统
biolatency 看的是块层,ext4slower 看的是文件系统层(包含 page cache 命中的情况),应用感受到的是后者。两者差距大说明问题在文件系统或锁,不在磁盘。
十六、用户态程序跟踪
uprobe 挂 C++ 函数
C++ 符号是 mangled 的,先查名字:
| |
# 调用次数
# 延迟
注意:
- 成员函数的
arg0是this,真正的第一个参数从arg1开始 - 内联函数没有入口可挂;
-O2下小函数大多被内联。用-fno-inline重编或挂它的调用者 - uretprobe 的实现是把返回地址替换成 trampoline,与某些栈保护机制、协程切换栈冲突;能用
fexit式的 uprobe(6.x 起部分支持)就不用 uretprobe - 库函数写
uprobe:libc:malloc,bpftrace 会解析libc到实际路径;容器里的进程要用/proc/PID/root/path
读参数
# std::string const& 参数:libstdc++ 的 string 对象前 8 字节是数据指针
# C 字符串
# 结构体:用 bpftrace 的 struct 定义 + cast
有 DWARF 调试信息时 bpftrace 0.20+ 能直接用 args->name(需要 -g 编译的二进制和 bpftrace 编译时开了 DWARF 支持)。
USDT:应用主动埋点
在源码里放静态探针,未启用时只是一条 nop,几乎零开销;启用时被替换成 int3:
void
现成 USDT 探针:PostgreSQL、MySQL、Node.js、Python(--with-dtrace 构建)、JVM(-XX:+ExtendedDTraceProbes)、glibc(libc:memory_malloc_retry 等)、libpthread(mutex_acquired 等)。
TLS 明文
加密发生在用户态库里,在 SSL_write 入口和 SSL_read 返回处挂 uprobe,就能拿到加密前/解密后的数据(BCC sslsniff,Pixie 用同样的方法做 HTTP/gRPC 协议解析)。不需要证书,也不改应用。
解释型 / JIT 语言
| 语言 | 方法 |
|---|---|
| Python | USDT python:function__entry(需要 dtrace 构建);BCC 的 pythoncalls、pythonflow、pythongc;或 py-spy(非 eBPF) |
| Java | perf-map-agent 导出 JIT 符号到 /tmp/perf-PID.map,profile 就能解析栈;USDT 需 -XX:+ExtendedDTraceProbes;javacalls、javagc |
| Node.js | --perf-basic-prof 导出符号;USDT 内置 |
| Go | 静态链接、无 libc,uprobe 直接挂函数。不能用 uretprobe——Go 的 goroutine 栈会增长和搬迁,uretprobe 改写了栈上的返回地址,搬迁后返回地址错乱导致崩溃。做法是反汇编函数,在每条 RET 指令上挂 uprobe。另外 Go 1.17 起内部函数用寄存器传参,参数位置按 Go 自己的 ABI 取 |
| Rust | 与 C++ 相同,符号 mangled;rustfilt demangle |
十七、网络:跟踪与数据面
网络问题的排查(本章前半)和网络数据面的实现(本章后半)用的是不同的钩子:前者用 kprobe/tracepoint 观察协议栈,后者用 XDP、tc、socket 程序改变包的去向。
跟踪
tcpdrop 和 kfree_skb 的 reason 是排查"丢包在哪"的最直接手段,替代过去猜 netstat -s 里哪个计数在涨。
收发包路径上的钩子
flowchart TD
subgraph RX["收包方向"]
NIC["网卡"] --> XDP["XDP<br/>驱动收包,未分配 skb"]
XDP --> SKB["分配 sk_buff、GRO"]
SKB --> TCI["tc ingress<br/>有 skb,未进 IP 层"]
TCI --> NF1["netfilter PREROUTING<br/>(iptables / conntrack)"]
NF1 --> RT["路由"]
RT --> CGI["cgroup_skb ingress"]
CGI --> SK["socket 接收队列<br/>socket filter / sk_skb"]
SK --> APP["应用 recv()"]
end
subgraph TX["发包方向"]
APP2["应用 connect() / send()"] --> CSA["cgroup/connect4、sendmsg4<br/>系统调用时改写目的地址"]
CSA --> SKM["sk_msg<br/>sockmap 重定向"]
SKM --> TCP["TCP/IP 协议栈"]
TCP --> CGE["cgroup_skb egress"]
CGE --> NF2["netfilter OUTPUT / POSTROUTING"]
NF2 --> TCE["tc egress"]
TCE --> NIC2["qdisc → 网卡"]
end
越靠近网卡越快、能拿到的上下文越少;越靠近 socket 越"懂"应用,但每个包走过的路径越长。
| 钩子 | 方向 | 数据结构 | 能看到的信息 | 典型用途 |
|---|---|---|---|---|
| XDP | 只有收 | xdp_buff:原始帧的 data/data_end |
只有报文字节 | DDoS 丢包、四层负载均衡、快速转发 |
| tc | 收、发 | sk_buff |
报文 + mark、cgroup、ifindex 等元数据 | 容器网络、策略、NAT、限速 |
| cgroup sock_addr | 发起连接时 | socket 地址 | 进程、cgroup、目的地址 | socket 级负载均衡 |
| sockops + sk_msg | socket 事件 / 发送 | socket | 连接五元组、TCP 状态 | 同机 socket 直连、TCP 参数调优 |
XDP
三种运行模式:
| 模式 | ip link 参数 |
运行位置 | 性能 | 条件 |
|---|---|---|---|---|
| generic | xdpgeneric |
协议栈入口,skb 已分配 | 与 tc 相当 | 任何网卡(包括 veth),用于测试 |
| native | xdpdrv |
驱动 NAPI 收包函数里,skb 分配之前 | 高 | 驱动支持(ixgbe、i40e、ice、mlx5、virtio_net、veth 等) |
| offload | xdpoffload |
网卡硬件 | 最高,不占主机 CPU | 少数 SmartNIC(如 Netronome) |
返回码:
| 返回码 | 动作 |
|---|---|
XDP_DROP |
丢弃,驱动直接回收页,是内核里代价最低的丢包方式 |
XDP_PASS |
交给协议栈继续处理 |
XDP_TX |
从收包的同一网卡发回去 |
XDP_REDIRECT |
转发到另一网卡(DEVMAP)、另一 CPU(CPUMAP)或 AF_XDP socket(XSKMAP) |
XDP_ABORTED |
程序出错,丢弃并触发 xdp:xdp_exception tracepoint |
XDP 不是"绕过内核":它运行在内核里,处理完的包可以 XDP_PASS 继续走完整协议栈。这与 DPDK 那种把网卡交给用户态、彻底绕过内核的方案不同——XDP 可以只拦截关心的流量,其余流量和内核的路由、iptables、socket 照常工作。AF_XDP 则是折中:选定的流量经 XSKMAP 零拷贝送到用户态处理。
XDP 快的原因:在 skb 分配之前运行(省掉 skb 分配和初始化),不经过 GRO、netfilter、路由;一次 NAPI poll 处理一批包,程序和数据都在缓存里。单核 XDP_DROP 可达每秒千万包量级。
最小例子:丢弃发往 UDP 53 端口的包(未实测)。
int
tc
- 通过
clsactqdisc 挂在入向和出向,da(direct-action,4.4 起)模式下程序的返回值直接就是动作(TC_ACT_OK、TC_ACT_SHOT、TC_ACT_REDIRECT),不需要再配 tc 的 action - 收包时在 GRO 之后、IP 层和 iptables 之前执行;发包时在 IP 层和 iptables 之后、进入网卡队列之前执行
- 能挂任何网络设备,包括容器的 veth,是容器网络的主力钩子
bpf_redirect_peer()(5.10):从宿主机侧 veth 的 ingress 直接把包送进容器命名空间内的对端设备,跳过一次软中断和 backlog 队列- 6.6 的 tcx 用
bpf_link管理 tc 程序,支持多程序有序排列;6.7 的 netkit 设备专为容器设计,替代 veth,在设备的发送路径上直接执行 BPF 程序
socket 层:同机 socket 直连
场景:同一台机器上两个进程通过 TCP 通信(应用 ↔ sidecar、应用 ↔ 本机负载均衡器)。正常路径是发送方走完 TCP/IP 发送栈、经 loopback 或 veth、再走完接收栈,两遍协议栈处理只是为了把数据从一个 socket 挪到另一个 socket。
做法:
sequenceDiagram
participant A as 进程 A 的 socket
participant OPS as sockops 程序<br/>(挂在 cgroup)
participant MAP as SOCKHASH<br/>key=五元组
participant MSG as sk_msg 程序<br/>(挂在 SOCKHASH)
participant B as 进程 B 的 socket
Note over A,B: 连接建立(ACTIVE/PASSIVE_ESTABLISHED_CB)
OPS->>MAP: bpf_sock_hash_update(本端五元组 → 本 socket)
Note over A,B: 数据发送
A->>MSG: send(),进入 sk_msg
MSG->>MAP: 用对端五元组查找对端 socket
MSG->>B: bpf_msg_redirect_hash():<br/>数据直接放进 B 的接收队列
Note over A,B: 跳过 TCP/IP 发送栈、网卡、接收栈
要点:
sockops挂在 cgroup(bpftool cgroup attach /sys/fs/cgroup/ sock_ops pinned ...),对 cgroup 内所有 socket 生效;在连接建立的两个回调里把 socket 存进SOCKHASHsk_msg挂在 map 上(bpftool prog attach ... msg_verdict pinned <map>),每次sendmsg执行- 查找对端时,key 是把本端五元组的源和目的对调
- 只对两端 socket 都在同一台主机内核里的连接有效;跨网络命名空间也可以(容器之间),前提是两端看到的五元组能对上,即中间没有 NAT。跨主机流量照常走协议栈
- 用这种方式做 sidecar 加速的开源项目有 Merbridge(Istio / Linkerd)
四层负载均衡
课程案例:同一宿主机上的 Docker 容器,客户端 → 负载均衡器 → 两个 Web 后端,用 wrk -c100 压测。
| 方案 | 请求数/秒 | 平均延迟 | 说明 |
|---|---|---|---|
| Nginx 反向代理 | 13798 | 7.53 ms | 用户态七层代理 |
| Nginx + sockops/sk_msg | 15300(+10.8%) | 6.88 ms | Nginx 仍在路径上,省掉的是同机 socket 之间的协议栈 |
| XDP 负载均衡(generic 模式,挂在 veth) | 18048(比上一行 +18%) | 6.37 ms | 在收包时改写 MAC/IP、重算校验和后 XDP_TX 发回 |
数据来自课程的容器实验,环境不同结果不同。XDP 用的是 generic 模式,native 模式下差距会更大。
课程里的 XDP 程序是教学用的最小实现(后端 IP/MAC 写死、按时间戳最低位随机选后端)。逐包随机选后端会把同一个 TCP 连接的包发往不同后端,生产实现必须按连接保持一致。生产级四层负载均衡(如 Meta 的 Katran)的设计要点:
| 要点 | 做法 |
|---|---|
| VIP 查找 | HASH:(VIP, 端口, 协议)→ 服务配置 |
| 后端选择 | 对五元组做哈希,查一致性哈希环(Katran 用 Maglev 算法),后端增减时只影响少量连接 |
| 连接保持 | LRU_HASH(per-CPU 版本)记录五元组 → 后端,已有连接优先查表,后端列表变化也不影响存量连接 |
| 转发方式 | IPIP/GUE 封装后发给后端,不改原始包;后端解封装后直接回包给客户端(DSR),回程流量不经过负载均衡器 |
| 控制面 | 用户态进程做健康检查,更新后端 map;数据面程序无需重载 |
| 横向扩展 | 多台负载均衡器通过 ECMP 共享同一个 VIP;一致性哈希保证任一台都把同一连接发往同一后端 |
| 校验和 | 改写报头后用 bpf_csum_diff / bpf_l3_csum_replace 增量更新,否则接收方丢包 |
一个按连接保持的 XDP 骨架(示意,未实测,省略了 IP 选项、分片、IPv6、校验和细节):
;
;
conn_table ;
backends ;
const volatile __u32 nr_backends = 2; // 用户态加载前设置
int
char LICENSE = "GPL";
调试 XDP 时应在容器或网络命名空间里挂到 veth 上:程序写错会把整张网卡的流量丢掉,包括远程登录用的 SSH。
Kubernetes 容器网络:Cilium
kube-proxy 的 iptables 模式把每个 Service 展开成 iptables 规则链。Cilium 用 eBPF 替代 kube-proxy 和大部分 iptables 规则:
| 维度 | kube-proxy(iptables 模式) | Cilium eBPF |
|---|---|---|
| Service 查找 | KUBE-SERVICES 链上按规则顺序匹配,Service 越多越慢 |
hash map 查找,与 Service 数量基本无关 |
| 规则更新 | 规则集整体重写(iptables-restore),规模大时耗时明显 |
增量更新 map 中的单个条目 |
| 集群内访问 Service | 每个包经过 conntrack + DNAT | cgroup/connect4 在 connect() 时直接把目的地址改成后端 Pod,之后的包不再需要 NAT |
| NodePort / 外部流量 | iptables DNAT + SNAT | tc 或 XDP 程序处理,支持 DSR |
| 网络策略 | iptables 规则按 IP 匹配 | 按身份(由 Pod 标签派生的数字 identity)匹配,Pod IP 变化不需要改规则;可做 L7(HTTP 路径)策略 |
| 可观测 | 需要另外抓包 | Hubble 直接输出每条流的策略判定结果 |
Cilium 同时用了本章所有的钩子:XDP 做 NodePort 加速和 DDoS 防护,tc 挂在每个 Pod 的 veth(或 netkit)上做策略和转发,cgroup sock_addr 做 socket 级负载均衡,sockops/sk_msg 做同机加速。GKE Dataplane V2 基于 Cilium,Calico 也提供了 eBPF 数据面。
其他网络用途
| 用途 | 机制 |
|---|---|
| DDoS 防护 | XDP 在驱动层按规则(LPM_TRIE 存黑名单网段、按特征匹配)直接 XDP_DROP;Cloudflare 公开过基于 XDP 的丢包方案 |
| 出向限速 | tc 程序给 skb 设置发送时间戳(EDT,Earliest Departure Time),配合 fq qdisc 按时间发送,替代 HTB 的多级队列与锁(Cilium Bandwidth Manager) |
SO_REUSEPORT 调度 |
sk_reuseport 程序决定新连接进入组内哪个 socket,实现按 CPU/按业务分流、进程热升级时的连接迁移 |
| socket 查找 | sk_lookup(5.9)程序决定包交给哪个监听 socket,一个 socket 可以接管整段 IP/端口范围 |
| 流量统计 | cgroup_skb 按 cgroup 计量流量;Android 从 9 开始用 eBPF 统计每个应用的流量 |
| 可编程路由 | LWT(轻量隧道)程序、SRv6(LWT_SEG6LOCAL) |
十八、安全
容器共享宿主机内核,namespace、cgroup、capabilities 负责隔离和资源限制,但它们不负责"观察容器里正在发生什么"和"根据运行时行为动态拦截"。eBPF 补的是这一块。
三种安全能力
| 能力 | 目标 | 钩子 | 代表项目 |
|---|---|---|---|
| 分析诊断 | 事件发生后还原现场:谁、何时、执行了什么、访问了什么 | kprobe、tracepoint、uprobe | Tracee(Aqua) |
| 检测告警 | 实时发现可疑行为:容器内起了 shell、写 /etc/passwd、提权、异常外连 |
syscall tracepoint、LSM、kprobe | Falco(CNCF,Sysdig 贡献) |
| 策略执行 | 在内核里阻止违规操作 | LSM、bpf_send_signal、bpf_override_return、cgroup、XDP |
Tetragon(Cilium)、KubeArmor |
eBPF 相对传统方案的优势:
- 事件驱动:每次系统调用、每次文件打开都会触发,不存在采样工具的漏检
- 在内核里就地过滤,只把命中规则的事件交给用户态,开销低
- AppArmor、seccomp 需要预先写好策略、改策略要重启容器;eBPF 程序的规则在 map 里,用户态随时更新即时生效
阻断的三种方式与它们的差别
| 方式 | 时机 | 能否真正阻止 | 说明 |
|---|---|---|---|
tracepoint + bpf_send_signal(SIGKILL) |
系统调用入口 | 不可靠 | 信号要等返回用户态时才处理,系统调用本身可能已经执行完成(文件已写入、连接已建立)。bpftrace 里对应 signal(),需要 --unsafe |
kprobe + bpf_override_return |
目标函数入口 | 能,但范围有限 | 让函数不执行、直接返回指定错误码,只对标注了 ALLOW_ERROR_INJECTION 的函数有效(包括大部分系统调用入口) |
| LSM 程序 | LSM 钩子,内核做权限检查的位置 | 能 | 返回 -EPERM 即拒绝,与 SELinux/AppArmor 在同一个检查点,同步生效 |
LSM 程序示例(示意,未实测):禁止指定 cgroup 内的进程执行 /bin/sh。
const volatile __u64 target_cgid = 0;
int
char LICENSE = "GPL";
启用条件:内核配置 CONFIG_BPF_LSM=y,且启动参数的 lsm= 列表中包含 bpf(cat /sys/kernel/security/lsm 查看)。部分发行版默认不包含,需要改 grub 配置后重启。
TOCTOU:系统调用参数不可信
在 sys_enter_openat 里读取路径参数做判断,存在检查时间与使用时间之间的竞争:
线程 A:openat(path_ptr) ──▶ [BPF 读 *path_ptr = "/tmp/ok",放行] ──▶ 内核 copy_from_user(*path_ptr)
线程 B: 在这两步之间把 *path_ptr 改成 "/etc/shadow"
BPF 读到的和内核实际使用的不是同一份数据。攻击者可以借此绕过检测。对策:在内核已经把参数复制到内核内存之后的位置取数据——LSM 钩子(如 security_file_open 拿到的是已解析的 struct file)或 fentry 挂在内部函数上,而不是系统调用入口。Tetragon 等项目优先挂在 LSM 和内核内部函数上,原因就在这里。
与其他安全机制的关系
| 机制 | 与 eBPF 的关系 |
|---|---|
| seccomp | 使用的是 cBPF,只能看系统调用号和参数的值(不能解引用指针)。Docker/K8s 的 seccomp profile 走这条路 |
| cgroup v2 设备控制 | 没有 devices.allow 文件,runc / systemd 生成 CGROUP_DEVICE 类型的 eBPF 程序实现 |
| AppArmor / SELinux | 同属 LSM 框架,BPF LSM 与它们叠加执行,任何一个拒绝即拒绝 |
| iptables | 网络层的访问控制可以由 tc/XDP/cgroup_skb 程序替代 |
eBPF 自身的攻击面
eBPF 的能力对攻击者同样有用:
- 验证器漏洞提权:非特权用户加载精心构造的程序,利用验证器范围推演错误读写内核内存。对策:保持
kernel.unprivileged_bpf_disabled=1或2,及时更新内核 - eBPF rootkit:已获得 root 的攻击者加载 BPF 程序持久化——挂
getdents64出口用bpf_probe_write_user改写返回的目录项来隐藏进程和文件,或挂 XDP 实现隐蔽后门。对策:审计bpf()系统调用(auditd、Falco 规则)、定期用bpftool prog list/bpftool link list盘点、开启内核 lockdown(integrity/confidentiality模式会限制bpf_probe_write_user和读内核内存) - 权限收敛:给 agent 只授予
CAP_BPF+CAP_PERFMON(或CAP_NET_ADMIN),不要给CAP_SYS_ADMIN;6.9 的 BPF token 允许把受限的 BPF 权限委派给容器内的非特权进程。程序签名机制在社区推进中
十九、内核行为定制
跟踪和网络之外,eBPF 正在成为"用程序替换内核策略"的通用机制。核心是 struct_ops:内核里很多子系统用一组函数指针描述一种策略(拥塞控制算法、调度类),struct_ops 允许用 BPF 程序实现这组函数指针并注册。
TCP 拥塞控制(5.6)
struct tcp_congestion_ops 的回调(ssthresh、cong_avoid、cwnd_event 等)可以用 BPF 实现,并通过 kfunc 调用内核已有的 tcp_slow_start()、tcp_reno_cong_avoid() 等函数。内核 selftests 里有 bpf_cubic、bpf_dctcp 的完整实现。
意义:换算法、改参数不必再编译内核模块,可以按业务灰度上线。
sched_ext:用 BPF 写 CPU 调度器(6.12)
sched_ext 新增一个调度类,其行为由 BPF struct_ops 程序定义:选择 CPU(select_cpu)、入队(enqueue)、分派(dispatch)等。
| 特性 | 说明 |
|---|---|
| 安全退出 | BPF 调度器出错(任务长时间得不到调度、程序报错)时,内核自动卸载它,所有任务回到默认调度器(EEVDF) |
| 快速迭代 | 改调度策略不用重新编译、重启内核 |
| 示例调度器 | scx_simple(最简 FIFO/加权)、scx_lavd(面向游戏的延迟敏感调度,Valve/Igalia)、scx_rusty/scx_rustland(用户态 Rust 做决策) |
| 使用场景 | 游戏与桌面交互延迟、特定负载(如 Meta 在数据中心试验)、调度算法研究 |
其他
| 机制 | 版本 | 用途 |
|---|---|---|
| BPF iterator | 5.8 | 遍历内核对象(任务、文件、socket、map),按自定义格式输出,替代解析 /proc |
| HID-BPF | 6.3 | 修正 HID 设备(键盘、手写板)的报告描述符与数据,不必写内核驱动补丁 |
| fmod_ret / 错误注入 | 5.7 | 修改内核函数返回值,用于故障注入测试 |
| BPF timer / workqueue | 5.15 / 6.10 | 程序内定时执行逻辑,不依赖事件触发 |
| eBPF for Windows | — | 在 Windows 上提供 libbpf 兼容 API,使同一套 BPF 程序可在两个系统运行(开发中) |
| 用户态运行时 | — | bpftime 等项目在用户态执行 BPF 字节码,用于 uprobe 加速和插件系统 |
二十、生产环境:开销、限制与容器
各类探针的开销
| 探针 | 单次开销(量级) | 备注 |
|---|---|---|
| tracepoint / raw_tracepoint | 几十纳秒 | 最便宜 |
| fentry / fexit | 几十纳秒 | 直接跳转,无异常 |
| kprobe | 100 纳秒级 | 用 int3 或优化成跳转 |
| kretprobe | 比 kprobe 贵,且有 maxactive 限制(并发超过时丢事件) |
|
| uprobe | 1-3 微秒 | int3 陷入内核再返回,比 kprobe 贵一个数量级 |
| uretprobe | 比 uprobe 更贵 | 替换返回地址 |
| USDT | 未启用 0(nop),启用后同 uprobe |
|
| perf_event 采样 | 与频率成正比,99 Hz 可忽略 | |
| XDP | 每包几十纳秒 |
总开销 = 单次开销 × 事件频率 + BPF 程序自身执行时间(通常几十到几百纳秒)+ 栈回溯(ustack 几微秒)。
估算:事件每秒 10 万次、每次 2 微秒 = 每秒 0.2 秒 CPU = 20% 一个核。sched_switch、malloc、tcp_sendmsg 都可能到这个量级,先用 funccount 数频率再决定挂不挂。
降低开销的手段:
- 挂 tracepoint/fentry 而非 kprobe,挂 USDT 而非 uprobe
- 过滤条件写在 BPF 侧(
/pid == N/),不要把所有事件传到用户态再过滤 - 聚合在 BPF 侧(
count()、hist()),只把摘要传出来 - 采样(
memleak -s、profile -F) - per-CPU map 代替全局 map 计数,避免原子操作争用
- ring buffer 代替 perf buffer
- 限定
-p PID
bpftool prog profile 能直接测量某个已加载程序自身的 cycles/instructions。
栈回溯的前提
- 用户态:frame pointer。
ustack顺着rbp链走,-fomit-frame-pointer(GCC/Clang-O2默认)会让栈断在第一层。解决:编译加-fno-omit-frame-pointer;发行版层面 Ubuntu 24.04+、Fedora 38+ 默认保留。DWARF 回溯(perf --call-graph dwarf那种)eBPF 侧不支持,个别工具在用户态做后处理 - 内核态:内核自带 ORC unwinder,
kstack总是完整的 - 符号:主程序和库不能 strip;JIT 语言要导出
/tmp/perf-PID.map
内核与权限要求
| 需要 | 说明 |
|---|---|
| 内核 4.9+ | 基本可用;5.4+ 才有 BTF 与 CO-RE;5.8+ ring buffer;越新越好 |
CONFIG_BPF_SYSCALL、CONFIG_BPF_JIT、CONFIG_DEBUG_INFO_BTF、CONFIG_KPROBES、CONFIG_UPROBES、CONFIG_TRACEPOINTS |
主流发行版默认全开 |
| 权限 | root,或 5.8+ 的 CAP_BPF + CAP_PERFMON(跟踪)+ CAP_NET_ADMIN(网络);kernel.unprivileged_bpf_disabled=1 是默认 |
/sys/kernel/debug 或 /sys/kernel/tracing 挂载 |
tracepoint 列表和 kprobe 事件在这里 |
kernel.perf_event_paranoid |
采样类需要 ≤ 1 或 root |
kernel.kptr_restrict=0 |
否则 kstack 全是 0 地址 |
在容器里跑 eBPF
- 容器里的 BCC/bpftrace 需要
--privileged(或上面那组 capability)、挂载/sys/kernel/debug、/sys/kernel/btf、/lib/modules - BCC 需要宿主机内核头文件:挂载
/usr/src和/lib/modules;libbpf CO-RE 工具不需要 - 跟踪容器内进程:
pid是宿主机视角的 pid;用--pidnss(runqlat)或cgroup内置变量按容器分组;uprobe 的路径写/proc/PID/root/usr/lib/... - Kubernetes 上常见部署形态是 DaemonSet(Pixie、Cilium Hubble、Parca、Inspektor Gadget 都是这样)
- 老内核(3.10 的 CentOS 7)不支持大部分功能,只能上
perf/ftrace
局限与应对
| 限制 | 原因 | 应对 |
|---|---|---|
| 内核版本决定可用特性 | 特性随版本逐步加入 | 生产最低 4.9 起步,建议 5.8+(ring buffer、CAP_BPF);用 bpftool feature probe 探测,程序里对可选特性做降级 |
| 栈只有 512 字节 | 内核栈本身很小,BPF 栈嵌在其中 | 大结构放 PERCPU_ARRAY 当临时缓冲区 |
| 合法代码被验证器拒绝 | 编译器优化后的形式验证器跟不上 | 看验证器日志最后几行;__always_inline;用常量收窄范围;新内核验证器更聪明 |
| 没有浮点 | 内核上下文不保存 FPU 状态 | 定点数;复杂计算放用户态 |
| 字符串处理 helper 很少 | 验证器难以证明变长操作安全 | 复杂解析放用户态 |
| 不能随意分配内存 | 程序可能运行在不能分配内存的上下文 | 预分配的 map;6.2 起 bpf_obj_new;6.9 起 arena |
| 不能做长计算 | 指令数上限、不能睡眠(可睡眠程序除外) | 拆成 tail call;计算放用户态 |
| 不能任意调用内核函数 | 只能用 helper/kfunc | 需要的能力没有对应 helper 就做不了 |
| kprobe 目标不稳定 | 函数改名、被内联、签名变化 | 优先 tracepoint / fentry;CO-RE;多版本兼容逻辑 |
| 老内核没有 BTF | 5.2 前无,发行版默认开启更晚 | BTFHub;或退回 BCC 现场编译 |
| 高频钩子开销 | uprobe 每次微秒级;kprobe 挂在每秒千万次的函数上也会有可见开销 | 先 funccount 估频率;内核里聚合;采样 |
| 事件丢失 | ring buffer 满、map 满、递归保护 | 监控 lost 计数和 recursion_misses;调大缓冲区;改为聚合 |
| 需要 root 或特定 capability | 安全模型 | 最小授权;K8s 里以 DaemonSet + 特定 capability 部署 |
| 调试手段少 | 不能单步、没有 printf 以外的输出 | bpf_printk + trace_pipe;BPF_PROG_TEST_RUN 用构造的输入跑网络程序;bpftool prog profile 看程序自身开销;veristat 统计验证复杂度 |
二十一、与 perf / ftrace / SystemTap 的比较
| eBPF(BCC/bpftrace/libbpf) | perf | ftrace | SystemTap | DTrace | |
|---|---|---|---|---|---|
| 机制 | 内核虚拟机,事件时执行程序 | perf_event 采样/计数,样本写 perf.data |
内核函数跟踪框架,/sys/kernel/tracing 文件接口 |
编译成内核模块加载 | Solaris/BSD/macOS 原生;Linux 版基于 eBPF |
| 可编程 | 完全可编程,内核内聚合 | 不可编程(只能记录) | 少量过滤和触发器 | 完全可编程 | 完全可编程 |
| 安全 | 验证器保证 | 安全 | 安全 | 内核模块,有崩溃风险 | 安全 |
| 数据量 | 摘要传出,小 | 全部样本落盘,大 | 文本 ring buffer | 可编程 | 可编程 |
| 开销 | 低,可控 | 采样低,跟踪(perf trace)高 |
低 | 中 | 低 |
| 用户态跟踪 | uprobe/USDT | uprobe | uprobe(有限) | 有 | 有 |
| 内核版本 | 4.9+ 可用,5.x 好用 | 2.6.31+ | 2.6.27+ | 需要内核调试信息 | |
| 典型用法 | 定制分析、常驻观测 | 快速 CPU 剖析、PMU 计数 | 函数调用图 function_graph、延迟跟踪 |
老系统深度跟踪 |
实践中:perf stat / perf top 看整体,perf record 做一次性 CPU 剖析,eBPF 做需要过滤、聚合、配对(延迟、泄漏)或长期常驻的分析,ftrace 的 function_graph 看内核函数调用树。
二十二、面试速答
eBPF 是什么,和内核模块有什么区别
内核中的受限虚拟机。用户态把 BPF 字节码通过 bpf() 系统调用交给内核,验证器在加载时静态证明程序一定结束、不越界、只调白名单 helper,通过后 JIT 成本机指令,挂载到钩子上由事件触发执行,状态存在 map 里与用户态共享。
安全来自验证器(静态)+ helper 白名单(接口)+ bpf_probe_read 的异常保护(运行时)+ capability(权限)。和内核模块相比:安全(验证器保证)、可移植(CO-RE)、随 fd 关闭自动卸载;代价是只能调白名单 helper、栈 512 字节、循环必须有界。
验证器具体检查什么
两遍:第一遍 DFS 检查控制流(无不可达指令、5.3 前无回边);第二遍沿所有路径模拟执行,为每个寄存器跟踪类型(ctx 指针、map value 指针、可能为空的指针、报文指针、标量)和取值范围,确保解引用前指针类型合法且偏移在边界内、map 查找结果判过空、报文访问前与 data_end 比较过、helper 参数类型匹配。用状态剪枝控制路径爆炸,上限 100 万条已处理指令。
kprobe、tracepoint、fentry、uprobe 的区别和实现
tracepoint 是编译进内核的静态点,static key 控制,稳定;kprobe 在任意指令处写断点或跳转,灵活但不稳定;fentry 利用函数入口的 ftrace nop 跳到 BPF trampoline,比 kprobe 快且参数带 BTF 类型;uprobe 在用户进程代码页写 int3,每次命中两次上下文切换,微秒级。选型:tracepoint > fentry > kprobe;用户态 USDT > uprobe。
BTF 和 CO-RE 解决什么问题
内核结构体布局随版本变。BTF 把内核类型信息嵌进内核本身;CO-RE 让 clang 把字段访问记录成"类型名 + 字段路径"的重定位项,libbpf 加载时查目标机 BTF 算出真实偏移再改写指令。结果是编译一次的静态二进制可以在不同内核上运行,目标机不需要内核头和 LLVM。
怎么用 eBPF 查内存泄漏
memleak -p PID:uprobe 挂 malloc/free 等,分配时记大小和调用栈到 map,释放时删掉,定期按栈汇总未释放字节数。连续几轮里持有量单调上涨的栈是泄漏点。它不判断可达性,所以逻辑泄漏也能显现,但正常的长期对象也在列表里,要看趋势,-o 过滤年轻分配。前提是被测程序有 frame pointer 和符号。
怎么用 eBPF 做 CPU 剖析,和 perf 的区别
profile -F 99:perf_event 定时器每 CPU 每秒 99 次触发,BPF 程序取当前用户态 + 内核态栈存到 STACK_TRACE map 计数,输出折叠栈喂 FlameGraph。区别是聚合在内核里完成,不像 perf record 把每个样本写盘,长时间采样输出仍然很小。CPU 不高但慢时用 offcputime 挂 sched_switch 看阻塞在哪。
eBPF 的开销怎么估
单次开销 × 事件频率 + 程序执行时间 + 栈回溯。tracepoint 几十纳秒、kprobe 百纳秒级、uprobe 微秒级。先用 funccount 数频率;过滤和聚合放在 BPF 侧,采样,限定进程。
XDP 为什么快,和 DPDK 有什么区别
XDP 在驱动收包时、分配 skb 之前运行,省掉 skb 分配、GRO、netfilter、路由,按批处理;XDP_DROP 不进协议栈,XDP_TX 直接从同一网卡发回,单核每秒千万包量级。DPDK 把网卡整个交给用户态轮询,完全绕过内核,要独占 CPU 核、自己实现协议栈;XDP 仍在内核里,只处理关心的包,其余 XDP_PASS 给正常协议栈,和内核的路由、socket、工具链共存。用户态确实需要收包时,XDP_REDIRECT 到 AF_XDP socket 零拷贝。
XDP 和 tc 程序怎么选
XDP 只有收包方向、没有 skb、需要驱动支持才快,适合在最早位置丢包或转发(DDoS、四层负载均衡)。tc 收发双向、有 skb 元数据、挂任何设备包括 veth,适合容器网络、策略、NAT、出向处理。
Cilium 为什么能替代 kube-proxy
kube-proxy 的 iptables 模式按规则链顺序匹配 Service,规则更新要整体重写。Cilium 把 Service → 后端存在 hash map 里,查找和更新都是 O(1) 级别;集群内访问在 connect() 时由 cgroup 程序直接改写目的地址,之后的包不需要 conntrack 和 DNAT;外部流量由 tc/XDP 处理。网络策略按 Pod 身份而非 IP 匹配。
sockmap 加速的原理和适用范围
sockops 程序在连接建立时把 socket 按五元组存进 SOCKHASH;sk_msg 程序在发送时用对端五元组查到对端 socket,调用 bpf_msg_redirect_hash 把数据直接放进对端接收队列,跳过两遍 TCP/IP 协议栈。只对同一主机上的 socket 对有效,典型是应用与 sidecar 之间。
用 eBPF 做安全阻断,为什么推荐 LSM 而不是 syscall tracepoint
tracepoint 里只能发信号,信号在返回用户态时才处理,系统调用可能已经完成;而且在系统调用入口读用户态参数存在 TOCTOU,攻击者可以在检查后、内核复制前改掉内存。LSM 钩子位于内核权限检查点,参数已在内核内存里,返回 -EPERM 同步拒绝。
生产环境部署 eBPF 要注意什么
内核 4.9+ 基本可用,5.8+ 较完整,确认 BTF 可用;最小权限(CAP_BPF + CAP_PERFMON/CAP_NET_ADMIN);被测程序保留 frame pointer 和符号;高频钩子先估开销;监控事件丢失;XDP/tc 程序不随进程退出,要有卸载与升级流程(用 bpf_link 管理);容器里要特权或对应 capability 并挂载 /sys/kernel/debug、/sys/kernel/btf;把 eBPF 当作攻击面审计。
附录:特性与内核版本、命令速查、参考资料
特性与内核版本
版本号为主线内核首次合入的版本;发行版可能回移植,以 bpftool feature probe 为准。
| 版本 | 特性 |
|---|---|
| 3.18 | bpf() 系统调用、hash/array map、eBPF socket filter |
| 4.1 | kprobe 程序、tc cls_bpf |
| 4.2 | tail call |
| 4.4 | bpffs pin、tc direct-action、非特权 socket filter |
| 4.6 | STACK_TRACE map、bpf_get_stackid |
| 4.7 | tracepoint 程序 |
| 4.8 | XDP |
| 4.9 | perf_event 程序 |
| 4.10 | cgroup skb/sock 程序 |
| 4.14 | sockmap、sk_skb |
| 4.15 | bpf_perf_event_read_value |
| 4.16 | BPF-to-BPF 调用、bpf_override_return(错误注入) |
| 4.17 | raw tracepoint、sk_msg、cgroup sock_addr |
| 4.18 | BTF、AF_XDP、bpf_get_stack |
| 5.1 | bpf_spin_lock |
| 5.2 | 指令上限 100 万(原 4096)、全局变量 |
| 5.3 | 有界循环、bpf_send_signal |
| 5.5 | fentry/fexit(BPF trampoline)、bpf_probe_read_user/kernel 区分 |
| 5.6 | struct_ops(TCP 拥塞控制) |
| 5.7 | BPF LSM、bpf_link、fmod_ret |
| 5.8 | ring buffer、CAP_BPF/CAP_PERFMON、BPF iterator、bpf_ktime_get_boot_ns |
| 5.9 | sk_lookup |
| 5.10 | 可睡眠程序、bpf_redirect_peer |
| 5.11 | map 内存改为 memcg 计费,不再受 RLIMIT_MEMLOCK 限制 |
| 5.13 | kfunc |
| 5.15 | BPF timer |
| 5.17 | bpf_loop()、kfree_skb 丢包原因 |
| 5.18 | kprobe_multi、XDP 多缓冲区 |
| 6.2 | bpf_obj_new 动态分配 |
| 6.4 | open-coded 迭代器 |
| 6.6 | tcx、uprobe_multi |
| 6.7 | netkit |
| 6.9 | BPF arena、BPF token |
| 6.12 | sched_ext |
命令速查
| 目的 | 命令 |
|---|---|
| 列出探针 | bpftrace -l 'tracepoint:syscalls:*'、bpftrace -lv tracepoint:sched:sched_switch |
| 一行计数 | bpftrace -e 'kprobe:X { @[comm] = count(); }' |
| 一行延迟 | bpftrace -e 'kprobe:X { @s[tid]=nsecs; } kretprobe:X /@s[tid]/ { @=hist(nsecs-@s[tid]); delete(@s[tid]); }' |
| 内存泄漏 | memleak-bpfcc -p PID -o 60000 5 |
| 内核泄漏 | memleak-bpfcc 10 |
| 谁在分配 | bpftrace -e 'uprobe:libc:malloc /pid==N/ { @[ustack]=sum(arg0); }' |
| on-CPU 火焰图 | profile-bpfcc -F 99 -adf 30 > o.folded && flamegraph.pl o.folded > cpu.svg |
| off-CPU 火焰图 | offcputime-bpfcc -p PID -f 30 > o.folded && flamegraph.pl --colors io o.folded > off.svg |
| 调度延迟 | runqlat-bpfcc 5、runqslower-bpfcc 10000 |
| 函数延迟 | funclatency-bpfcc -p PID -u c:malloc |
| 参数分布 | argdist-bpfcc -p PID -H 'p:c:malloc(size_t s):size_t:s' |
| 块 IO 延迟 | biolatency-bpfcc 5、biosnoop-bpfcc |
| 文件系统慢请求 | ext4slower-bpfcc 10 |
| page cache | cachestat-bpfcc 5 |
| 缺页 | bpftrace -e 'software:page-faults:1 { @[comm, ustack(4)]=count(); }' |
| OOM | oomkill-bpfcc |
| TCP 连接 | tcpconnect-bpfcc、tcpaccept-bpfcc、tcplife-bpfcc |
| 重传/丢包 | tcpretrans-bpfcc、tcpdrop-bpfcc |
| DNS 延迟 | gethostlatency-bpfcc |
| 已加载程序 | bpftool prog list、bpftool prog profile id N duration 10 cycles |
| map 内容 | bpftool map dump id N |
| 生成 vmlinux.h | bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h |
| 编译 BPF | clang -O2 -g -target bpf -c x.bpf.c -o x.bpf.o && bpftool gen skeleton x.bpf.o > x.skel.h |
| XDP 挂载 | ip link set dev eth0 xdpdrv obj x.bpf.o sec xdp |
| tc 挂载 | tc qdisc add dev eth0 clsact && tc filter add dev eth0 ingress bpf da obj x.bpf.o sec tc |
| socket 重定向挂载 | bpftool cgroup attach /sys/fs/cgroup/ sock_ops pinned /sys/fs/bpf/sockops、bpftool prog attach pinned /sys/fs/bpf/redir msg_verdict pinned /sys/fs/bpf/sock_map |
| struct_ops 注册 | bpftool struct_ops register x.bpf.o |
| 内核支持的特性 | bpftool feature probe |
| 当前启用的 LSM | cat /sys/kernel/security/lsm |
参考资料
- 倪朋飞,《eBPF 核心技术与实战》,极客时间
- Brendan Gregg,BPF Performance Tools,Addison-Wesley,2019
- Liz Rice,Learning eBPF,O'Reilly,2023
- 内核文档:
Documentation/bpf/(验证器、map、程序类型、kfunc 等) man 2 bpf、man 7 bpf-helpers、man bpftool- ebpf.io、docs.cilium.io 的 BPF 参考指南
- 内核源码:
kernel/bpf/verifier.c、kernel/bpf/syscall.c、include/uapi/linux/bpf.h、tools/testing/selftests/bpf/
暂无评论,欢迎留下第一条评论。