Linux eBPF Stack Trace Hack

Linux eBPF对堆栈跟踪的支持将使许多新的和令人敬畏的事情成为可能,但是,它并没有进入刚刚发布的 Linux 4.4,它添加了其他 eBPF 特性。设想一些时间在具有 eBPF 但没有堆栈跟踪的旧内核上,我已经开发了一个 hacky 解决方法来做一些很棒的事情。
我将展示我的新密件抄送工具(eBPF 前端),然后解释它是如何工作的。
stackcount:频率计数内核堆栈跟踪
stackcount工具频率计算给定函数的内核堆栈。这是在内核中使用 eBPF 映射来提高效率的。只有唯一的堆栈及其计数被复制到用户级别进行打印。
例如,导致 submit_bio() 的频率计数内核堆栈跟踪:

./stackcount submit_bio

跟踪“submit_bio”的 1 个函数…按 Ctrl-C 结束。
^C
提交_生物
提交_bh
journal_submit_commit_record.isra.13
jbd2_journal_commit_transaction
kjournald2
线程
ret_from_fork
mb_cache_list
1

[…截断…]

提交_生物
提交_bh
jbd2_journal_commit_transaction
kjournald2
线程
ret_from_fork
mb_cache_list
38

提交_生物
ext4_writepages
do_writepages
__filemap_fdatawrite_range
文件映射刷新
ext4_alloc_da_blocks
ext4_rename
ext4_rename2
vfs_rename
sys_rename
entry_SYSCALL_64_fastpath
79
打印堆栈跟踪的顺序是从最少到最频繁。此示例中最频繁的,最后打印的,在跟踪期间被拍摄了 79 次。
最后的堆栈跟踪显示系统调用处理、ext4_rename() 和 filemap_flush():看起来应用程序发出的文件重命名由于 ext4 块分配和 filemap_flush() 而导致后端磁盘 I/O。
该工具对于探索和研究内核行为非常有用,可以快速回答给定函数的调用方式。
stacksnoop:打印内核堆栈跟踪
stacksnoop工具为每个事件打印内核堆栈跟踪。例如,对于 ext4_sync_fs():

./stacksnoop -v ext4_sync_fs

TIME(s) COMM PID CPU 堆栈
42005557.056332998 同步 22352 1
42005557.056336999 同步 22352 1 ip: ffffffff81280461 ext4_sync_fs
42005557.056339003 同步 22352 1 r0: ffffffff811ed7f9 iterate_supers
42005557.056340002 同步 22352 1 r1:ffffffff8121ba25 sys_sync
42005557.056340002 sync 22352 1 r2: ffffffff81775cb6 entry_SYSCALL_64_fastpath
42005557.056358002 sync 22352 1
42005557.056358002 sync 22352 1 ip: ffffffff81280461 ext4_sync_fs
42005557.056359001 sync 22352 1 r0: ffffffff811ed7f9 iterate_supers
42005557.056359999 sync 22352 1 r1: ffffffff8121ba35 sys_sync
42005557.056359999 sync 22352 1 r2: ffffffff81775cb6 entry_SYSCALL_64_fastpath
Since the output is verbose, this isn’t suitable for high frequency calls (eg, over 1,000 per second). You can use funccount from bcc tools to measure the rate of a function call, and if it is high, try stackcount instead.
How It Works: Crazy Stuff
eBPF 是一个内核虚拟机,它可以做各种各样的事情,包括“疯狂的事情”。所以我在 eBPF 中编写了一个用户定义的堆栈遍历器,内核可以运行它。以下是来自 stackcount 的相关代码(您不会理解这一点):
#define 最大深度 10

结构键_t {
u64 ip;
u64 右[MAXDEPTH];
};
BPF_HASH(计数,结构 key_t);

静态 u64 get_frame(u64 *bp) {
如果(*bp){
// 以下堆栈遍历器是 x86_64 特定的
u64 右 = 0;
if (bpf_probe_read(&ret, sizeof(ret), (void *)(*bp+8)))
返回0;
if (bpf_probe_read(bp, sizeof(*bp), (void *)*bp))
*bp = 0;
如果 (ret < __START_KERNEL_map)
返回0;
返回正确;
}
返回0;
}

int trace_count(struct pt_regs *ctx) {
筛选
结构 key_t 键 = {};
u64 零 = 0,*val,bp = 0;
整数深度 = 0;

key.ip = ctx->ip;
bp = ctx->bp;

// 展开循环,10 (MAXDEPTH) 帧深:
if (!(key.ret[depth++] = get_frame(&bp))) goto out;
if (!(key.ret[depth++] = get_frame(&bp))) goto out;
if (!(key.ret[depth++] = get_frame(&bp))) goto out;
if (!(key.ret[depth++] = get_frame(&bp))) goto out;
if (!(key.ret[depth++] = get_frame(&bp))) goto out;
if (!(key.ret[depth++] = get_frame(&bp))) goto out;
if (!(key.ret[depth++] = get_frame(&bp))) goto out;
if (!(key.ret[depth++] = get_frame(&bp))) goto out;
if (!(key.ret[depth++] = get_frame(&bp))) goto out;
if (!(key.ret[depth++] = get_frame(&bp))) goto out;

出去:
val = counts.lookup_or_init(&key, &zero);
(*val)++;
返回0;
}
一旦 eBPF 正确支持这一点,上述大部分代码将成为单个函数调用。
如果你好奇:我使用展开的循环遍历每一帧(eBPF 不进行向后跳转),在这种情况下最多十帧。它遍历 RBP 寄存器(基指针)并将每一帧的返回指令指针保存到一个数组中。我不得不使用显式的 bpf_probe_read()s 来取消引用指针(bcc 在某些情况下可以自动执行此操作)。我还在代码中保留了展开的循环(Python 可以生成它)以保持简单,并帮助说明开销。
这个 hack(到目前为止)仅适用于 x86_64、内核模式和有限的堆栈深度。如果我(或您)真的需要更多,请继续进行黑客攻击,但请记住,这只是一种解决方法,直到存在适当的堆栈遍历。
其他解决方案
stackcount 为核心 Linux 内核实现了一项重要的新功能:频率计数堆栈跟踪。像 stacksnoop 一样打印堆栈跟踪已经有很长时间了:ftrace 可以做到这一点,我在perf-tools的kprobe工具中使用它。perf_events还可以转储堆栈跟踪,并具有将打印唯一路径和百分比的报告模式(尽管在用户模式下执行效率较低)。
SystemTap 长期以来一直能够对内核模式和用户模式堆栈跟踪进行频率计数,也在内核中以提高效率,尽管它是一个附加组件而不是主线内核的一部分。
未来的读者
如果您使用的是 Linux 4.5 或更高版本,那么 eBPF 可能会正式支持堆栈遍历。要检查,请在 bpf_func_id 中查找类似 BPF_FUNC_get_stack 的内容。或者查看stackcount等工具的最新源代码——该工具应该仍然存在,但是上面的 stack walker hack 可以用一个简单的调用来代替。
感谢 Brenden Blanco (PLUMgrid) 对这个 hack 的帮助。如果您在SCaLE14x,您可以在周六观看他的IO Visor eBPF 演讲,以及周日我的Broken Linux Performance Tools演讲!

转载翻译自: https://www.brendangregg.com