开源工具学习记录之流程梳理
近期对腾讯的的开源项目: nettrace(网络故障分析工具) ,进行源码学习。
开源仓库:Nettrace开源仓库
开源工具实现注释:nettrace学习记录
Nettrace 源码分析
nettrace数据分析模块主要涉及到以下几个文件中的源码:
nettrace.c
trace.c
,trace.h
analysis.c
,analysis.h
trace_group.c
1. 数据采集模块
详细分析:Nettrace数据采集方法研究
1.1 main()入口函数
在nettrace.c
的main()
函数中,实现了从初始化跟踪数组, 到准备跟踪环境,再到加载并附加bpf程序, 最终通过trace_poll
函数启动网络数据监听与数据处理;
int main(int argc, char *argv[])
{
/*1.初始化跟踪组*/
init_trace_group();
/*2.参数解析*/
do_parse_args(argc, argv);
/*3.跟踪点有效性标记*/
if (trace_prepare())
goto err;
/*4.加载并附加eBPF程序*/
if (trace_bpf_load_and_attach()) {
pr_err("failed to load bpf\n");
goto err;
}
signal(SIGTERM, do_exit);
signal(SIGINT, do_exit);
pr_info("begin trace...\n");
/*5.启动网络追踪功能*/
trace_poll(trace_ctx);
do_exit(0);
return 0;
err:
return -1;
}
1.1.1初始化跟踪组
在项目编译过程中,会通过gen_trace.py
脚本去解析trace.yaml
文件,生成trace_group.c
与kprobe_trace.h
文件,前者定义了所有的挂载点以及跟踪点链表, 并组建成了跟踪点树, 后者对所有的挂载点定义了其索引;
nettrace.c ----------------- nettrace
trace.c |
xxxxx |
|
|
trace_group.c |
╱ |
trace.yaml -- gen_trace.py |
╲ |
kprobe_trace.h |
╲ |
kprobe.o → kprobe.skel.h
╱
kprobe.c
在trace_group.c
文件中依靠init_trace_group()函数初始化了跟踪组,将项目所涉及的所有跟踪点构建成树的形式;
trace_group_t root_group = {
.name = "all",
.desc = "trace the whole kernel network stack",
.children = LIST_HEAD_INIT(root_group.children),
.traces = LIST_HEAD_INIT(root_group.traces),
.list = LIST_HEAD_INIT(root_group.list),
};
trace_group_t group_link = {
.name = "link",
.desc = "link layer (L2) of the network stack",
.children = LIST_HEAD_INIT(group_link.children),
.traces = LIST_HEAD_INIT(group_link.traces),
.list = LIST_HEAD_INIT(group_link.list),
};
trace_t trace_dev_gro_receive = {
.desc = "",
.type = TRACE_FUNCTION,
.analyzer = &ANALYZER(default),
.is_backup = false,
.probe = false,
.name = "dev_gro_receive",
.skb = 2,
.custom = false,
.def = true,
.index = INDEX_dev_gro_receive,
.prog = "__trace_dev_gro_receive",
.parent = &group_link_in,
.rules = LIST_HEAD_INIT(trace_dev_gro_receive.rules),
};
trace_list_t trace_dev_gro_receive_list = {
.trace = &trace_dev_gro_receive,
.list = LIST_HEAD_INIT(trace_dev_gro_receive_list.list)
};
rule_t rule_trace_dev_gro_receive_0 = { .level = RULE_ERROR,
.expected = 4,c
.type = RULE_RETURN_EQ,
.msg = PFMT_ERROR"packet is dropped by GRO"PFMT_END,
};
1.1.2 参数解析
do_parse_args
函数对参数进行解析,根据不同的传参,设定不同的跟踪模式;
1.1.3 环境检查、模式设定、跟踪点的初始化
trace_prepare()
函数会检查系统是否支持BTF、内核版本的兼容性、并通过trace_prepare_args()
设置跟踪模式、通过trace_prepare_traces()
进行挂载点的预处理(遍历项目所涉及全部跟踪点,并标记其是否有效)
1.1.4 加载并附加bpf程序
这里需要注意的是: 在trace_bpf_load_and_attach()
中,除了load_bpf , attach_bpf,还进行了相关ops操作的初始化:
/*加载并附加eBPF程序*/
int trace_bpf_load_and_attach()
{
/*1.load_bpf*/
if (trace_bpf_load())
goto err;
pr_debug("begin to attach eBPF program...\n");
/*2.attach bpf*/
if (trace_ctx.ops->trace_attach()) {
trace_ctx.ops->trace_close();
goto err;
}
pr_debug("eBPF program attached successfully\n");
/*3.准备ops操作函数*/
trace_prepare_ops();
return 0;
err:
return -1;
}
我们看一下trace_prepare_ops()
函数,他根据不同的mode模式初始化trace_ctx.ops->trace_poll
所对应的回调函数;
/*根据不同模式初始化操作集*/
static void trace_prepare_ops()
{
/*1.根据配置,决定是否启用函数调用统计功能
* trace_ctx.bpf_args.func_stats:指定是否启用函数调用统计功能
* func_stats_poll_handle函数会定期从ebpf map中读取统计数据;
*/
if (trace_ctx.bpf_args.func_stats) {
trace_ctx.ops->raw_poll = func_stats_poll_handler;
return;
}
/*2.不需要统计功能,根据mod模式匹配对应的trace_poll或raw_poll函数
*/
switch (trace_ctx.mode) {
case TRACE_MODE_BASIC:
case TRACE_MODE_DROP:
case TRACE_MODE_MONITOR:
trace_ctx.ops->trace_poll = basic_poll_handler;
break;
case TRACE_MODE_SOCK:
trace_ctx.ops->trace_poll = async_poll_handler;
break;
case TRACE_MODE_DIAG:
case TRACE_MODE_TIMELINE:
trace_ctx.ops->trace_poll = ctx_poll_handler;
break;
case TRACE_MODE_LATENCY:
if (trace_ctx.bpf_args.latency_summary)
trace_ctx.ops->raw_poll = stats_poll_handler;
else
trace_ctx.ops->trace_poll = latency_poll_handler;
break;
case TRACE_MODE_RTT:
trace_ctx.ops->raw_poll = stats_poll_handler;
default:
break;
}
}
1.1.5 trace_poll 监听及处理数据
main()函数中通过trace_poll()函数实现对网络追踪数据的持续监听与数据处理;
在这里,该函数先调用perf_buffer__new创建一个新的buffer,并将其回调函数绑定为poll_handler_wrap
,通过一个while循环持续监听perf_buffer缓冲区,如果有数据到来,就调用poll_hander_wrap
函数来进行数据处理。所以这里的重点在poll_handler_wrap
函数;
int trace_poll()
{
struct perf_buffer *pb;
int err, map_fd;
if (trace_ctx.ops->raw_poll) {
trace_ctx.ops->trace_ready();//该函数在tracepoint或kprobe对应的文件中有定义;
return trace_ctx.ops->raw_poll();
}
/*1.获取map描述符*/
map_fd = bpf_object__find_map_fd_by_name(trace_ctx.obj, "m_event");
if (!map_fd)
return -1;
#if defined(LIBBPF_MAJOR_VERSION) && (LIBBPF_MAJOR_VERSION >= 1)
/*2.创建perf_buffer 用于监听eBPF程序的输出;
* poll_handler_wrap函数根据指定的模式进行数据的聚合与分析;
* trace_on_lost 用于处理丢失事件的回调函数;
*/
pb = perf_buffer__new(map_fd, 1024, poll_handler_wrap,
trace_on_lost, NULL, NULL);
#else
struct perf_buffer_opts pb_opts = {
.sample_cb = poll_handler_wrap,
.lost_cb = trace_on_lost,
};
/*创建*/
pb = perf_buffer__new(map_fd, 1024, &pb_opts);
#endif
/*3. 检查perf buffer是否创建成功*/
err = libbpf_get_error(pb);
if (err) {
pr_err("failed to setup perf_buffer: %d\n", err);
return err;
}
trace_ctx.ops->trace_ready();
/*4.开始监听数据
* 调用perf_buffer__poll 监听数据,每次有数据进来,都会触发回调函数poll_handler_wrap;
*/
while ((err = perf_buffer__poll(pb, 1000)) >= 0) {
if (poll_timeout(err))
break;
}
return 0;
}
我们看一下poll_handler_wrap
函数源码,他会调用trace_ctx.ops->trace_poll
函数,而这个函数是我们1.1.1中通过trace_prepare_ops()
绑定的函数;
static inline void poll_handler_wrap(void *ctx, int cpu, void *data,
u32 size)
{
/*1.判断是否停止跟踪*/
if (trace_stopped())
return;
/*2.调用 trace_poll 函数完成具体的数据处理
* 具体实现依赖于 trace_ctx.ops
*/
trace_ctx.ops->trace_poll(ctx, cpu, data, size);//一个指向具体处理函数的指针;
}
也就是下图这几个函数:
2.数据处理
经过上一部分的源码分析,已经将数据处理的函数对应到了trace_ctx.ops->trace_poll函数中,接下来就是分析这些数据处理函数时如何进行数据处理和分析的;
我们以basic_poll_handler为例, 梳理该函数是如何处理TRACE_MODE_BASIC、TRACE_MODE_DROP、TRACE_MODE_MONITOR模式下采集到的的数据。
/*TRACE_MODE_BASIC
*TRACE_MODE_DROP
*TRACE_MODE_MONITOR
*/
void basic_poll_handler(void *ctx, int cpu, void *data, u32 size)
{
analy_entry_t entry = {
.event = data,
.cpu = cpu
};
/*对采集到的单条数据 analy_entry_t 执行分析处理*/
entry_basic_poll(&entry);
}
可以看出该函数先是初始化了analy_entry_t
结构体,然后通过entry_basic_poll(&entry)
进行数据分析处理.所以entry_basic_poll(&entry)
是处理数据的核心函数,analy_entry_t
是分析数据的核心结构体他存储着要分析的数据块的全部信息;
/*对采集到的单条数据 analy_entry_t 执行分析处理*/
static inline void entry_basic_poll(analy_entry_t *entry)
{
trace_t *trace;
/*1.从 analy_entry_t 中获取 trace_t 信息*/
trace = get_trace_from_analy_entry(entry);
/*2.尝试运行与 trace 绑定的 entry 分析器*/
try_run_entry(trace, trace->analyzer, entry);
/*3.如果需要分析返回值,调用 exit 分析器*/
if (trace_analyse_ret(trace)) {
analy_exit_t analy_exit = {
.event = {
.val = entry->event->retval,
},
.entry = entry,
};
try_run_exit(trace, trace->analyzer, &analy_exit);
}
/*4.输出 entry 的分析结果*/
analy_entry_output(entry, NULL);
}
entry_basic_poll()
函数会先通过get_trace_from_analy_entry()
找到数据块的源头trace_t的信息,即哪一个跟踪点产生了这个数据块;接着尝试使用trace对应的分析器的entry和exit函数去进行数据处理与分析(try_run_entry()
,try_run_exit()
),最后调用analy_entry_output()
输出分析结果;
所以说实现数据分析的具体函数在分析器中定义着,需要进一步去查看分析器相关实现代码;
3.分析器的实现
在第二部分中,entry_basic_poll()
函数是通过trace_t结构体找到分析器的entry和exit处理函数,先看一下trace_t结构体:
typedef struct trace {
// 内核函数或跟踪点的名称,最长 64 字符
char name[64];
// 对跟踪点的简要描述,例如用途或功能
char *desc;
// 关联的消息,用于日志记录或调试输出
char *msg;
// 绑定到该跟踪点的 eBPF 程序的名称
char *prog;
// 跟踪点的类型,例如函数跟踪(TRACE_FUNCTION)或 tracepoint(TRACE_TP)
enum trace_type type;
// 可选的条件,用于判断是否启用跟踪点
char *cond;
// 可选的正则表达式,用于匹配内核函数名称
char *regex;
// 如果是 tracepoint 类型,记录 tracepoint 的名称
char *tp;
// 函数参数列表中 skb(socket buffer)的索引,从 1 开始,0 表示没有 skb
u8 skb;
// 在 ftrace 事件中 skb 的偏移量
u8 skboffset;
// 函数参数列表中 socket 的索引,与 skb 类似
u8 sk;
// 在 ftrace 事件中 socket 的偏移量
u8 skoffset;
// 全局链表,包含所有的 trace_t 实例
struct list_head all;
// 属于同一组的链表,管理 trace 分组
struct list_head list;
// 与该跟踪点关联的规则链表,例如匹配条件或阈值
struct list_head rules;
// 指向备份 trace 的指针,用于冗余或备用
struct trace *backup;
// 标记该 trace 是否为备份点
bool is_backup;
// 标志是否为探针模式(通常用于调试)
bool probe;
// 标志是否默认启用此跟踪点
bool def;
// 标志 eBPF 程序是否为自定义的
bool custom;
// 监控模式的标志,定义该 trace 的监控模式
int monitor;
// 唯一索引,用于标识此 trace
int index;
// 被跟踪函数的参数数量
int arg_count;
// 跟踪点的状态,例如加载、启用或无效等标志位
u32 status;
// 指向父组的指针,表示此 trace 的分组归属
trace_group_t *parent;
// 指向分析器模块的指针,用于关联分析逻辑
struct analyzer *analyzer;
} trace_t;
这个结构体中的最后一个字段是analyzer
也就是当前跟踪点对应的分析器,entry_basic_poll()
就是通过这个分析器中的函数进行数据分析的;
2.3.1 trace_t与分析器的关系
不同的跟踪点根据trace.yaml
配置文件定义了该跟踪点是否需要分析器以及用哪个分析器;trace.yaml
文件会通过gen_trace.py
文件生成trace_group.c
以及kprobe_trace.h
文件。在所生成的trace_group.c
文件中,定义了每个跟踪点的trace_t结构体,我们拿trace_tcp_ack_update_rtt
挂载点为例进行介绍:
trace_t trace_tcp_ack_update_rtt = {
.desc = "",
.type = TRACE_FUNCTION,
/*分析器*/
.analyzer = &ANALYZER(rtt),
.is_backup = false,
.probe = false,
.name = "tcp_ack_update_rtt",
.sk = 1,
.custom = true,
.def = true,
.index = INDEX_tcp_ack_update_rtt,
.prog = "__trace_tcp_ack_update_rtt",
.parent = &group_tcp_state,
.rules = LIST_HEAD_INIT(trace_tcp_ack_update_rtt.rules),
};
可以看到该跟踪点的分析器指向了某个地址&ANALYZER(rtt)
,该地址做进一步的探索:
在analysis.h文件中有对ANALYZER
的宏定义:
#define ANALYZER(name) analyzer_##name
将宏ANALYZER(rtt)
展开后便是:.analyzer = &analyzer_rtt,
analyzer_rtt是什么?它对应的地址有什么?后面我们会涉及;
2.3.2 分析器的定义
在analysis.c
文件中定义了八个不同的分析器,我们以rtt为例进行介绍;
DEFINE_ANALYZER_ENTRY(rtt, TRACE_MODE_ALL_MASK)
{
/*1.通过define_pure_event提取rtt事件数据*/
define_pure_event(rtt_event_t, event, e->event);
char *msg = malloc(1024);
msg[0] = '\0';
/*2.将first_rtt和last_rtt格式化成字符串,用于输出和日志记录*/
sprintf(msg, PFMT_EMPH_STR(" *rtt:%ums, rtt_min:%ums*"),
event->first_rtt, event->last_rtt);
/*3.entry_set_msg将聚合后的消息绑定到当前分析条目analy_entry_t*/
entry_set_msg(e, msg);
return RESULT_CONT;
}
DEFINE_ANALYZER_EXIT_FUNC_DEFAULT(rtt)
上面这段代码是对rtt分析器的定义,我们进一步了解一下DEFINE_ANALYZER_ENTRY(rtt, TRACE_MODE_ALL_MASK)
和DEFINE_ANALYZER_EXIT_FUNC_DEFAULT(rtt)
是什么,展开后是什么样的;
在analysis.h
文件中包含着与分析器相关的宏定义:
#define ANALYZER(name) analyzer_##name
#define DEFINE_ANALYZER_PART(name, type, mode_mask) \
analyzer_result_t analyzer_##name##_exit(trace_t *trace, \
analy_exit_t *e) __attribute__((weak)); \
analyzer_result_t analyzer_##name##_entry(trace_t *trace, \
analy_entry_t *e) __attribute__((weak)); \
analyzer_t ANALYZER(name) = { \
.analy_entry = analyzer_##name##_entry, \
.analy_exit = analyzer_##name##_exit, \
.mode = mode_mask, \
}; \
analyzer_result_t analyzer_##name##_##type(trace_t *trace, \
analy_##type##_t *e)
#define DEFINE_ANALYZER_ENTRY(name, mode) \
DEFINE_ANALYZER_PART(name, entry, mode)
#define DEFINE_ANALYZER_EXIT(name, mode) \
DEFINE_ANALYZER_PART(name, exit, mode)
#define DEFINE_ANALYZER_EXIT_FUNC(name) \
analyzer_result_t analyzer_##name##_exit(trace_t *trace, \
analy_exit_t *e)
#define DEFINE_ANALYZER_EXIT_FUNC_DEFAULT(name) \
DEFINE_ANALYZER_EXIT_FUNC(name) \
{ \
rule_run_ret(e->entry, trace, e->event.val); \
return RESULT_CONT; \
}
#define DECLARE_ANALYZER(name) extern analyzer_t ANALYZER(name)
#define IS_ANALYZER(target, name) (target == &(ANALYZER(name)))
我们以rtt为例,将分析器的定义进行宏展开:
analyzer_result_t analyzer_rtt_exit(trace_t *trace, analy_exit_t *e) __attribute__((weak));
analyzer_result_t analyzer_rtt_entry(trace_t *trace, analy_entry_t *e) __attribute__((weak));
/*analyzer_rtt结构体,trace_t中的.analyzer指向的就是这个结构体*/
analyzer_t analyzer_rtt = {
.analy_entry = analyzer_rtt_entry,
.analy_exit = analyzer_rtt_exit,
.mode = TRACE_MODE_ALL_MASK,
};
analyzer_result_t analyzer_rtt_entry(trace_t *trace, analy_entry_t *e)
{
/*1.通过define_pure_event提取rtt事件数据*/
define_pure_event(rtt_event_t, event, e->event);
char *msg = malloc(1024);
msg[0] = '\0';
/*2.将first_rtt和last_rtt格式化成字符串,用于输出和日志记录*/
sprintf(msg, PFMT_EMPH_STR(" *rtt:%ums, rtt_min:%ums*"),
event->first_rtt, event->last_rtt);
/*3.entry_set_msg将聚合后的消息绑定到当前分析条目analy_entry_t*/
entry_set_msg(e, msg);
return RESULT_CONT;
}
analyzer_result_t analyzer_rtt_exit(trace_t *trace, analy_exit_t *e)
{
rule_run_ret(e->entry, trace, e->event.val);
return RESULT_CONT;
}
2.3.1小节中所提的两个问题:analyzer_rtt是什么?它对应的地址有什么?便得到了解答:
- analyzer_rtt是我们在定义分析器时,描述该分析器rtt的结构体地址,该结构体中包含着当前分析器的entry数据分析函数和exit数据分析函数;
2.3.3 数据处理->数据分析
在第二部分中,我们知道了数据在采集到之后是如何进行数据处理的,也知道在对应的数据处理函数中是如何找到器对应跟踪点所使用的分析器的。在本章中我们了解到分析器时如何定义与实现的,try_run_entry()
函数通过调用提前定义好的analyzer_rtt_entry
和analyzer_rtt_exit
函数,进行真正的数据分析与处理;