XDP-2-xdp-tutorial-base

  • xdp-tutorial 教程中 base1-4 笔记

  • 资料来源:

    • https://github.com/xdp-project/xdp-tutorial
  • 更新

    1
    2024.12.05 初始

导语

xdp-tutorial 是非常好的 xdp 教程, 这一节先从入门开始.

流水账, 非翻译, 只记录了部分内容.

Base

01

1
2
3
4
5
SEC("xdp")
int xdp_prog_simple(struct xdp_md *ctx)
{
return XDP_PASS;
}

最简单的 xdp 程序, 通过所有流量.编译后是 xdp_pass_kern.o

1
2
3
4
5
6
7
8
9
10
❯ llvm-objdump -S xdp_pass_kern.o

xdp_pass_kern.o: file format elf64-bpf

Disassembly of section xdp:

0000000000000000 <xdp_prog_simple>:
; return XDP_PASS;
0: b7 00 00 00 02 00 00 00 r0 = 2
1: 95 00 00 00 00 00 00 00 exit
  • section xdp 的名字是 SEC("xdp")

加载 xdp 程序当然可以使用 ip 命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 通用模式加载
sudo ip link set dev lo xdpgeneric obj xdp_pass_kern.o sec xdp

❯ ip link show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
prog/xdp id 57
❯ sudo ip link show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
prog/xdp id 57 tag 3b185187f1855c4c jited

# 卸载
sudo ip link set dev lo xdpgeneric off

ip 命令加载是直接加载到 nic, 因此一次 nic 只能加载一个 xdp 程序, 但是 xdp-loader 实现了 xdp nult-dispatch 更加强大更加丰富

1
2
3
4
5
6
7
8
❯ sudo xdp-loader load -m skb lo xdp_pass_kern.o
❯ sudo xdp-loader status lo
CURRENT XDP PROGRAM STATUS:

Interface Prio Program name Mode ID Tag Chain actions
--------------------------------------------------------------------------------------
lo xdp_dispatcher skb 67 90f686eb86991928
=> 50 xdp_prog_simple 76 3b185187f1855c4c XDP_PASS

自然也有使用用户程序加载, 简单来说是 libxdp

  • xdp_program__create -> xdp_program__attach
  • xdp_program__fd 获取程序的 fd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
❯ sudo ./xdp_pass_user --dev lo
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
Success: Loading XDP prog name:xdp_prog_simple(id:95) on device:lo(ifindex:1)
❯ sudo ./xdp_pass_user -d lo
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
libbpf: elf: skipping unrecognized data section(7) xdp_metadata
Success: Loading XDP prog name:xdp_prog_simple(id:114) on device:lo(ifindex:1)
❯ xdp-loader status lo
This program must be run as root.
❯ sudo xdp-loader status lo
CURRENT XDP PROGRAM STATUS:

Interface Prio Program name Mode ID Tag Chain actions
--------------------------------------------------------------------------------------
lo xdp_dispatcher skb 105 90f686eb86991928
=> 50 xdp_prog_simple 95 3b185187f1855c4c XDP_PASS
=> 50 xdp_prog_simple 114 3b185187f1855c4c XDP_PASS
  • 这里警告似乎是无所谓;
  • 两次加载的 Prio 是完全相同的, 使用其他加载方式也是一样
1
2
3
4
# 卸载 通过 fd
sudo ./xdp_pass_user --dev lo -U 745
# 卸载全部
sudo ./xdp_pass_user --dev lo --unload-all
  • xdp_multiprog__detach 卸载全部的 api

02

一个 elf 文件可以包含多个 xdp 程序, 使用 libxdp 选择需要加载的程序.

libbpf libxdp 提供了系统调用的封装和对应的操作 api

1
2
3
4
5
6
7
8
9
struct bpf_object
struct bpf_program
struct bpf_map

struct xdp_program
struct xdp_multiprog
# xdp 相关
struct xsk_umem
struct xsk_socket

在 libxdp 内附加到一个 nic 的 xdp 程序以 struct xdp_program 表示;

  • xdp_program_opts
  • xdp_program__create() 创建
  • xdp_program__bpf_obj(prog) 转换为 bpf_obj
