目录
正在加载目录…
专栏文章
专栏文章
Linux 专栏
1. 进程与线程 2. 内存管理 3. 文件系统 4. 网络与 IO 模型 5. 中断机制 6. 性能分析工具 7. eBPF 技术实战

eBPF 技术实战

发布于 2026-06-08 07:30 · 最后编辑于 2026-07-31 15:52 · 字数 3,419 👁 75 次阅读

eBPF(extended Berkeley Packet Filter)是 Linux 内核中一个安全可编程的虚拟机框架,允许在不修改内核源码、不重新编译内核的前提下,将自定义逻辑注入内核执行。它从最初的包过滤演进为覆盖网络、可观测性、安全控制的通用内核扩展平台,是当前最活跃的 Linux 内核子系统之一。

Linux 系列进程与线程 · 内存管理 · 文件系统 · 网络与 IO 模型 · 性能分析工具

目录

章节说明
eBPF 是什么发展历程与核心定位
工作原理虚拟机、验证器、JIT 编译
程序类型kprobe/tracepoint/XDP/socket 等
BPF Map内核态与用户态共享数据
工具链对比BCC vs libbpf vs bpftrace
实战:内核跟踪kprobe/tracepoint 追踪系统调用
实战:用户态跟踪uprobe/USDT 追踪应用函数
实战:网络可观测性追踪网络丢包与连接延迟
实战:XDP 高性能包过滤超越 iptables 的包处理
CO-RE 原理一次编译到处运行
局限性eBPF 不是万能的

eBPF 是什么

发展历程

1992 年  BPF(BSD Packet Filter)诞生,用于网络包过滤,比当时最快方案快 20 倍
         核心设计:内核态虚拟机 + 用户态字节码,避免每包复制到用户空间

2014 年  eBPF 诞生(Alexei Starovoitov)
         扩展寄存器数量(11 个 64 位寄存器)
         引入 BPF Map(内核/用户态共享存储)
         从网络包过滤扩展到内核函数、用户函数、跟踪点、性能事件、安全控制

2016 年+ BCC、bpftrace 工具链成熟,大幅降低 eBPF 开发门槛
         Cilium(K8s 网络)、Katran(负载均衡)、Falco(安全)等开源项目涌现

2021 年  eBPF 基金会成立;微软发布 eBPF for Windows

核心价值

eBPF 开启了一种新的内核扩展范式:无需修改内核源码,无需重新编译,无需加载内核模块,即可在内核中安全运行自定义逻辑

工作原理

执行流程

开发者写 C 代码(eBPF 程序)
    ↓
LLVM/Clang 编译为 BPF 字节码
    ↓
bpf() 系统调用提交到内核
    ↓
验证器(Verifier)安全检查
    ├── 构建有向无环图,确保无不可达指令
    ├── 模拟执行,确保无无效指令
    ├── 不含无限循环
    └── 必须在有限时间内完成
    ↓
JIT 编译器将字节码编译为本地机器指令
    ↓
绑定到事件(kprobe/tracepoint/XDP 等)
    ↓
事件触发时自动执行 eBPF 程序

内核运行时组件

组件职责
eBPF 辅助函数提供与内核交互的 API(不同程序类型可用的函数集不同)
验证器安全检查,拒绝不安全的程序
寄存器 + 栈11 个 64 位寄存器(R0~R10),512 字节栈空间
JIT 编译器将字节码编译为本地机器码,提升执行效率
BPF Map大块持久化存储,用于内核态/用户态数据交换

安全保障

  • 只有特权进程(CAP_BPF 或 root)才能加载 eBPF 程序
  • 验证器拒绝包含无限循环、越界访问、空指针解引用的程序
  • eBPF 程序栈空间最多 512 字节,超出必须用 Map
  • 内核 5.2 前最多 4096 条指令,5.2 后提升到 100 万条

程序类型

Linux 内核 v5.13 支持 30 种程序类型,按场景分为三大类:

跟踪类(Tracing)

类型触发时机典型用途
BPF_PROG_TYPE_KPROBE内核函数入口/返回追踪任意内核函数调用
BPF_PROG_TYPE_TRACEPOINT内核预定义静态跟踪点稳定 API,推荐优先使用
BPF_PROG_TYPE_PERF_EVENT性能计数器事件(PMU)CPU 周期、Cache Miss 等
BPF_PROG_TYPE_RAW_TRACEPOINT原始跟踪点(参数未处理)性能更高

kprobe vs tracepoint:kprobe 挂载到任意内核函数,灵活但不稳定(内核升级可能函数改名);tracepoint 是内核显式定义的稳定接口,推荐优先使用。

