我并不觉得丢人,一点也不。
我是说我工作这么多年做和Linux内核相关的事,竟然在上上周才第一次使用tracepoint。
这并不奇怪,我不会的东西还多着呢,比方说,我一直强调的,我不会编程,我也不会用git。
言归正传,如果这么多年我都没用过tracepoint,那么作为替代,我用什么呢?
如果我调试内核或者调试内核模块,最最最常用的方法就是在代码里加一条printk(如果是用户态程序,我就加一条printf。),我来告诉你理由:
所以,我钟爱printk。我不喜欢gdp/crash tool,也不喜欢kprobe/systemtap,更不喜欢bpftrace,当然,我很懂这些东西,只是我觉得它们太复杂,需要记忆的语法太多,就像一个无家可归的流浪汉,不喜欢携带任何负担而已。
扯了这么多,这和tracepoint有毛关系。
有关系,因为我发现在需要添加一条printk的地方添加一个tracepoint工作量差不多,且效果更好。tracepoint更像是添加了一个HOOK,你可以在外面自定义你要做什么,而不仅仅拘泥于printk打印一条消息。
使用tracepoint,如果你只是想达到printk的效果,你可以在/sys/kernel/debug/tracing/trace看到你要打印的信息,如果你还想做点别的,你还可以用eBPF。
类似《史记》太史公曰形而上学的部分在本文最后,现在先给点实际的东西。
在内核代码添加一个tracepoint简单到不必说,只是需要重新编译内核,我给一个在模块里添加tracepoint的Howto吧。
首先给出我要添加tracepoit的内核模块代码:
#include
#include
#include
#define IPPROTO_MYPROTO 123
int myproto_rcv(struct sk_buff *skb)
{
struct udphdr *uh;
struct iphdr *iph;
iph = ip_hdr(skb);
uh = udp_hdr(skb);
printk("proto 123\n");
kfree_skb(skb);
return 0;
}
static const struct net_protocol myproto_protocol = {
.handler = myproto_rcv,
.no_policy = 1,
.netns_ok = 1,
};
int init_module(void)
{
int ret = 0;
ret = inet_add_protocol(&myproto_protocol, IPPROTO_MYPROTO);
if (ret) {
printk("failed\n");
return ret;
}
return 0;
}
void cleanup_module(void)
{
inet_del_protocol(&myproto_protocol, IPPROTO_MYPROTO);
}
int init_module(void);
void cleanup_module(void);
MODULE_LICENSE("GPL");
很现实的例子,这个代码注册了一个和TCP,UDP平级的四层协议,但实际上就是UDP,只是协议号改为了123,我们的预期是当收到这样的协议包时,printk打印出端口。
我现在想用tracepoint替代printk,该怎么办呢?下面是方法。下面是新的代码:
/*
* 注意:
* 1. CREATE_TRACE_POINTS 必须define
* 2. CREATE_TRACE_POINTS 必须在头文件之前define
*/
#define CREATE_TRACE_POINTS
#include "test_tp.h"
#include
#include
#include
#define IPPROTO_MYPROTO 123
int myproto_rcv(struct sk_buff *skb)
{
struct udphdr *uh;
struct iphdr *iph;
iph = ip_hdr(skb);
uh = udp_hdr(skb);
// 这里增加一个tracepoint点
trace_myprot_port(uh->dest, uh->source);
kfree_skb(skb);
return 0;
}
static const struct net_protocol myproto_protocol = {
.handler = myproto_rcv,
.no_policy = 1,
.netns_ok = 1,
};
int init_module(void)
{
int ret = 0;
ret = inet_add_protocol(&myproto_protocol, IPPROTO_MYPROTO);
if (ret) {
printk("failed\n");
return ret;
}
return 0;
}
void cleanup_module(void)
{
inet_del_protocol(&myproto_protocol, IPPROTO_MYPROTO);
}
int init_module(void);
void cleanup_module(void);
MODULE_LICENSE("GPL");
增加了一个头文件引用并且增加了一个宏定义,别的什么都没变,是不是很简单呢。
所有和tracepoint相关的元数据定义都在头文件里:
// test_tp.h
#undef TRACE_SYSTEM
#define TRACE_SYSTEM myprot
#if !defined(_TEST_TRACE_H) || defined(TRACE_HEADER_MULTI_READ)
#define _TEST_TRACE_H
#include
// tracepoint的定义就在这里了。可用的参数都在这里,日后debugfs输出的就是按照下面这个TP_printk的格式来的。
// 如果eBPF来输出,依然要遵循这个格式取参数,下面有例子。
TRACE_EVENT(myprot_port,
TP_PROTO(unsigned short dest, unsigned short source),
TP_ARGS(dest, source),
TP_STRUCT__entry(
__field(unsigned short, dest)
__field(unsigned short, source)
),
TP_fast_assign(
__entry->dest = dest;
__entry->source = source;
),
TP_printk("dest:%d, source:%d", __entry->dest, __entry->source)
);
#endif
/*
* 需要添加以下这一坨
* 由于模块不能修改内核头文件,因此我们需要在自己的目录放头文件,
* TRACE_INCLUDE_PATH 必须重新定义,而不是仅仅在内核头文件目录寻找定义。
*
* 为了支持这个重定义,Makefile中必须包含:
* CFLAGS_myprot.o = -I$(src)
*
*/
#undef TRACE_INCLUDE_PATH
#define TRACE_INCLUDE_PATH .
#define TRACE_INCLUDE_FILE test_tp // 这就是该头文件的名字
#include
接下来是Makefile,我们只需要注意第二行:
obj-m += test.o
# CFLAGS_xx.o 其中 xx 为内核模块的名字
CFLAGS_test.o = -DDEBUG -I$(src)
OK,编译模块,加载之。
首先我们开启该tracepoint:
echo 1 >/sys/kernel/debug/tracing/events/myprot/enable
此时,通过raw socket发送一个协议为123的报文,我们会在debugfs看到输出:
cat /sys/kernel/debug/tracing/trace
# tracer: nop
#
# entries-in-buffer/entries-written: 1/1 #P:4
#
# _-----=> irqs-off
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |
ksoftirqd/1-16 [001] ..s. 174074.362291: myprot_port: dest:36895, source:53764
当然,这只是模拟了printk,这只不过是把printk统一到了一个地方而已,没什么大不了的。tracepoint更有意义的是,它可以挂载eBPF程序。现在我们可以试一下:
bpftrace -l|grep tracepoint.*myprot
tracepoint:myprot:myprot_port
我们可以用bpftrace简单debug一下:
#!/usr/bin/bpftrace
tracepoint:myprot:myprot_port
{
$dest = args->dest;
$source = args->source;
printf("bpftrace:dest:%d source:%d\n",
($source & 0xff) << 8 | ($source & 0xff00) >> 8,
($dest & 0xff) << 8 | ($dest & 0xff00) >> 8);
}
运行之,发包:
./test_bpf_tp.tp
Attaching 1 probe...
bpftrace:dest:1234 source:8080
唯一要吐槽的就是为什么像ntohs这样常用的接口竟然没有builtin!
tracepoint和kprobe/kretprobe相比,优缺点分别是什么呢?为什么我不喜欢kprobe这种,我甚至不喜欢live patch,但这又是为什么。
tracepoint的唯一缺点就是它是静态的,它需要通过写代码的方式静态插入到源码的特定位置,然后重新编译之后执行才能生效,与此相反,kprobe是动态的,它在运行时就可以将HOOK动态attach到特定的位置,但很不幸,这是kprobe唯一的优点。
在任何人面前,我都透露出对kprobe的不满,原因和我对live patch的不满一样,那就是, 为了动态插入哪怕一句printk,维护kprobe/live patch框架的代码执行成本太高了!
你去跟踪一下ftrace框架下调用live patch新函数的代码路径有多长,你去跟踪一下krpobe/kretprobe为了执行handler,做了多少额外的事,我需要的只是handler,而handler里只是printk一些变量的值而已,我不想为了这么小的事去大动干戈,如果调用handler的路径上再遭遇一把spinlock,那事情更会及其尴尬,为了调试性能瓶颈而引入的新瓶颈超过了最初的性能瓶颈本身。在性能优化这么精细的动作上,这种 测不准效应 极为掣肘!
我是手艺人,我对kprobe,live patch这种在我看来完全不可控的东西很难信任。当然我也不在乎那些可以熟练操作这些工具的人的耻笑,我有自己的办法。
如果我想patch一个函数,或者说我想在某处插入一条printk,我会手工去做,很简单,直接在特定位置修改代码为call myhandler即可,当myhandler返回,重新执行被替换的指令,这种方式非常简单且直接,我信任这种方式,当我找到了这种方式并且可以熟练运用的时候,在我看来,tracepoint和kprobe也就没有本质的区别了。
忘掉什么是kprobe,live patch,忘掉tracepoint可能比记得它们更好。
招手拦出租车或比手机打滴滴更轻便,同样,使用纸币支付也无需担心电池续航以及隐私问题…
浙江温州皮鞋湿,下雨进水不会胖。