2022年9月21日
过程
依照谢宝友老师针对高负载所给出的模块功能,写了一个ebpf程序,重点在如何获取进程的栈和输出它。类似内核函数stack_trace_save_tsk
和stack_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程序提供非负 size 的 buf。
最后的参数 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程序提供了非负 size 的 buf。
最后的参数 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() # 退出程序