网络类(Networking)

类型触发位置典型用途
BPF_PROG_TYPE_XDP网卡驱动收包最早期(协议栈之前)DDoS 防御、高性能包过滤、4 层负载均衡
BPF_PROG_TYPE_SCHED_CLS/ACTTC 流量控制(协议栈内)流量整形、包改写、重定向
BPF_PROG_TYPE_SOCKET_FILTER套接字收包包过滤(tcpdump 底层就是这个)
BPF_PROG_TYPE_SOCK_OPSTCP 连接状态变化调整 TCP 参数、连接追踪
BPF_PROG_TYPE_CGROUP_SKBcgroup 网络包容器网络控制

XDP 返回码

返回码含义
XDP_DROP丢弃包(最快,直接在驱动层丢弃)
XDP_PASS传递给内核协议栈正常处理
XDP_TX从同一网卡发回(用于反射攻击防御)
XDP_REDIRECT重定向到其他网卡或 CPU
XDP_ABORTED程序错误,丢弃并记录

BPF Map

BPF Map 是 eBPF 程序与用户态程序之间的共享数据结构,也是 eBPF 程序在多次调用之间保存状态的唯一方式。

常用 Map 类型

类型数据结构典型用途
BPF_MAP_TYPE_HASH哈希表(key-value)统计每个进程/IP 的计数
BPF_MAP_TYPE_ARRAY定长数组(index-value)全局计数器、配置项
BPF_MAP_TYPE_PERF_EVENT_ARRAY环形缓冲区将事件流式传输到用户态
BPF_MAP_TYPE_RINGBUF无锁环形缓冲区(推荐)高性能事件上报(内核 5.8+)
BPF_MAP_TYPE_LRU_HASHLRU 淘汰哈希表连接追踪(自动淘汰旧条目)
BPF_MAP_TYPE_PROG_ARRAYeBPF 程序数组尾调用(tail call),绕过指令数限制
BPF_MAP_TYPE_STACK_TRACE调用栈存储火焰图生成

用户态操作 Map

# 查看系统中所有 BPF Map
bpftool map list

# 查看特定 Map 的内容
bpftool map dump id <map_id>

# 更新 Map 条目
bpftool map update id <map_id> key 0x01 0x00 0x00 0x00 value 0x0a 0x00 0x00 0x00

工具链对比

工具适用场景优点缺点
bpftrace快速排查、单行脚本语法简洁,无需编译,即写即用功能有限,不适合复杂程序
BCC开发复杂 eBPF 程序内置丰富工具,Python/C++ 接口友好依赖 LLVM + 内核头文件,每次运行时编译
libbpf生产环境分发编译一次即可分发,不需要目标机安装 LLVM需内核开启 BTF,开发复杂度高
内核源码 samples/bpf学习内核 eBPF 原理最接近底层不适合生产使用

选择建议

  • 临时排查问题 → bpftrace
  • 开发工具并在内部使用 → BCC
  • 开发需要分发到生产环境的工具 → libbpf + CO-RE
# 安装 bpftrace(Ubuntu 19.04+)
sudo apt-get install -y bpftrace

# 安装 BCC
sudo apt-get install -y bpfcc-tools linux-headers-$(uname -r)

# 查看当前内核支持的 eBPF 特性
bpftool feature probe | grep program_type

实战:内核跟踪

查询跟踪点

# 查询所有内核插桩和跟踪点
sudo bpftrace -l

# 查询所有系统调用跟踪点
sudo bpftrace -l 'tracepoint:syscalls:*'

# 查询包含 execve 的跟踪点
sudo bpftrace -l '*execve*'

# 查询跟踪点的参数格式
sudo bpftrace -lv tracepoint:syscalls:sys_enter_execve
# 输出:
#   int __syscall_nr
#   const char * filename
#   const char *const * argv
#   const char *const * envp

用 bpftrace 追踪短时进程

# 追踪所有 execve 调用(短时进程排查利器)
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_execve,
tracepoint:syscalls:sys_enter_execveat {
    printf("%-6d %-8s ", pid, comm);
    join(args->argv);
}'

用 bpftrace 追踪 syscall 延迟

# 追踪 read 系统调用的延迟分布(单位:纳秒)
sudo 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]);
}
interval:s:5 { print(@latency); clear(@latency); }'

用 bpftrace 追踪内核函数调用

# 追踪内核 kfree_skb(网络丢包)的调用栈
sudo bpftrace -e '
kprobe:kfree_skb /comm=="curl"/ {
    printf("kstack: %s\n", kstack);
}'

