首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >eBPF入门与实践:深入解析Linux系统的可观测引擎

eBPF入门与实践:深入解析Linux系统的可观测引擎

作者头像
宅蓝三木
发布2025-08-18 09:06:29
发布2025-08-18 09:06:29
35400
代码可运行
举报
文章被收录于专栏:三木的博客三木的博客
运行总次数:0
代码可运行

在Linux系统中,内核是“操作系统的核心”,负责管理硬件资源、进程调度、网络交互等核心功能。但长期以来,内核的可编程性一直是个难题——如果想自定义内核行为(比如监控进程、过滤网络包),传统方式要么依赖内核模块(风险高、兼容性差,内核模块加载后拥有与内核相同的权限,错误的代码可能导致系统崩溃或安全漏洞。),要么只能修改内核源码重新编译(成本极高)。

eBPF(extended Berkeley Packet Filter)的出现彻底改变了这一局面。它是一种运行在Linux内核中的动态追踪与可编程技术,允许用户在不修改内核源码、不重启系统的情况下,安全地向内核注入自定义逻辑,实现对系统行为的细粒度观测、控制与优化。

如今,eBPF已成为云原生、性能分析、网络安全等领域的“基础设施”,被Google、Facebook、Netflix等企业广泛用于生产环境,是理解和优化Linux系统的“瑞士军刀”。

eBPF是什么?

eBPF起源于Linux网络子系统(最初用于数据包过滤),经过多年发展,已成为一套通用的内核可编程框架。简单来说,eBPF是一种“运行在内核中的安全沙箱”,允许用户编写小型程序并加载到内核中执行。

其核心特点包括:

  • 可观测性:能直接访问内核数据结构(如进程信息、网络包),实现细粒度监控;
  • 轻量性:程序体积小,经过即时编译(JIT)后,执行效率接近原生内核代码;
  • 通用性:支持网络、进程、文件系统等多场景,而非局限于单一功能;
  • 安全性:通过内核验证机制确保程序不会崩溃系统。

eBPF与内核模块的对比

传统内核定制方案(如内核模块)存在诸多问题,而eBPF恰好弥补了这些缺陷:

对比维度

内核模块

eBPF

安全性

可能直接崩溃内核(无验证)

必须通过内核安全验证器检查

性能

加载耗时,但执行效率高

JIT编译后执行效率接近原生内核代码

可移植性

依赖特定内核版本,兼容性差

助CO-RE (Compile Once - Run Everywhere) 技术(如libbpf库),可实现跨内核版本兼容

灵活性

修改或加载新版本通常需要卸载旧模块再加载新模块(可能影响服务)

动态加载/卸载,实时生效

简单来说,eBPF既保留了内核级别的控制力,又避免了传统方案的风险与复杂度。

eBPF的主要应用场景

eBPF的灵活性使其能覆盖多种核心场景:

  • 网络流量分析与控制:可过滤、修改网络包(如实现自定义防火墙、流量调度),比传统iptables更灵活高效;
  • 内核行为监控与安全防护:检测异常内核模块加载、系统调用滥用(如恶意进程创建文件),用于入侵检测;
  • 系统性能优化:追踪进程调度延迟、I/O耗时、函数调用开销等,定位性能瓶颈(如通过监控磁盘I/O延迟优化存储策略);
  • 动态追踪:实时获取进程、线程的运行状态(如函数调用栈、内存分配),用于调试复杂系统问题。

eBPF的使用流程

使用eBPF实现自定义功能的流程可概括为“编写-加载-执行-交互”五步:

  1. 用户态编写eBPF程序:用C语言(或bpftrace等简化工具)编写核心逻辑,定义需要监控的内核事件(如系统调用、网络包);
  2. 加载到内核:通过bpf()系统调用将程序提交给内核;
  3. 安全验证:内核的“安全验证器”检查程序是否存在风险(如内存越界、死循环),只有通过验证的程序才能执行;
  4. 即时编译与执行:通过JIT编译器将eBPF指令转为原生机器码,绑定到目标事件(如某个内核函数被调用时触发);
  5. 与用户态交互:通过“map”(键值对存储)将内核中收集的数据(如监控指标)传递给用户态程序(如可视化工具)。

eBPF的核心组成部分

eBPF是一套完整的技术栈,核心组成部分包括:

libbpf

内核官方提供的辅助库,简化eBPF程序的编译、加载与管理(如自动处理内核版本兼容、map初始化),降低开发门槛。

