内核开发者实战:如何用eBPF排查Linux内核问题?
作为一名内核开发者,你是否经常遇到这些头疼的问题?线上环境内核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工具,例如
tcpdump
、opensnoop
、execsnoop
等,可以用于网络分析、文件访问监控、进程执行跟踪等。它使用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的实现原理:
- BTF (BPF Type Format): BTF是一种描述内核数据结构的格式。内核在编译时会生成BTF信息,并将其嵌入到vmlinux文件中。
- libbpf CO-RE: libbpf CO-RE库可以在运行时读取BTF信息,并根据当前内核的数据结构,动态地调整eBPF程序。
如何使用CO-RE:
- 确保内核支持BTF: 较新的内核版本(通常是5.x以上)都支持BTF。
- 使用libbpf编译eBPF程序: 在编译eBPF程序时,需要使用libbpf提供的头文件和API。
- 使用libbpf加载eBPF程序: 在加载eBPF程序时,libbpf会自动读取BTF信息,并根据当前内核的数据结构,动态地调整eBPF程序。
通过使用CO-RE技术,你可以大大简化eBPF程序的开发和维护工作,并提高eBPF程序的兼容性。
5. 总结与展望
eBPF作为一种强大的内核观测和分析技术,正在被越来越多的内核开发者所使用。它可以帮助你深入了解内核的运行状态,快速定位和解决内核问题,并优化内核性能。虽然eBPF的学习曲线可能比较陡峭,但只要你掌握了基本概念和工具,就可以充分利用eBPF的优势,成为内核问题排查高手。
未来展望:
- 更强大的工具链: 未来将会出现更多更易用的eBPF工具,例如图形化的eBPF IDE、自动化的eBPF程序生成器等。
- 更广泛的应用场景: eBPF将会被应用到更多的领域,例如安全分析、容器监控、云原生应用等。
- 更深入的内核集成: eBPF将会与内核更加紧密地集成,例如通过eBPF来实现内核模块的热更新、内核功能的动态扩展等。
希望本文能够帮助你入门eBPF,并在实际工作中应用eBPF来解决内核问题。祝你成为一名优秀的内核开发者!