WEBKT

内核开发者实战:如何用eBPF排查Linux内核问题?

37 0 0 0

作为一名内核开发者,你是否经常遇到这些头疼的问题?线上环境内核panic了,日志信息不足,难以定位问题;某个内核模块性能不佳,但苦于没有趁手的工具来分析瓶颈;想深入理解内核的某个机制,但阅读源码效率太低,希望能够动态地观测内核行为。别担心,eBPF(extended Berkeley Packet Filter)就是解决这些问题的利器。它允许你在内核中安全地运行自定义的代码,而无需修改内核源码或重启系统。本文将深入探讨如何利用eBPF来调试和优化Linux内核,助你成为内核问题排查高手。

1. eBPF简介:内核观测的瑞士军刀

eBPF最初设计用于网络数据包过滤,但如今已发展成为一个通用的内核事件监控和分析框架。它可以附加到各种内核事件上,例如函数调用、系统调用、网络事件等,并执行用户自定义的eBPF程序。这些程序运行在内核的沙箱环境中,保证了安全性和稳定性。简而言之,eBPF就像一个内核观测的瑞士军刀,可以帮助你深入了解内核的运行状态。

eBPF的核心优势:

  • 安全: eBPF程序运行在内核的沙箱环境中,经过严格的验证,防止恶意代码破坏内核。
  • 高效: eBPF程序可以被JIT(Just-In-Time)编译成机器码,执行效率非常高。
  • 灵活: eBPF程序可以使用多种编程语言编写,例如C、Go等,并可以使用BPF CO-RE(Compile Once – Run Everywhere)技术,实现一次编译,到处运行。
  • 无需重启: 可以在不重启系统的情况下,动态地加载和卸载eBPF程序。

2. eBPF工具链:武装你的内核调试工具箱

要使用eBPF,你需要一套完整的工具链。下面介绍几个常用的工具:

  • bcc (BPF Compiler Collection): bcc是一个Python库,它提供了一组用于编写、编译和加载eBPF程序的工具。bcc包含了许多有用的eBPF工具,例如tcpdumpopensnoopexecsnoop等,可以用于网络分析、文件访问监控、进程执行跟踪等。它使用LLVM将C代码编译成BPF字节码。

  • bpftrace: bpftrace是一种高级的eBPF跟踪语言,类似于awk或DTrace。它使用简单的语法,可以方便地编写eBPF程序,用于跟踪内核事件、分析性能瓶颈等。bpftrace底层也是使用LLVM将代码编译成BPF字节码。

  • libbpf: libbpf是一个C库,它提供了一组API,用于加载、管理和与eBPF程序交互。libbpf是bcc和bpftrace的底层依赖,也可以直接用于编写eBPF程序。

  • CO-RE (Compile Once – Run Everywhere): CO-RE是一种eBPF技术,它允许你编写一次eBPF程序,然后在不同的内核版本上运行,而无需重新编译。CO-RE通过在运行时动态地调整eBPF程序,以适应不同的内核结构。

3. eBPF实战:排查内核问题的案例分析

下面通过几个实际案例,演示如何使用eBPF来排查内核问题。

案例1:跟踪函数调用

假设你需要跟踪某个内核函数的调用情况,例如vfs_read函数。你可以使用bpftrace来编写一个简单的eBPF程序:

#!/usr/bin/env bpftrace

BEGIN {
  printf("Tracing vfs_read...\n");
}

kprobe:vfs_read
{
  @count[func] = count();
  printf("vfs_read called by %s\n", comm);
}

END {
  clear(@count);
}

这个程序使用kprobe附加到vfs_read函数上,每次vfs_read被调用时,都会打印出调用者的进程名。@count[func] = count(); 这行代码统计了vfs_read函数的调用次数。

运行结果示例:

Tracing vfs_read...
vfs_read called by cat
vfs_read called by cat
vfs_read called by cat
vfs_read called by cat
...

