WEBKT

告别盲人摸象:如何用 eBPF 洞察 Linux 内核运行时黑盒?

156 0 0 0

作为一名 Linux 系统工程师,你是否也曾遇到过这样的困境?线上服务 CPU 占用率居高不下,却苦于无法定位到具体是哪个函数在作祟?亦或是,网络延迟突增,却难以追踪到是哪个 socket 连接出现了问题?传统的性能分析工具,如 topperf 等,虽然能提供一些宏观的信息,但往往无法深入到内核内部,让你如同盲人摸象,难以找到问题的根源。别担心,eBPF (extended Berkeley Packet Filter) 的出现,为我们打开了一扇通往 Linux 内核深处的大门。它允许你在内核运行时动态地插入自定义的代码,无需修改内核源码,即可观测内核的行为,进行性能分析和故障排查。

什么是 eBPF?

简单来说,eBPF 就像一个内核“探针”,你可以在内核的关键位置(如函数入口、系统调用等)安装这个探针,当内核执行到这些位置时,探针就会被触发,收集你感兴趣的数据,然后将数据传递到用户空间进行分析。与传统的内核模块相比,eBPF 具有以下优势:

  • 安全:eBPF 程序在加载到内核之前,会经过严格的验证器的检查,确保程序的安全性,防止程序崩溃或恶意修改内核。
  • 高效:eBPF 程序运行在内核空间,避免了用户空间和内核空间之间频繁的切换,提高了性能。
  • 灵活:你可以使用 C 等高级语言编写 eBPF 程序,然后使用 LLVM 等工具将其编译成 eBPF 字节码,加载到内核中运行。
  • 无需重启:eBPF 程序可以在内核运行时动态加载和卸载,无需重启系统。

eBPF 的应用场景

eBPF 的应用场景非常广泛,几乎涉及到 Linux 内核的各个方面。以下是一些常见的应用场景:

  • 性能分析:使用 eBPF 可以跟踪函数的执行时间、系统调用的次数、网络包的延迟等,帮助你找出性能瓶颈。
  • 安全监控:使用 eBPF 可以监控进程的创建、文件的访问、网络连接的建立等,及时发现安全威胁。
  • 网络优化:使用 eBPF 可以实现流量控制、负载均衡、DDoS 防护等,提高网络性能。
  • 故障排查:使用 eBPF 可以收集内核的运行时信息,帮助你定位和解决各种故障。

如何使用 eBPF 监控 Linux 内核运行时行为?

接下来,我将以一个简单的例子,向你展示如何使用 eBPF 监控 Linux 内核的运行时行为。我们将使用 bcc (BPF Compiler Collection) 工具包,它提供了一组 Python 封装,简化了 eBPF 程序的开发过程。我们的目标是:跟踪 sys_enter_openat 系统调用的执行情况,记录每个进程打开的文件名。

1. 安装 bcc

首先,你需要安装 bcc 工具包。具体的安装步骤可以参考 bcc 的官方文档:https://github.com/iovisor/bcc。不同的 Linux 发行版安装方式可能略有不同,请根据你的系统选择合适的安装方式。

以 Ubuntu 为例,你可以使用以下命令安装 bcc:

sudo apt-get update
sudo apt-get install -y bpfcc-tools linux-headers-$(uname -r)

2. 编写 eBPF 程序

创建一个名为 opensnoop.py 的文件,并输入以下代码:

#!/usr/bin/env python

from bcc import BPF

# 定义 eBPF 程序
program = '''
#include <linux/sched.h>

// 定义事件结构体
struct event_t {
    u32 pid;
    char filename[128];
};

// 定义 BPF 映射
BPF_PERF_OUTPUT(events);

// kprobe 探测点
int kprobe__sys_enter_openat(struct pt_regs *ctx, int dirfd, const char *pathname, int flags)
{
    // 获取当前进程的 PID
    u32 pid = bpf_get_current_pid_tgid();

    // 创建事件结构体实例
    struct event_t event = {};
    event.pid = pid;

    // 从内核空间拷贝文件名到事件结构体
    bpf_probe_read_user_str(event.filename, sizeof(event.filename), (void *)pathname);

    // 将事件发送到用户空间
    events.perf_submit(ctx, &event, sizeof(event));

    return 0;
}
'''

