第一次使用Linux内核的Tracepoint的体验

我并不觉得丢人,一点也不。

我是说我工作这么多年做和Linux内核相关的事,竟然在上上周才第一次使用tracepoint。

这并不奇怪,我不会的东西还多着呢,比方说,我一直强调的,我不会编程,我也不会用git。

言归正传,如果这么多年我都没用过tracepoint,那么作为替代,我用什么呢?

如果我调试内核或者调试内核模块,最最最常用的方法就是在代码里加一条printk(如果是用户态程序,我就加一条printf。),我来告诉你理由:

  • printk/printf 不需要安装任何别的依赖包。
  • printk/printf 随手可用,不需要写太多为了重用而过度设计的代码。
  • 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可能比记得它们更好。

招手拦出租车或比手机打滴滴更轻便,同样,使用纸币支付也无需担心电池续航以及隐私问题…


浙江温州皮鞋湿,下雨进水不会胖。

你可能感兴趣的:(tracepoint)