通过这个程序,你可以清楚地看到哪些进程调用了vfs_read函数,以及调用的频率。这对于分析文件I/O相关的性能问题非常有帮助。

案例2:分析内存分配

假设你需要分析内核的内存分配情况,例如跟踪kmalloc函数的调用。你可以使用bcc来编写一个eBPF程序:

from bcc import BPF

# 定义eBPF程序
program = """
#include <uapi/linux/ptrace.h>

struct key_t {
    u64 ip;
    u32 pid;
    char comm[TASK_COMM_LEN];
};

BPF_HASH(counts, struct key_t, u64);

int kprobe__kmalloc(struct pt_regs *ctx, size_t size) {
    struct key_t key = {};
    key.ip = PT_REGS_IP(ctx);
    key.pid = bpf_get_current_pid_tgid();
    bpf_get_current_comm(&key.comm, sizeof(key.comm));

    u64 *val = counts.lookup_or_init(&key, &size);
    if (val) {
        *val += size;
    }

    return 0;
}
"""

# 创建BPF对象
bpf = BPF(text=program)

# 加载eBPF程序
kmalloc_fn = bpf.get_syscall_fnname("kmalloc")
bpf.attach_kprobe(event=kmalloc_fn, fn_name="kprobe__kmalloc")

# 打印头部信息
print("Tracing kmalloc... Ctrl-C to end.")

# 循环打印结果
try:
    while True:
        for k, v in bpf["counts"].items():
            print("IP: 0x%x PID: %d COMM: %s Size: %d" % (k.ip, k.pid, k.comm.decode(), v.value))
        bpf["counts"].clear()
        sleep(2)
except KeyboardInterrupt:
    pass

这个程序使用kprobe附加到kmalloc函数上,每次kmalloc被调用时,都会记录调用者的IP地址、进程ID、进程名和分配的大小。然后,程序会定期打印出统计结果。

运行结果示例:

Tracing kmalloc... Ctrl-C to end.
IP: 0xffffffff8110b3a0 PID: 1234 COMM: cat Size: 4096
IP: 0xffffffff8110b3a0 PID: 5678 COMM: ls Size: 8192
IP: 0xffffffff8110b3a0 PID: 1234 COMM: cat Size: 4096
...

通过这个程序,你可以了解哪些进程在分配内存,以及分配了多少内存。这对于分析内存泄漏或内存碎片问题非常有帮助。

案例3:网络延迟分析

假设你需要分析网络延迟,你可以使用tc (Traffic Control) 命令和eBPF结合起来实现。首先,你需要编写一个eBPF程序来记录数据包的发送和接收时间:

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>

#define SEC(NAME) __attribute__((section(NAME), used))

struct data_t {
  u64 timestamp;
  u32 src_addr;
  u32 dst_addr;
  u16 src_port;
  u16 dst_port;
};

BPF_PERF_OUTPUT(events);

SEC("tracepoint/net/kfree_skb")
int bpf_prog1(void *ctx) {
  struct sk_buff *skb = ctx;
  struct ethhdr *eth = bpf_hdr_pointer(skb->data);

  if (eth->h_proto == htons(ETH_P_IP)) {
    struct iphdr *ip = bpf_hdr_pointer(skb->data + sizeof(struct ethhdr));
    if (ip->protocol == IPPROTO_TCP) {
      struct tcphdr *tcp = bpf_hdr_pointer(skb->data + sizeof(struct ethhdr) + sizeof(struct iphdr));

      struct data_t data = {};
      data.timestamp = bpf_ktime_get_ns();
      data.src_addr = ip->saddr;
      data.dst_addr = ip->daddr;
      data.src_port = tcp->source;
      data.dst_port = tcp->dest;

      events.perf_submit(ctx, &data, sizeof(data));
    }
  }

  return 0;
}

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

这个程序附加到kfree_skb tracepoint 上,当内核释放一个socket buffer时,会记录时间戳、源IP地址、目的IP地址、源端口和目的端口,并将数据发送到用户空间。

