专栏文章
专栏文章
Linux 专栏
1. Linux 专栏 #01:进程与线程 2. Linux 专栏 #02:内存管理 3. Linux 专栏 #03:文件系统 4. Linux 专栏 #04:网络与 IO 模型 5. Linux 专栏 #05:中断机制 6. Linux 专栏 #05:性能分析工具 7. Linux 专栏 #06:eBPF 技术实战

Linux 专栏 #06:eBPF 技术实战

发布于 2026-06-08 07:30 👁 15 次阅读
#性能#操作系统#linux#ebpf

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 大块持久化存储,用于内核态/用户态数据交换

安全保障


程序类型

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/ACT TC 流量控制(协议栈内) 流量整形、包改写、重定向
BPF_PROG_TYPE_SOCKET_FILTER 套接字收包 包过滤(tcpdump 底层就是这个)
BPF_PROG_TYPE_SOCK_OPS TCP 连接状态变化 调整 TCP 参数、连接追踪
BPF_PROG_TYPE_CGROUP_SKB cgroup 网络包 容器网络控制

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_HASH LRU 淘汰哈希表 连接追踪(自动淘汰旧条目)
BPF_MAP_TYPE_PROG_ARRAY eBPF 程序数组 尾调用(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(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 Mpps DDoS 防御、高性能过滤
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 核心技术与实战》— 倪朋飞,极客时间
  • 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)

暂无评论,来留下第一条吧。

发表评论