1
2
3
4
5
6
// filename 和 progname
DECLARE_LIBBPF_OPTS(bpf_object_open_opts, opts);
DECLARE_LIBXDP_OPTS(xdp_program_opts, xdp_opts,
.open_filename = cfg->filename,
.prog_name = cfg->progname,
.opts = &opts);

02

先初始化程序

1
2
3
4
# 创建测试 xdp 程序的 veth
sudo ../testenv/testenv.sh enter --name veth-basic02
# ping 可以测试环境
ping fc00:dead:cafe:1::1
  • testenv.sh 可以做到的事情实在是太多了. 初始化多个测试环境, 切换不同的测试环境 xxx
  • 应该是通过不同的网络命名空间隔离, 使用 veth 连接;

xdp_prog_kern.c 是使用 libxdp 加载卸载 xdp 程序的示例, 几个需要留意的点

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
// 遍历 bpf_object 输出有效的 xdp 程序段
static void list_avail_progs(struct bpf_object *obj)
{
struct bpf_program *pos;

printf("BPF object (%s) listing available XDP functions\n",
bpf_object__name(obj));

bpf_object__for_each_program(pos, obj) {
if (bpf_program__type(pos) == BPF_PROG_TYPE_XDP)
printf(" %s\n", bpf_program__name(pos));
}
}

/** 加载 bpf_obj, 通过 prog_name 加载一个 elf 文件中的不同的 xdp 程序
*/
// 通过 filename 加载 bpf_obj
DECLARE_LIBBPF_OPTS(bpf_object_open_opts, bpf_opts);
obj = bpf_object__open_file(cfg.filename, &bpf_opts);
// 这里是 prog_name 和传入的 bpf_obj
// 没有 `open_filename`
DECLARE_LIBXDP_OPTS(xdp_program_opts, xdp_opts,
.obj = obj,
.prog_name = cfg.progname);
struct xdp_program *prog = xdp_program__create(&xdp_opts);

03

小结: 本节介绍了 bpf 程序中存储机制, 如何在 kernel userspace 中共享/修改数据.

  • BPF_MAP_TYPE_ARRAYBPF_MAP_TYPE_PERCPU_ARRAY 两种类型的 bpf map
  • 两个实际的例子

定义数据结构, 这个部分在 bpf 程序中

1
2
3
4
5
6
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__type(key, __u32);
__type(value, struct datarec);
__uint(max_entries, XDP_ACTION_MAX);
} xdp_stats_map SEC(".maps");

BPF_MAP_TYPE_ARRAY 数组

  • 固定长度 预先分配, 32 整数索引, 访问 O(1)
  • 多个 cpu 共享无碍

struct bpf_object 代表了 elf 文件和其全部内容, map 也在其中. 通过名称找到 map 再找到 map 的 fd

  • find_map_fd -> bpf_map__fd = bpf_object__find_map_fd_by_name

用户空间读取 map:

  • bpf_map_lookup_elem: 将值存入用户传入指针 (内核也能调用, 两者实现有一些区别)
  • bpf_obj_get_info_by_fd: 查看 map 本身的属性

字节计数器

数据结构定义在 bpf 程序中

1
2
3
4
5
6
7
8
9
10
11
struct datarec {
__u64 rx_packets;
/* Assignment#1: Add byte counters */
};

struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__type(key, __u32);
__type(value, struct datarec);
__uint(max_entries, XDP_ACTION_MAX); // 这里定义的长度是和 xpd_action 对应的. 读取时候就是 传入 xpd_pass xxxx
} xdp_stats_map SEC(".maps");

elf 文件中不存在 map 中 value 的具体类型只有 value 的长度, 类型安全由 用户 or 内核包装.

xdp 中获取 pkt 的长度 -> struct xdp_md, 这个结构体有点意思: (Note: type __u32 is NOT the real-type)

  • int xdp_prog_simple(struct xdp_md *ctx)
1
2
3
4
5
6
7
8
9
struct xdp_md {
// (Note: type __u32 is NOT the real-type)
__u32 data;
__u32 data_end;
__u32 data_meta;
/* Below access go through struct xdp_rxq_info */
__u32 ingress_ifindex; /* rxq->dev->ifindex */
__u32 rx_queue_index; /* rxq->queue_index */
};
1
2
3
4
5
6
// 真正访问时需要转换为真正的数据类型
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;

