bcc:调用栈采集的学习和使用

2022年9月21日

过程

依照谢宝友老师针对高负载所给出的模块功能,写了一个ebpf程序,重点在如何获取进程的栈和输出它。类似内核函数stack_trace_save_tskstack_trace_print

最开始的思路是在官方给出的程序集/usr/share/bcc中寻找类似功能的程序,发现example/tracing/stacksnoop.py工具有类似的功能,它可以跟踪指定跟踪点,输出触发了跟踪点的进程的内核栈,通过stress -i 1触发ext4_sync_fs探测点的效果如下:

在这里插入图片描述

通过查看它的代码我发现,它使用了BPF_STACK_TRACE(stack_traces, 128);声明

BPF_STACK_TRACE

Syntax: BPF_STACK_TRACE(name, max_entries)

Creates stack trace map named name, with a maximum entry count provided. These maps are used to store stack traces.

For example:

BPF_STACK_TRACE(stack_traces, 1024);

This creates stack trace map named stack_traces, with a maximum number of stack trace entries of 1024.

This is a wrapper macro for BPF_TABLE("stacktrace", ...).

Methods (covered later): map.get_stackid().

Examples in situ: search /examples, search /tools

在内核端,通过stack_traces.get_stackid(ctx, 0)通过当前进程ctx上下文保存栈,获取当前进程的内核栈的栈编号,其中ctx是上下文信息,0的意义暂时未知;该工具将每个进程的stackid都传送到了用户端。

map.get_stackid()

Syntax: int map.get_stackid(void *ctx, u64 flags)

This walks the stack found via the struct pt_regs in ctx, saves it in the stack trace map, and returns a unique ID for the stack trace.

Examples in situ: search /examples, search /tools

在用户端,通过stack_traces.walk(event.stack_id)获取了stackid对应的栈列表。

在查找资料的过程中,<src>\tools\include\uapi\linux\bpf.h文件引起了我的注意,里面有很对helper function的索引和完整的注释,可惜的是我并没有找到get_stackid的定义,有个相似的函数:

long bpf_get_stackid(void *ctx, struct bpf_map *map, u64 flags)

其部分注释通过翻译得:

最后一个参数 flags 保存要跳过的堆栈帧数(从0到255),用掩码 BPF_F_SKIP_FIELD_MASK 获取。之后的位可以用来设置一个以下标志的组合:

BPF_F_USER_STACK

收集用户空间堆栈而不是内核堆栈。

BPF_F_FAST_STACK_CMP

只通过哈希比较堆栈。

BPF_F_REUSE_STACKID

如果两个不同的堆栈哈希到相同的堆栈id,则丢弃旧的堆栈。

猜测get_stackid中的flag是同样的功能。

这里,还发现了另外两个栈相关的helper函数:

long bpf_get_task_stack(struct task_struct *task, void *buf, u32 size, u64 flags)

描述

返回bpf程序提供的缓冲区中的用户或内核堆栈。要实现这一点,helper需要 task,它是指向 struct task_struct 的有效指针。为了存储堆栈跟踪,bpf程序提供非负 sizebuf

最后的参数 flags 保存要跳过的堆栈帧数(从0到255),用掩码 BPF_F_SKIP_FIELD_MASK 获取。接下来的位可以用来设置以下标志:

  • BPF_F_USER_STACK

    • 收集用户空间堆栈而不是内核堆栈。
  • BPF_F_USER_BUILD_ID

    • 为用户堆栈收集 buildid+offset 而不是ips,仅当 BPF_F_USER_STACK 也指定时有效。

bpf_get_task_stack()可以收集多达PERF_MAX_STACK_DEPTH的内核帧和用户帧,前提是缓冲区足够大。请注意,这个限制可以用sysctl程序控制,并且应该手动增加它,以便分析长用户堆栈(例如Java程序的堆栈)。要做到这一点,使用:

sysctl kernel.perf_event_max_stack = <新值>

返回

成功时是等于或小于 size 的非负值,失败时是表示错误的负值。

long bpf_get_stack(void *ctx, void *buf, u32 size, u64 flags)

描述

返回 bpf 程序提供的缓冲区中的用户或内核堆栈。要实现这一点,辅助程序需要 ctx,这是一个指向跟踪程序所执行的上下文的指针。为了存储堆栈跟踪,bpf程序提供了非负 sizebuf

最后的参数 flags 保存要跳过的堆栈帧数(从0到255),用掩码 BPF_F_SKIP_FIELD_MASK 获取。接下来的位可以用来设置以下标志:

  • BPF_F_USER_STACK

    • 收集用户空间堆栈而不是内核堆栈。
  • BPF_F_USER_BUILD_ID

    • 为用户堆栈收集 buildid+offset 而不是ips,仅当 BPF_F_USER_STACK 也指定时有效。