然后,你需要使用tc命令将eBPF程序附加到网络接口上:

tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj ./latency.o sec tracepoint/net/kfree_skb

最后,你需要编写一个用户空间的程序来接收eBPF程序发送的数据,并计算延迟:

from bcc import BPF
import argparse

parser = argparse.ArgumentParser(description="Trace network latency")
parser.add_argument("-i", "--interface", type=str, help="Network interface to trace", required=True)
args = parser.parse_args()

# Load the BPF program
b = BPF(src_file="latency.c", cflags=["-w"])

# Define the callback function
def print_event(cpu, data, size):
    event = b["events"].event(data)
    print("Timestamp: %d, Src Addr: %s, Dst Addr: %s, Src Port: %d, Dst Port: %d" % (
        event.timestamp,
        inet_ntop(AF_INET, pack(">I", event.src_addr)),
        inet_ntop(AF_INET, pack(">I", event.dst_addr)),
        event.src_port,
        event.dst_port
    ))

# Attach the callback function to the perf buffer
b["events"].open_perf_buffer(print_event)

# Print header
print("Tracing network latency on interface %s... Ctrl-C to end." % args.interface)

# Read events
while True:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()

注意事项:

  • tc 命令可能需要root权限。
  • eBPF程序的性能开销需要仔细评估,避免对系统性能产生过大的影响。
  • 不同的内核版本可能需要调整eBPF程序。

4. eBPF进阶:CO-RE与内核版本兼容性

在实际使用中,你可能会遇到内核版本兼容性问题。由于不同的内核版本可能具有不同的数据结构和API,因此你需要为每个内核版本编写不同的eBPF程序。这显然是不可接受的。

CO-RE(Compile Once – Run Everywhere)技术就是为了解决这个问题而生的。CO-RE允许你编写一次eBPF程序,然后在不同的内核版本上运行,而无需重新编译。CO-RE通过在运行时动态地调整eBPF程序,以适应不同的内核结构。

CO-RE的实现原理:

  1. BTF (BPF Type Format): BTF是一种描述内核数据结构的格式。内核在编译时会生成BTF信息,并将其嵌入到vmlinux文件中。
  2. libbpf CO-RE: libbpf CO-RE库可以在运行时读取BTF信息,并根据当前内核的数据结构,动态地调整eBPF程序。

如何使用CO-RE:

  1. 确保内核支持BTF: 较新的内核版本(通常是5.x以上)都支持BTF。
  2. 使用libbpf编译eBPF程序: 在编译eBPF程序时,需要使用libbpf提供的头文件和API。
  3. 使用libbpf加载eBPF程序: 在加载eBPF程序时,libbpf会自动读取BTF信息,并根据当前内核的数据结构,动态地调整eBPF程序。

通过使用CO-RE技术,你可以大大简化eBPF程序的开发和维护工作,并提高eBPF程序的兼容性。

5. 总结与展望

eBPF作为一种强大的内核观测和分析技术,正在被越来越多的内核开发者所使用。它可以帮助你深入了解内核的运行状态,快速定位和解决内核问题,并优化内核性能。虽然eBPF的学习曲线可能比较陡峭,但只要你掌握了基本概念和工具,就可以充分利用eBPF的优势,成为内核问题排查高手。

未来展望:

  • 更强大的工具链: 未来将会出现更多更易用的eBPF工具,例如图形化的eBPF IDE、自动化的eBPF程序生成器等。
  • 更广泛的应用场景: eBPF将会被应用到更多的领域,例如安全分析、容器监控、云原生应用等。
  • 更深入的内核集成: eBPF将会与内核更加紧密地集成,例如通过eBPF来实现内核模块的热更新、内核功能的动态扩展等。

希望本文能够帮助你入门eBPF,并在实际工作中应用eBPF来解决内核问题。祝你成为一名优秀的内核开发者!

内核侦探 eBPFLinux内核内核调试

评论点评