__u64 bytes = data_end - data; // 所以 pkt 的字节长度就是
lock_xadd(&rec->rx_bytes, bytes); // 加到真正的计数上
  • lock_xadd 是 bpf 提供的原子操作函数

用户空间读取不再赘述.

Pre CPU 统计

lock_xadd 的时间成本很高, 原子操作 + 内存屏障;

更好的做法是: pre cpu, 每个 cpu 都有一个数据的副本, 读写都是在自己的副本上;

1
2
3
4
5
6
7
8
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__type(key, __u32);
__type(value, struct datarec);
__uint(max_entries, XDP_ACTION_MAX); // 这里定义的长度是和 xpd_action 对应的. 读取时候就是 传入 xpd_pass xxxx
} xdp_stats_map SEC(".maps");

rec->rx_bytes += 1; // 直接操作而不需要使用原子操作

求和的成本就转移到了 userspace;

对于 BPF_MAP_TYPE_PERCPU_ARRAY 调用 bpf_map_lookup_elem 返回的是这个 数据类型 * cpu 数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* For percpu maps, user space gets a value per possible CPU */
unsigned int nr_cpus = libbpf_num_possible_cpus();
struct datarec values[nr_cpus]; // 这里承载数据的是 数组!!
__u64 sum_bytes = 0;
__u64 sum_pkts = 0;
int i;
// 读取数据
if ((bpf_map_lookup_elem(fd, &key, values)) != 0) {
fprintf(stderr,
"ERR: bpf_map_lookup_elem failed key:0x%X\n", key);
return;
}

/* Sum values from each CPU */
for (i = 0; i < nr_cpus; i++) {
sum_pkts += values[i].rx_packets;
sum_bytes += values[i].rx_bytes;
}
value->rx_packets = sum_pkts;
value->rx_bytes = sum_bytes;

04

03 节中对用户空间读取 bpf map 还有一个隐形限制: 必须是加载 bpf 的程序才能读取到 map.

破局的关键: 可以将 bpf map 挂载到文件系统,公开访问;

base-04 将 03 的程序进一步划分成了:

  • xdp_loader.c
  • xdp_stats.c: 如果是仅读取 map 甚至不需要 libxdp, libbpf 就足够了.

将 bpf_map pin 到文件系统 -> bpf_object__pin_maps (struct bpf_object *obj, const char *path)

  • 与其他 libxdp 的函数一样, 包接包送,一学就会.
  • 传入的子路径如何不存在, 这个函数也会直接给你创建;

bpf_map 的挂载流程 -> pin_maps_in_bpf_object

  • 多了对程序启动前遗留状态的处理;
  • 在子路径已存在文件, 就先 int bpf_object__unpin_maps(struct bpf_object *obj, const char *path) 卸载
  • 最后挂上 map

原文特别提了一点: xdp_loader.c 的挂载并不会重用已存在的 map

  • 说人话就是,一个 map 你通过 xdp_loader.c 挂载两次,已有的 map 就删掉了, 第二次是一个新创建的 map;
  • 但是通过 iproute2 命令不会, 第二次挂载时会尝试将 已有的 map 关联到 bpf 程序上, 即使 bpf 程序逻辑变了, 只要 map 定义不变, 已有数据在新 bpf 程序中就能得到保留.

那么如何在 xdp_loader.c 中实现 iproute2 的效果? 教程给出了示例:

1
2
3
4
5
6
7
8
9
10
// 找到已有的 map 的 fd
int pinned_map_fd = bpf_obj_get("/sys/fs/bpf/veth0/xdp_stats_map");
// 打开 bpf 程序
struct bpf_object *obj = bpf_object__open(cfg.filename);
// 查找 bpf 程序中对应的 "xdp_stats_map"
struct bpf_map *map = bpf_object__find_map_by_name(obj, "xdp_stats_map");
// 重点是这一步: bpf 对象关联到了已有的 map_fd
bpf_map__reuse_fd(map, pinned_map_fd);
// 重新加载修改后的 bpf 对象
bpf_object__load(obj);

这一节内容可能比较适合对 bpf 程序的监控? maybe

尾巴

这一篇其实也出现了拖延症, 但是只看短期目标还是好的, 一个一个子任务无限拆解, 快速反馈.这样才是自己的节奏.