在 eBPF Talk: 记录一次网络抖动排障 中,遇到了 ethtool -m XXX
导致的网络抖动问题。因为没有趁手的工具直接排查出问题的原因,所以,我写了一个 ethtool
跟踪工具 ethtoolsnoop
。即使 exporter 通过 library 的方式去实现 ethtool -m XXX
,也能跟踪到。
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_CMD
:ethtool
提供给内核执行的 ioctl()
命令或者 genetlink message 命令
ethtool args
: IOCTL_CMD/GENL_CMD
对应的 ethtool
命令行参数(可能不全)
大概翻阅了一下 ethtool
的源代码,发现 ethtool
可以通过 ioctl()
或者 genetlink message 与内核通信。于是,我继续翻阅内核里面 ethtool
的 ioctl()
和 genetlink message 的实现:
所以,对于 ethtool
ioctl()
的跟踪,只需要 kprobe
dev_ethtool()
即可;对于 ethtool
genetlink message 的跟踪,需要 kprobe
ethnl_default_doit()
,同时对 ethnl_default_doit()
进行 kprobe
和 kretprobe
(为了拿到 Interface)。或许对 ethtool
genetlink message 的跟踪不够全面,但已经足够了。
最终,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()
进行 kprobe
和 kretprobe
呢?
因为 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 比较复杂,实现起来并不容易。
尽管 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。