bpf()系统调用

用户态与内核eBPF系统交互的接口,支持加载程序、创建map、查询程序状态等操作。

安全验证器

eBPF的“安全门卫”,负责静态分析程序的所有可能执行路径,检查是否存在内存越界、死循环、类型错误等问题。只有通过验证的程序才能加载到内核。

安全验证器是eBPF的核心安全保障,其工作原理可概括为“全路径静态分析”:

  • 上下文隔离:eBPF程序运行在独立上下文(如特定内核函数调用时),只能访问预定义的资源(如事件参数);
  • 执行路径检查:验证器会模拟程序的所有可能执行路径,确保不存在死循环(程序必须能终止);
  • 内存安全:严格检查内存访问范围,禁止越界读写(如只能访问map或上下文指定的内存区域);
  • 类型安全:确保变量类型匹配(如不能将指针当作整数使用),避免类型混淆漏洞;
  • 无副作用:禁止执行可能破坏内核状态的操作(如修改关键内核数据结构)。

通过这些检查,eBPF程序即使存在bug,也只会影响自身执行(如被终止),不会导致内核崩溃或系统不稳定。

eBPF存储(map)

用户态与内核态交互的“桥梁”,以键值对形式存储数据(支持数组、哈希表等类型)。eBPF程序可在内核中读写map,用户态程序通过文件描述符(fd)访问map,实现数据互通。

map是eBPF程序与用户态交互的核心机制,类似“共享内存”,但更安全、可控。

1. map的基本特性
  • 键值对存储:支持数组、哈希表、链表等多种类型,类似字典;
  • 双向访问:eBPF程序(内核态)和用户态程序均可读写;
  • 生命周期管理:可独立于eBPF程序存在,支持持久化存储。
2. 查看map内容

可通过bpftool查看map的内容:

代码语言:javascript
代码运行次数:0
运行
复制
# 查看map内容(id从程序指令中获取)
sudo bpftool map dump id 8

输出示例:

代码语言:javascript
代码运行次数:0
运行
复制
[{
    "value": {
        ".rodata": [{
                "hello_world.____fmt": "Hello world"
            }
        ]
    }
}]
3. 定义自定义map

下面是一个定义哈希表map的示例,用于统计进程打开文件的次数:

代码语言:javascript
代码运行次数:0
运行
复制
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

// 定义哈希表map:key为进程PID(int),value为打开次数(int)
struct {
    __uint(type, BPF_MAP_TYPE_HASH);  // 类型:哈希表
    __type(key, int);                 // key类型:PID(进程ID)
    __type(value, int);               // value类型:计数
    __uint(max_entries, 1024);        // 最大条目数:1024
} open_count_map SEC(".maps");  // 声明在.maps段,供加载器识别

// 挂载到openat2系统调用的内核实现
SEC("kprobe/do_sys_openat2")
int count_open(void *ctx) {
    int pid = bpf_get_current_pid_tgid() >> 32;  // 获取当前进程PID
    int *count;

    // 从map中查找当前PID的计数
    count = bpf_map_lookup_elem(&open_count_map, &pid);
    if (count) {
        // 若存在,计数+1
        (*count)++;
    } else {
        // 若不存在,初始化计数为1
        int init = 1;
        bpf_map_update_elem(&open_count_map, &pid, &init, BPF_ANY);
    }
    return 0;
}

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

代码说明:

  • struct { ... } open_count_map SEC(".maps"):定义map并声明在.maps段(eBPF加载器会自动识别并创建);
  • bpf_map_lookup_elem:查询map中key对应的value;
  • bpf_map_update_elem:向map中添加或更新键值对。
4. map的存储与加载

map的定义被存储在ELF目标文件的.maps段中,eBPF加载器(如libbpf)会:

  1. 解析.maps段,创建对应的内核map;
  2. 分配唯一的map ID和文件描述符(fd);
  3. 将程序中对map的引用替换为实际的fd或ID,确保内核中能正确访问。

eBPF指令集

一套精简的指令集(含150+条指令),专为内核环境设计。指令格式简单(64位),支持常见操作(加减、跳转、内存访问),且可被高效编译为原生机器码。

helper函数与kfunc

内核提供的“工具函数”:

  • helper函数:eBPF程序可直接调用的基础功能(如bpf_printk打印日志、bpf_map_lookup_elem操作map);
  • kfunc:允许eBPF程序调用内核函数(如获取进程信息),基于白名单机制确保安全。

