eBPF Talk: 记录一次网络抖动排障 中,遇到了 ethtool -m XXX 导致的网络抖动问题。因为没有趁手的工具直接排查出问题的原因,所以,我写了一个 ethtool 跟踪工具 ethtoolsnoop。即使 exporter 通过 library 的方式去实现 ethtool -m XXX,也能跟踪到。

ethtoolsnoop 用法

1
2
3
4
5
6
# echo Execute `ethtool -i enp0s1; ethtool -l enp0s1; ethtool -g enp0s1` in another terminal.
# ./ethtoolsnoop
Interface             PID:Process                          IOCTL_CMD/GENL_CMD             ethtool args
enp0s1              11198:ethtool(parent 6373:zsh)         ETHTOOL_GDRVINFO               -d|--register-dump(Do a register dump), -e|--eeprom-dump(Do a EEPROM dump), -i|--driver(Show driver information)
enp0s1              11199:ethtool(parent 6373:zsh)         ETHTOOL_MSG_CHANNELS_GET       -l|--show-channels(Query Channels)
enp0s1              11200:ethtool(parent 6373:zsh)         ETHTOOL_MSG_RINGS_GET          -g|--show-ring(Query RX/TX ring parameters)

在输出中,

  • Interface:网卡名称,不区分 netns
  • PID:Process:执行 ethtool 的进程信息;如果是 ethtool 本身,同时给出执行 ethtool 命令的父进程信息
  • IOCTL_CMD/GENL_CMDethtool 提供给内核执行的 ioctl() 命令或者 genetlink message 命令
  • ethtool args: IOCTL_CMD/GENL_CMD 对应的 ethtool 命令行参数(可能不全)

ethtoolsnoop 实现

大概翻阅了一下 ethtool 的源代码,发现 ethtool 可以通过 ioctl() 或者 genetlink message 与内核通信。于是,我继续翻阅内核里面 ethtoolioctl() 和 genetlink message 的实现:

所以,对于 ethtool ioctl() 的跟踪,只需要 kprobe dev_ethtool() 即可;对于 ethtool genetlink message 的跟踪,需要 kprobe ethnl_default_doit(),同时对 ethnl_default_doit() 进行 kprobekretprobe(为了拿到 Interface)。或许对 ethtool genetlink message 的跟踪不够全面,但已经足够了。

ethtoolsnoop 源代码

最终,ethtoolsnoop 的 bpf 代码比较简单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
static __always_inline u32
get_ethcmd(void *useraddr)
{
    u32 cmd;

    bpf_probe_read_user(&cmd, sizeof(cmd), useraddr);
    if (cmd == ETHTOOL_PERQUEUE)
        cmd = bpf_probe_read_user(&cmd, sizeof(cmd), useraddr + sizeof(cmd));

    return cmd;
}

static __always_inline int
__kp_dev_ethtool(void *ctx, struct net *net, struct ifreq *ifr, void *useraddr)
{
    struct event ev = {};

    ev.type = EVENT_TYPE_IOCTL;
    ev.ethcmd = get_ethcmd(useraddr);

    ev.pid = bpf_get_current_pid_tgid() >> 32;

    bpf_probe_read_kernel_str(ev.ifname, sizeof(ev.ifname), ifr->ifr_ifrn.ifrn_name);
    bpf_get_current_comm(ev.comm, sizeof(ev.comm));

    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &ev, sizeof(ev));

    return BPF_OK;
}

SEC("kprobe/dev_ethtool")
int kp_dev_ethtool(struct pt_regs *ctx)
{
    struct net *net = (typeof(net))(void *)(u64) PT_REGS_PARM1(ctx);
    struct ifreq *ifr = (typeof(ifr))(void *)(u64) PT_REGS_PARM2(ctx);
    void *useraddr = (typeof(useraddr))(void *)(u64) PT_REGS_PARM3(ctx);
    return __kp_dev_ethtool(ctx, net, ifr, useraddr);
}

static __always_inline void
__get_dev_name(struct event *ev, struct ethnl_req_info *req)
{
    struct net_device *dev = BPF_CORE_READ(req, dev);

    if (likely(dev))
        bpf_probe_read_kernel_str(ev->ifname, sizeof(ev->ifname), dev->name);
}

SEC("kprobe/ethnl_default_doit")
int kp_ethnl_doit(struct pt_regs *ctx)
{
    struct genl_info *info = (typeof(info))(void *)(u64) PT_REGS_PARM2(ctx);
    u8 cmd = BPF_CORE_READ(info, genlhdr, cmd);
    struct event *ev = __get_or_init_event();

    if (unlikely(!ev))
        return BPF_OK;

    ev->type = EVENT_TYPE_GENL;
    ev->genlhdr_cmd = cmd;
    ev->req = NULL;

    return BPF_OK;
}

SEC("kprobe/ethnl_parse_header_dev_get")
int kp_ethnl_dev(struct pt_regs *ctx)
{
    struct ethnl_req_info *req = (typeof(req))(void *)(u64) PT_REGS_PARM1(ctx);
    struct event *ev = __get_event();

    if (unlikely(!ev))
        return BPF_OK;

    ev->req = req;

    return BPF_OK;
}

SEC("kretprobe/ethnl_parse_header_dev_get")
int krp_ethnl_dev(struct pt_regs *ctx)
{
    struct event *ev = __get_and_del_event();

    if (unlikely(!ev))
        return BPF_OK;

    if (likely(ev->req))
        __get_dev_name(ev, ev->req);

    ev->pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(ev->comm, sizeof(ev->comm));

    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, ev, SIZEOF_EVENT);

    return BPF_OK;
}

P.S. 完整 bpf 代码在 GitHub ethtoolsnoop ethtool.c

其中,为什么需要对 ethnl_parse_header_dev_get() 进行 kprobekretprobe 呢?

因为 ethnl_parse_header_dev_get() 会解析 genetlink message,并将解析得到的网络设备信息保存到 struct ethnl_req_info 里面的 struct net_device *dev。所以,需要在 kprobe ethnl_parse_header_dev_get() 时,将 struct ethnl_req_info 缓存一下,然后在 kretprobe ethnl_parse_header_dev_get() 时,从缓存中取出 struct ethnl_req_info,并从中取出 struct net_device *dev,然后再从 struct net_device *dev 中取出网卡名称。

同时,也是因为使用 bpf 解析 genetlink message 比较复杂,实现起来并不容易。

ethtoolsnoop 实现时踩的坑

尽管 bpf 代码比较简单,但其 Go 代码实现时,还是踩了一个大坑。

为了拿到 IOCTL_CMD/GENL_CMD 这些 cmd 对应的名称,我“复制”了它们的定义,然后在 Go 代码中进行了映射时,就踩在“复制”这一步;在 IOCTL_CMD 中,有一个断点 0x0000000d,这个断点在内核中是没有使用的,所以在 Go 中使用 []string 进行映射时,就未能一一映射上;而且,0x00000000 也没有被使用。

然后,为了提升 ethtoolsnoop 的使用效果,还需要将 IOCTL_CMD/GENL_CMD 映射到 ethtool 命令行参数上。但未能全部一一映射起来,毕竟是个脏活累活。

小结

ethtoolsnoop 的实现比较简单,但也有一些坑需要注意。如果你有更好的想法,欢迎在 GitHub ethtoolsnoop 上提 issue 或者 PR。