# 追踪 do_sys_openat2 调用(哪些进程在打开文件)
sudo bpftrace -e '
kprobe:do_sys_openat2 {
    printf("%-6d %-16s %s\n", pid, comm, str(arg1));
}'

BCC 工具直接使用

# 追踪所有 execve(短时进程)
execsnoop-bpfcc

# 追踪 TCP 连接建立
tcpconnect-bpfcc

# 追踪 open() 系统调用
opensnoop-bpfcc

# 追踪 I/O 延迟
biolatency-bpfcc

# 追踪 CPU 热点函数(On-CPU 分析)
profile-bpfcc -F 99 30

实战:用户态跟踪

uprobe 将 eBPF 程序插桩到用户程序的任意函数,不需要修改应用代码。

追踪编译型语言(C/Go)

# 追踪 Bash 中执行的命令(通过 uretprobe 获取 readline 返回值)
sudo bpftrace -e '
uretprobe:/usr/bin/bash:readline {
    printf("User %d executed \"%s\" command\n", uid, str(retval));
}'

# 追踪 Go 程序的函数调用(需要 -g 编译选项保留符号)
# 查询 Go 二进制的符号表
readelf -Ws /path/to/go-binary | grep -i "your_func"

# 用 bpftrace 挂载 uprobe
sudo bpftrace -e '
uprobe:/path/to/go-binary:main.yourFunction {
    printf("called with arg0=%d\n", arg0);
}'

追踪 Python(解释型语言,通过 USDT)

# 查询 Python3 的 USDT 跟踪点
bpftrace -l 'usdt:/usr/bin/python3:*'
# 输出:
#   usdt:/usr/bin/python3:python:function__entry
#   usdt:/usr/bin/python3:python:function__return
#   ...

# 追踪 Python 函数调用(文件名:行号 函数名)
sudo bpftrace -e '
usdt:/usr/bin/python3:function__entry {
    printf("%s:%d %s\n", str(arg0), arg2, str(arg1));
}'

追踪 Java(JIT 编译型语言)

# Java 需要开启 USDT(--enable-dtrace 编译选项)
# 或使用 async-profiler 绕过 JIT 限制

# 用 async-profiler 生成火焰图(推荐)
./profiler.sh -d 30 -f flamegraph.html <java_pid>

# 追踪 JVM 的 GC 事件(通过 USDT)
sudo bpftrace -e '
usdt:/usr/lib/jvm/java-11-openjdk-amd64/lib/server/libjvm.so:hotspot:gc__begin {
    printf("GC started at %llu\n", nsecs);
}'

实战:网络可观测性

追踪 TCP 连接

# 实时追踪新建 TCP 连接(源/目的 IP 和端口)
sudo bpftrace -e '
kprobe:tcp_connect {
    $sk = (struct sock *)arg0;
    printf("%-16s:%-5d → %-16s:%-5d\n",
        ntop(2, $sk->__sk_common.skc_rcv_saddr),
        $sk->__sk_common.skc_num,
        ntop(2, $sk->__sk_common.skc_daddr),
        $sk->__sk_common.skc_dport >> 8);
}'

# 或直接用 BCC 工具
tcpconnect-bpfcc     # 追踪 connect() 调用
tcpaccept-bpfcc      # 追踪 accept() 调用
tcpretrans-bpfcc     # 追踪 TCP 重传

追踪网络丢包(调用栈分析)

# 保存为 dropwatch.bt 文件后执行:sudo bpftrace dropwatch.bt
# 追踪 kfree_skb 的内核调用栈,定位丢包原因

# 简化版单行命令(过滤 curl 进程的 TCP 丢包)
sudo bpftrace -e '
#include <linux/skbuff.h>
#include <linux/ip.h>
kprobe:kfree_skb /comm=="curl"/ {
    $skb = (struct sk_buff *)arg0;
    $iph = (struct iphdr *)($skb->head + $skb->network_header);
    if ($iph->protocol == 6) {   // 6 = TCP
        printf("DROP: %s→%s\n%s\n",
            ntop(AF_INET, $iph->saddr),
            ntop(AF_INET, $iph->daddr),
            kstack);
    }
}'

追踪 TCP 延迟分布

# 追踪 TCP 接收延迟(从数据到达到 read() 返回)
sudo bpftrace -e '
kprobe:tcp_rcv_established { @start[tid] = nsecs; }
kretprobe:tcp_rcv_established /@start[tid]/ {
    @tcp_latency_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}
interval:s:10 { print(@tcp_latency_us); }'

实战:XDP 高性能包过滤

