既然实现了 eBPF Talk: 跟踪 IRQ 绑核,那么也实现一下跟踪 RPS/XPS 配置变更吧。
RPS/XPS 是什么?RPS 是 Receive Packet Steering,XPS 是 Transmit Packet Steering。它们是 Linux 内核中的一种机制,用于将网络包分发到多个 CPU 核上,以提高网络包处理性能。参考 Scaling in the Linux Networking Stack[1]。
RPS/XPS 配置变更的内核函数
RPS/XPS 配置变更的方式如下:
echo 1 > /sys/class/net/${NET_DEV}/queues/rx-${QUEUE_ID}/rps_cpus
。echo 1 > /sys/class/net/${NET_DEV}/queues/tx-${QUEUE_ID}/xps_cpus
。
所以,直接看这 2 个操作对应的内核源代码 net/core/net-sysfs.c
吧。
找到如下 2 个函数:
// https://github.com/torvalds/linux/blob/master/net/core/net-sysfs.c
static ssize_t store_rps_map(struct netdev_rx_queue *queue,
const char *buf, size_t len)
{
// ...
}
static ssize_t xps_cpus_store(struct netdev_queue *queue,
const char *buf, size_t len)
{
// ...
}
使用 bpftrace
确认一下:
# bpftrace -l 'k:store_rps_map'
kprobe:store_rps_map
# bpftrace -l 'k:xps_cpus_store'
kprobe:xps_cpus_store
# bpftrace -e 'k:store_rps_map, k:xps_cpus_store { printf("rps/xps: %s\n", comm); }'
Attaching 2 probes...
跟踪 RPS/XPS 配置变更函数
翻看 store_rps_map
和 xps_cpus_store
的源代码,可以看出它们的返回值要么是 err
要么是 len
,因而需要判断一下返回值是否小于 0,只有大于等于 0 的情况下才是配置变更成功。
所以推荐使用 fexit
,并在 fexit
时判断一下返回值。
因为只需要跟踪某几个网络设备的 RPS/XPS 配置变更,所以在跟踪的时候需要过滤一下 ifindex
。
无论 RPX 还是 XPS,获取 ifindex
的方式都是一样的,即 queue->dev->ifindex
。
问题:如何获取 queue 序号?
查看了一下 struct netdev_rx_queue
和 struct netdev_queue
的定义,发现都没有 queue 序号的字段。
不过,翻看 xps_cpus_store
的源代码,返现内核是如此获取 queue 序号的:
// https://github.com/torvalds/linux/blob/master/net/core/net-sysfs.c
static unsigned int get_netdev_queue_index(struct netdev_queue *queue)
{
struct net_device *dev = queue->dev;
unsigned int i;
i = queue - dev->_tx;
BUG_ON(i >= dev->num_tx_queues);
return i;
}
static ssize_t xps_cpus_store(struct netdev_queue *queue,
const char *buf, size_t len)
{
// ...
index = get_netdev_queue_index(queue);
// ...
}
所以,在 eBPF 里是否可以如法炮制呢?试试看吧。
static __always_inline void
get_netdev_rx_queue_index(struct event *event, struct netdev_rx_queue *queue)
{
struct netdev_rx_queue *_rx = BPF_CORE_READ(queue, dev, _rx);
event->queue = queue - _rx;
}
然后就 GG 了:
$ go generate
Error at line 47: Unsupport signed division for DAG: 0x5f0a9b732cb0: i64 = sdiv exact 0x5f0a9b732bd0, Constant:i64<192>, ./rpsxps.c:47:26 @[ ./rpsxps.c:66:5 @[ ./rpsxps.c:55:5 ] ]Please convert to unsigned div/mod.
好的,根据错误提示改一下吧:
static __always_inline void
get_netdev_rx_queue_index(struct event *event, struct netdev_rx_queue *queue)
{
struct netdev_rx_queue *_rx = BPF_CORE_READ(queue, dev, _rx);
event->queue = ((void *) queue - (void *) _rx) / (sizeof(*queue));
}
然后,还是 GG 了:
$ sudo ./fexit_rpsxps
2024/06/23 05:21:19 Failed to load bpf obj: field FexitStoreRpsMap: program fexit_store_rps_map: load program: invalid argument: math between ptr_ pointer and register with unbounded min value is not allowed (65 line(s) omitted)
load program: invalid argument:
...
; event->queue = ((void *) queue - (void *) _rx) / (sizeof(*queue));
48: (1f) r9 -= r1
math between ptr_ pointer and register with unbounded min value is not allowed
processed 49 insns (limit 1000000) max_states_per_insn 0 total_states 2 peak_states 2 mark_read 2
经过一番折腾,还是没解决这个 verifier 错误;所以,只能放弃这个思路了。
改道到用户态程序里来计算 queue 序号吧。
static __always_inline void
get_netdev_rx_queue_index(struct event *event, struct netdev_rx_queue *queue)
{
struct netdev_rx_queue *_rx = BPF_CORE_READ(queue, dev, _rx);
event->queue = (__u64)(void *)queue;
event->queue_base = (__u64)(void *)_rx;
event->queue_size = sizeof(*queue);
}
q := (event.Queue - event.QueueBase) / uint64(event.QueueSize)
问题:读取 buf?
看到 buf
和 len
,使用 bpf_probe_read_kernel()
读取一下吧。
static __always_inline void
handle_event(void *ctx, struct event *event, const char *buf, size_t len)
{
bpf_probe_read_kernel(&event->cpus, len, buf);
event->cpus_len = len;
event->pid = bpf_get_current_pid_tgid() >> 32;
push_event(ctx, event);
}
然后也 GG 了:
$ sudo ./fexit_rpsxps
2024/06/23 05:29:31 Failed to load bpf obj: field FexitStoreRpsMap: program fexit_store_rps_map: load program: permission denied: 58: (85) call bpf_probe_read_kernel#113: R2 min value is negative, either use unsigned or 'var &= const' (78 line(s) omitted)
load program: permission denied:
...
; bpf_probe_read_kernel(&event->cpus, len, buf);
55: (07) r1 += -88 ; R1_w=fp-88
56: (bf) r2 = r7 ; R2_w=scalar(id=1) R7=scalar(id=1)
57: (79) r3 = *(u64 *)(r10 -96) ; R3_w=scalar() R10=fp0 fp-96=mmmmmmmm
58: (85) call bpf_probe_read_kernel#113
R2 min value is negative, either use unsigned or 'var &= const'
processed 59 insns (limit 1000000) max_states_per_insn 0 total_states 2 peak_states 2 mark_read 2
好的,根据错误提示改一下吧:
static __always_inline void
handle_event(void *ctx, struct event *event, const char *buf, size_t len)
{
int length = len & (sizeof(event->cpus) - 1);
bpf_probe_read_kernel(&event->cpus, length, buf);
event->cpus_len = len;
event->pid = bpf_get_current_pid_tgid() >> 32;
push_event(ctx, event);
}
最终,跑起来啦:
$ sudo ./fexit_rpsxps
2024/06/23 05:35:33 Attached fexit/store_rps_map
2024/06/23 05:35:33 Attached fexit/xps_cpus_store
2024/06/23 05:35:35 RPS: ens33(2) Queue: 0 Process: /usr/bin/zsh(4057964) Cpus: 0x0a
完整的源代码:fexit_rpsxps[2]。
总结
跟踪 RPS/XPS 配置变更的方式和跟踪 IRQ 绑核的方式类似,不过使用的是 fexit
而不是 kprobe
。
在跟踪的时候,需要过滤一下 ifindex
,并在 fexit
时判断一下返回值。
因为 verifier 的限制,有些操作无法在 eBPF 里完成,可以放到用户态程序里完成。
最终,解决另一个 verifier 问题后,成功实现了跟踪 RPS/XPS 配置变更。
Scaling in the Linux Networking Stack: https://www.kernel.org/doc/html/latest/networking/scaling.html
[2]fexit_rpsxps: https://github.com/Asphaltt/learn-by-example/tree/main/ebpf/fexit_rpsxps