bpf_get_stack()可以收集多达PERF_MAX_STACK_DEPTH的内核帧和用户帧,前提是缓冲区足够大。请注意,这个限制可以用sysctl程序控制,并且应该手动增加它,以便分析长用户堆栈(例如Java程序的堆栈)。要做到这一点,使用:

sysctl kernel.perf_event_max_stack = <新值>

返回

成功时是等于或小于 size 的非负值,失败时是表示错误的负值。

代码

于是用以上两个函数,写了一下目标程序,唯一的问题是无法遍历运行队列来获取当时全部的task_struct,这里通检测load均值达到阈值后的一段连续触发,将连续触发的进程栈存储进表中,来弥补这种问题。

from bcc import BPF
import time
code = """
#include <linux/sched.h>
#define STACK_DEPTH_MAX 20 // 栈最大深度
struct data { // 保存进程栈的相关信息
    char comm[TASK_COMM_LEN]; // 保存进程对应的命令 16B
    long depth; // 保存栈深度 4B
    unsigned pass; // 填充 4B
    unsigned long stack[STACK_DEPTH_MAX]; // 保存栈各函数的地址 20*4B
}; // 13*64bit 
struct rq {
	raw_spinlock_t lock; // 运行队列自旋锁
	unsigned int nr_running; // 运行队列进程数
}; // 运行队列的一部分,用来仿制队列结构,因为真实的队列结构不是内核头文件的一部分,无法直接使用
BPF_HASH(TraceTable, u32, struct data);
void kprobe__update_rq_clock(struct pt_regs *ctx) { // update_rq_clock事件的处理程序
    struct data trace = {0}, *tmp; // trace用来保存当前进程栈,tmp用来指向查找到的结果
    struct task_struct *task; // 用来保存当前进程结构
    struct rq p_rq; // 用来保存运行队列信息
    u32 pid; // 用来保存当前进程号
    bpf_probe_read_kernel(&p_rq, sizeof(struct rq), (void *)PT_REGS_PARM1(ctx)); // 将运行队列信息拷贝到p_rq中
    if(p_rq.nr_running > 4) { // 当负载高时,计划记录当前进程调用栈
        task = (struct task_struct *)bpf_get_current_task(); // 获取当前进程控制块
        if(task->__state == TASK_RUNNING) { // 当前进程为运行中时,记录栈
            pid = task->pid; // 获取当前进程pid
            tmp = TraceTable.lookup(&pid); // 查找当前进程的记录
            if(tmp) // 如果有记录,就不再次记录,直接返回
                return;
            trace.depth = bpf_get_stack( // 参考上下文信息,将进程的内核栈和栈大小记录到trace中
                ctx, // 上下文信息
                trace.stack, // 保存栈的数组
                STACK_DEPTH_MAX*sizeof(unsigned long), // 数组的大小(单位:字节)
                0ULL // 控制读取内核或用户栈、忽略的栈层数、偏移量,这里的0表示读取内核栈、无忽略、无偏移
            );
            trace.depth /= sizeof(unsigned long); // 将栈大小换算为栈深度
            bpf_probe_read_kernel_str(&trace.comm, TASK_COMM_LEN, task->comm); // 将当前进程对应的命令保存到trace中
            TraceTable.update(&pid, &trace); // 记录trace数据
        }
    }
};
"""
CueRQ = BPF(text=code) # 初始化bpf对象
TraceTable = CueRQ["TraceTable"] # 获取进程栈记录表
with open("/proc/cpuinfo", "r") as file: # 获取逻辑cpu数量
    nr_cpu = file.read().count("processor")
print("cpu number: ", nr_cpu) # 打印cpu数量
while True:
    try:
        if len(TraceTable) / nr_cpu > 4: # 如果平均负载超过4,就打印栈信息
            print("load average: ", len(TraceTable)/nr_cpu) # 打印平均负载
            print("_"*60) # 打印栈信息分隔符
            for k, v in TraceTable.items(): # 遍历进程栈记录表
                print("pid: %-6d\tcomm: %-16s" % (k.value, v.comm.decode())) # 打印进程信息头
                # print(list(v.stack), v.depth)
                print("\t%-16s\t%s" % ("address", "function")) # 打印栈的表头
                for i in range(0, v.depth): # 遍历栈
                    print("\t%#016x\t%s" % (v.stack[i], CueRQ.ksym(v.stack[i]).decode())) # 打印栈各层信息
            print("_"*60, "\n") # 打印栈信息分隔符
            TraceTable.clear() # 清除进程栈记录表
            time.sleep(5) # 挂起5s
    except KeyboardInterrupt: # 捕获按键中断异常(ctrl+c)
        print("\b\bQuit.\n") # 打印退出提示,\b表示退一格,两个\b可以退回ctrl+c的输出^c之前,以便之后Quit可以将^c掩盖
        exit() # 退出程序

结果

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lcy_Knight

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值