# 初始化 BPF 对象
bpf = BPF(text=program)

# 打印事件
def print_event(cpu, data, size):
    event = bpf['events'].event(data)
    print(f'PID: {event.pid}  Filename: {event.filename.decode()}')

# 绑定 perf_buffer
bpf['events'].open_perf_buffer(print_event)

# 循环读取 perf_buffer
while True:
    try:
        bpf.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()

这段代码主要做了以下几件事:

  • 定义 eBPF 程序:使用 C 语言编写 eBPF 程序,嵌入到 Python 代码中。程序定义了一个 event_t 结构体,用于存储进程 PID 和文件名。程序还定义了一个 BPF_PERF_OUTPUT 映射,用于将事件发送到用户空间。kprobe__sys_enter_openat 函数是一个 kprobe 探测点,它会在 sys_enter_openat 系统调用入口处被触发。在这个函数中,我们获取当前进程的 PID,并将文件名从内核空间拷贝到 event_t 结构体中,然后将事件发送到用户空间。
  • 初始化 BPF 对象:使用 BPF 类初始化 eBPF 对象,将 eBPF 程序加载到内核中。
  • 打印事件:定义 print_event 函数,用于打印从内核空间接收到的事件。这个函数会将 event_t 结构体中的数据解析出来,并打印到控制台上。
  • 绑定 perf_buffer:使用 open_perf_buffer 函数将 print_event 函数绑定到 events 映射上。这样,当内核空间有事件发送到 events 映射时,print_event 函数就会被调用。
  • 循环读取 perf_buffer:使用 perf_buffer_poll 函数循环读取 events 映射中的事件。当用户按下 Ctrl+C 时,程序退出。

3. 运行 eBPF 程序

使用以下命令运行 opensnoop.py

sudo python opensnoop.py

运行后,你可以看到类似以下的输出:

PID: 1234  Filename: /etc/passwd
PID: 5678  Filename: /var/log/syslog
PID: 1234  Filename: /etc/shadow
...

这表示进程 1234 打开了 /etc/passwd 文件,进程 5678 打开了 /var/log/syslog 文件,等等。通过这些信息,你可以了解到哪些进程在访问哪些文件,从而进行性能分析和安全监控。

4. 分析结果

这个简单的例子只是 eBPF 的冰山一角。你可以根据自己的需求,修改 eBPF 程序,收集更多更详细的信息。例如,你可以:

  • 跟踪函数的执行时间:使用 kprobekretprobe 探测点,分别在函数入口和出口处记录时间戳,计算函数的执行时间。
  • 监控网络连接:使用 socket 探测点,监控网络连接的建立、关闭、数据发送和接收等事件。
  • 分析系统调用:使用 tracepoint 探测点,监控系统调用的参数和返回值。

eBPF 的未来

eBPF 正在成为 Linux 内核的重要组成部分。越来越多的工具和框架都开始使用 eBPF,例如:

  • bpftrace:一种高级的 eBPF 跟踪语言,可以让你使用类似于 awk 的语法编写 eBPF 程序。
  • Falco:一种云原生运行时安全工具,使用 eBPF 监控容器和主机的行为。
  • Cilium:一种基于 eBPF 的网络和安全解决方案,可以提供高性能的网络策略和安全策略。

随着 eBPF 的不断发展,它将在 Linux 内核的性能分析、安全监控、网络优化和故障排查等领域发挥越来越重要的作用。掌握 eBPF 技术,将成为 Linux 系统工程师的一项必备技能。

总结

eBPF 是一项强大的技术,它可以让你深入到 Linux 内核的运行时,观测内核的行为,进行性能分析和故障排查。通过学习和掌握 eBPF,你可以更好地理解 Linux 内核,提高你的系统管理和开发能力。希望这篇文章能够帮助你入门 eBPF,开启你的内核探索之旅!

内核探险家 eBPFLinux内核性能分析

评论点评