XDP 在网卡驱动层处理数据包,无需经过内核协议栈,性能远超 iptables。

XDP vs iptables 性能对比

方案处理位置典型吞吐适用场景
iptables内核协议栈 Netfilter~1 Mpps通用防火墙
nftables内核协议栈 Netfilter~2 Mpps通用防火墙
XDP(原生模式)网卡驱动层~10-25 MppsDDoS 防御、高性能过滤
XDP(卸载模式)网卡固件>100 Mpps超高性能场景

简单 XDP 包过滤示例

// xdp_drop_icmp.c — 丢弃所有 ICMP 包
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <bpf/bpf_helpers.h>

SEC("xdp")
int xdp_drop_icmp(struct xdp_md *ctx) {
    void *data_end = (void *)(long)ctx->data_end;
    void *data     = (void *)(long)ctx->data;

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    if (eth->h_proto != __constant_htons(ETH_P_IP))
        return XDP_PASS;

    struct iphdr *iph = (void *)(eth + 1);
    if ((void *)(iph + 1) > data_end)
        return XDP_PASS;

    // 丢弃 ICMP(协议号 1)
    if (iph->protocol == 1)
        return XDP_DROP;

    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";
# 编译并加载 XDP 程序(通用模式,无需网卡驱动支持)
clang -O2 -target bpf -c xdp_drop_icmp.c -o xdp_drop_icmp.o
sudo ip link set dev eth0 xdpgeneric object xdp_drop_icmp.o sec xdp

# 卸载 XDP 程序
sudo ip link set dev eth0 xdpgeneric off

# 用 BCC 加载 XDP(更简单)
from bcc import BPF
b = BPF(src_file="xdp_drop_icmp.c")
fn = b.load_func("xdp_drop_icmp", BPF.XDP)
b.attach_xdp("eth0", fn, 0)

XDP 运行模式

模式要求性能说明
通用模式(xdpgeneric)无特殊要求最低在协议栈中模拟,用于测试
原生模式(xdpdrv)网卡驱动支持在驱动早期路径运行
卸载模式(xdpoffload)网卡固件支持最高直接在网卡上运行

CO-RE 原理

CO-RE(Compile Once - Run Everywhere,一次编译到处运行) 解决了 eBPF 程序的可移植性问题。

问题背景

eBPF 程序常需要访问内核数据结构(如 task_struct),但不同内核版本的结构体字段偏移不同。传统方式(BCC)在每台机器运行时动态编译,需要安装 LLVM 和内核头文件。

CO-RE 解决方案

内核编译时生成 BTF(BPF Type Format)信息
  ↓
eBPF 程序使用 BTF 描述所需的类型信息
  ↓
libbpf 在加载时根据目标内核的 BTF 重定位字段偏移
  ↓
同一个编译好的 eBPF 二进制可在不同内核版本上运行
方案每机器需要 LLVM需要内核头文件可移植性
BCC 动态编译差(每机器编译)
libbpf + CO-RE好(一次编译)
# 检查内核是否支持 BTF(CO-RE 前提)
ls /sys/kernel/btf/vmlinux

# 查看 BTF 信息
bpftool btf dump file /sys/kernel/btf/vmlinux format raw | head -20

# CO-RE 程序使用 vmlinux.h(包含所有内核类型定义)
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

局限性

  • 内核版本要求:稳定运行 eBPF 需要 Linux 4.9+,建议使用 5.x 内核体验完整特性
  • 栈空间限制:eBPF 程序栈最多 512 字节,复杂逻辑需借助 Map 存储
  • 指令数限制:5.2 前最多 4096 条,5.2+ 提升到 100 万条
  • 不能调用任意内核函数:只能调用 BPF 辅助函数白名单中的函数
  • uprobe 性能开销:高频用户态函数(如内存分配)使用 uprobe 会带来显著开销,应避免
  • BTF 依赖:CO-RE 需要内核开启 BTF,较老的发行版默认不开启
  • 调试困难:eBPF 程序在内核中运行,调试手段有限(主要靠 bpf_trace_printk 和 Map 输出)

参考资料

  • 《eBPF 核心技术与实战》— 倪朋飞,极客时间
  • Brendan Gregg — BPF Performance Tools(brendangregg.com/bpf-performance-tools-book.html)
  • eBPF 官方文档(ebpf.io)
  • BCC 参考手册(github.com/iovisor/bcc/blob/master/docs/reference_guide.md)
  • bpftrace 参考手册(github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md)
← 返回列表

评论 (0)

暂无评论,来留下第一条吧。
登录注册 后才能发表评论