既然实现了 eBPF Talk: 跟踪 IRQ 绑核,那么也实现一下跟踪 RPS/XPS 配置变更吧。

RPS/XPS 是什么?RPS 是 Receive Packet Steering,XPS 是 Transmit Packet Steering。它们是 Linux 内核中的一种机制,用于将网络包分发到多个 CPU 核上,以提高网络包处理性能。参考 Scaling in the Linux Networking Stack

RPS/XPS 配置变更的内核函数

RPS/XPS 配置变更的方式如下:

  1. echo 1 > /sys/class/net/${NET_DEV}/queues/rx-${QUEUE_ID}/rps_cpus
  2. echo 1 > /sys/class/net/${NET_DEV}/queues/tx-${QUEUE_ID}/xps_cpus

所以,直接看这 2 个操作对应的内核源代码 net/core/net-sysfs.c 吧。

找到如下 2 个函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 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 确认一下:

1
2
3
4
5
6
7
8
# 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_mapxps_cpus_store 的源代码,可以看出它们的返回值要么是 err 要么是 len,因而需要判断一下返回值是否小于 0,只有大于等于 0 的情况下才是配置变更成功。

所以推荐使用 fexit,并在 fexit 时判断一下返回值。

因为只需要跟踪某几个网络设备的 RPS/XPS 配置变更,所以在跟踪的时候需要过滤一下 ifindex

无论 RPX 还是 XPS,获取 ifindex 的方式都是一样的,即 queue->dev->ifindex

问题:如何获取 queue 序号?

查看了一下 struct netdev_rx_queuestruct netdev_queue 的定义,发现都没有 queue 序号的字段。

不过,翻看 xps_cpus_store 的源代码,返现内核是如此获取 queue 序号的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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 里是否可以如法炮制呢?试试看吧。

1
2
3
4
5
6
7
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 了:

1
2
$ 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.

好的,根据错误提示改一下吧:

1
2
3
4
5
6
7
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 了:

1
2
3
4
5
6
7
8
$ 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 序号吧。

1
2
3
4
5
6
7
8
9
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);
}
1
q := (event.Queue - event.QueueBase) / uint64(event.QueueSize)

问题:读取 buf?

看到 buflen,使用 bpf_probe_read_kernel() 读取一下吧。

1
2
3
4
5
6
7
8
9
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 了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ 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

好的,根据错误提示改一下吧:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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);
}

最终,跑起来啦:

1
2
3
4
$ 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

总结

跟踪 RPS/XPS 配置变更的方式和跟踪 IRQ 绑核的方式类似,不过使用的是 fexit 而不是 kprobe

在跟踪的时候,需要过滤一下 ifindex,并在 fexit 时判断一下返回值。

因为 verifier 的限制,有些操作无法在 eBPF 里完成,可以放到用户态程序里完成。

最终,解决另一个 verifier 问题后,成功实现了跟踪 RPS/XPS 配置变更。