eBPF实践:从“Hello World”开始

编写第一个eBPF程序

下面是一个简单的eBPF程序,功能是在进程执行openat2系统调用(打开文件)时,打印“Hello world”:

代码语言:javascript
代码运行次数:0
运行
复制
#include "vmlinux.h"       // 内核数据结构定义
#include <bpf/bpf_helpers.h>  // eBPF helper函数

// 定义探针:挂载到内核函数do_sys_openat2(openat2系统调用的内核实现)的入口
SEC("kprobe/do_sys_openat2")
int hello_world(void *ctx) {
    // 调用helper函数打印日志(类似printf,输出到内核日志)
    bpf_printk("Hello world");
    return 0;  // 程序结束
}

// 声明许可证(使用GPL helper函数需声明为GPL)
char LICENSE[] SEC("license") = "GPL";

代码说明:

  • SEC("kprobe/do_sys_openat2"):指定程序类型为kprobe(内核函数探针),挂载到do_sys_openat2函数(当该函数被调用时触发程序执行);
  • bpf_printk:内核提供的日志打印函数,输出内容可通过dmesgcat /sys/kernel/debug/tracing/trace_pipe查看;
  • LICENSE:eBPF程序必须声明许可证(如GPL),否则无法使用部分内核功能。

生成vmlinux.h:

代码语言:javascript
代码运行次数:0
运行
复制
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

eBPF程序需编译为eBPF指令格式(而非普通机器码),使用clang工具链:

代码语言:javascript
代码运行次数:0
运行
复制
# -target bpf:指定编译目标为eBPF
# -c:只编译不链接
# ebpf-demo.c:源码文件
# -o ebpf-demo.o:输出目标文件
clang -O2 -g -Wall -target bpf -c ebpf-demo.c -o ebpf-demo.o

更新libbpf:

代码语言:javascript
代码运行次数:0
运行
复制
# 安装依赖
sudo apt update
sudo apt install build-essential git libelf-dev zlib1g-dev libcap-dev binutils-dev

# 编译最新 bpftool
git clone --recurse-submodules https://github.com/libbpf/bpftool.git
cd bpftool/src
make -j$(nproc)
sudo make install

编译后,可通过bpftool(eBPF管理工具)加载并查看程序:

代码语言:javascript
代码运行次数:0
运行
复制
# 加载程序(需root权限,实际开发中通常通过libbpf自动加载)
sudo bpftool prog load ebpf-demo.o /sys/fs/bpf/ebpf_demo type kprobe autoattach

# 查看已加载的eBPF程序
sudo bpftool prog list | grep hello_world
# 输出示例:70: kprobe  name hello_world  tag bf163b23cd3b174d  gpl

# 查看编译后的eBPF指令(翻译后的伪代码)
sudo bpftool prog dump xlated id 70

输出的eBPF指令解析:

代码语言:javascript
代码运行次数:0
运行
复制
int hello_world(void * ctx):
; bpf_printk("Hello world");
   0: (18) r1 = map[id:8][0]+0  ; 加载字符串"Hello world"到寄存器r1(从map中读取)
   2: (b7) r2 = 12              ; 寄存器r2赋值为字符串长度(12字节)
   3: (85) call bpf_trace_printk#-115696  ; 调用bpf_trace_printk(即bpf_printk)
; return 0;
   4: (b7) r0 = 0                ; 寄存器r0(返回值)赋值为0
   5: (95) exit                  ; 程序退出

指令说明:

  • r0-r10:eBPF寄存器,r0用于返回值,r1-r5用于传递函数参数;
  • (18):内存加载指令,从map中读取数据到寄存器;
  • (b7):立即数赋值指令,给寄存器设置固定值;
  • (85):函数调用指令,调用helper函数;
  • (95):程序退出指令。

当有进程执行openat2系统调用(如打开文件)时,程序会触发并打印日志:

代码语言:javascript
代码运行次数:0
运行
复制
# 查看内核跟踪日志
sudo cat /sys/kernel/debug/tracing/trace_pipe

可看到类似于如下的输出:

代码语言:javascript
代码运行次数:0
运行
复制
bash-8310    [005] ...21  4840.586914: bpf_trace_printk: Hello world

简化eBPF开发:bpftrace

对于快速验证需求,编写C语言eBPF程序仍有门槛。bpftrace是一种基于eBPF的高级跟踪语言,语法简洁,无需手动处理编译、加载细节。

  1. bpftrace的核心语法

结构:probe_definition { action }

  • 探针定义:指定触发条件(如内核函数调用、系统调用);
  • 动作:触发时执行的逻辑(如打印、计数)。
  1. bpftrace示例

示例1:监控进程打开的文件

代码语言:javascript
代码运行次数:0
运行
复制
# 跟踪openat系统调用的进入事件,打印进程名和打开的文件名
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s opened %s\n", comm, str(args->filename)); }'

输出示例:

代码语言:javascript
代码运行次数:0
运行
复制
feishu opened /proc/self/status
DetectThread opened /proc/stat
...

说明:

  • tracepoint:syscalls:sys_enter_openat:跟踪openat系统调用的进入事件(静态跟踪点,比kprobe更稳定);
  • comm:内置变量,当前进程名;
  • args->filename:系统调用参数(打开的文件名),str()用于将指针转为字符串。

示例2:监控磁盘I/O延迟

代码语言:javascript
代码运行次数:0
运行
复制
sudo bpftrace -e '
# 跟踪磁盘请求发出事件,记录开始时间(以设备+扇区为key)
tracepoint:block:block_rq_issue
{
    @start[args->dev, args->sector] = nsecs;  # nsecs:当前时间(纳秒)
}

# 跟踪磁盘请求完成事件,计算延迟
tracepoint:block:block_rq_complete
{
    $latency = (nsecs - @start[args->dev, args->sector]) / 1000;  # 转为微秒
    if ($latency > 1000) {  # 只关注延迟超过1ms的请求
        @us = hist($latency);  # 生成延迟直方图
        printf("高I/O延迟: %d us, 设备: %d, 扇区: %d\n", $latency, args->dev, args->sector);
        @[kstack] = count();  # 统计导致高延迟的内核调用栈
    }
    delete(@start[args->dev, args->sector]);  # 清理临时数据
}
'

说明:

  • @start:bpftrace中的map(默认哈希表),存储请求开始时间;
  • hist():内置函数,生成直方图(方便查看延迟分布);
  • kstack:内置变量,当前内核调用栈(定位延迟原因)。

输出示例:

代码语言:javascript
代码运行次数:0
运行
复制
Attaching 2 probes...
高I/O延迟: 1619 us, 设备: 271581184, 扇区: 1556119456
高I/O延迟: 1676 us, 设备: 271581184, 扇区: 1604919480
高I/O延迟: 1680 us, 设备: 271581184, 扇区: 1604919464
高I/O延迟: 1682 us, 设备: 271581184, 扇区: 1604919320
...
@[
    blk_mq_end_request_batch+854
    blk_mq_end_request_batch+854
    nvme_pci_complete_batch+187
    nvme_irq+115
    __handle_irq_event_percpu+76
    handle_irq_event+57
    handle_edge_irq+140
    __common_interrupt+78
    common_interrupt+159
    asm_common_interrupt+39
    cpuidle_enter_state+218
    cpuidle_enter+46
    call_cpuidle+35
    cpuidle_idle_call+285
    do_idle+135
    cpu_startup_entry+42
    start_secondary+297
    secondary_startup_64_no_verify+388
]: 9
@start[271581184, 0]: 5388896055523
@us: 
[1K, 2K)               9 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[2K, 4K)               0 |  
...

总结

eBPF通过“安全可编程内核”的理念,解决了传统内核定制方案的安全性、灵活性与效率难题,已成为Linux系统观测、优化与安全的核心技术。

从简单的“Hello World”到复杂的性能分析工具,eBPF的学习曲线虽有一定坡度,但借助libbpf、bpftrace等工具,入门门槛已大幅降低。如果你需要深入理解Linux系统行为、优化性能或构建安全工具,eBPF无疑是值得掌握的关键技术。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-08-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • eBPF是什么?
    • eBPF与内核模块的对比
    • eBPF的主要应用场景
    • eBPF的使用流程
  • eBPF的核心组成部分
    • libbpf
    • bpf()系统调用
    • 安全验证器
    • eBPF存储(map)
      • 1. map的基本特性
      • 2. 查看map内容
      • 3. 定义自定义map
      • 4. map的存储与加载
    • eBPF指令集
    • helper函数与kfunc
  • eBPF实践:从“Hello World”开始
    • 编写第一个eBPF程序
    • 简化eBPF开发:bpftrace
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档