【翻译】 XDP的力量

https://blogs.oracle.com/linux/the-power-of-xdp

 

XDP的力量

Oracle Linux内核开发人员Alan Maguire谈到了XDP,即eXpress DataPath,它使用BPF来加速数据包处理。有关BPF的更多背景信息,请参阅BPF系列文章,其中他深入介绍了内核的“伯克利包过滤器”(Berkeley Packet Filter),它是有用且可扩展的内核功能,其功能远不止包过滤。

 

 

[重要提示:BPF博客系列提到了4.14内核中可用的BPF功能。此处描述的功能大部分也存在于该内核中,但是示例程序中使用的一些libbpf函数和xdp_md元数据结构的布局已更改,在这里我们参考最新的(从5.2内核开始)。

在以前的博客文章中,我对BPF进行了一般性描述,并将BPF概念应用于构建tc-bpf程序。在这种情况下,此类程序将附加到tc入口和出口挂钩,并在那里执行数据包转换和其他活动。

但是,这种处理是在分配了数据包元数据之后发生的,在Linux中,这是“ struct sk_buff”。因此,BPF可以在较早的干预点运行。

XDP的目标是在与现有内核网络堆栈配合工作的同时,提供与内核旁路解决方案相当的性能。例如,我们可以直接使用XDP丢弃或转发数据包,或者简单地将它们通过网络堆栈传递以进行常规处理。

XDP元数据

如BPF系列第一篇文章所述,XDP允许我们在包接收代码路径的早期附加BPF程序。设计的重点是最大程度地减少开销,因此每个数据包都使用最小的元数据描述符:

 
1个
2
3
4
5
6
7
8
9
10
11
/* user accessible metadata for XDP packet hook
 * new fields must be added to the end of this structure
 */
struct xdp_md {
        __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  */
};

将此与struct sk_buff定义进行对比,如下所示:

https://www.netdevconf.org/2.2/slides/miller-datastructurebloat-keynote.pdf

每个sk_buff要求分配至少216个字节的元数据。这转化为可观察的性能成本。

XDP程序执行

XDP有两种风格:

  • 本机XDP需要驱动程序支持,并且在分配sk_buffs之前先处理数据包。这使我们可以实现最小化元数据描述符的好处。该挂钩包含对bpf_prog_run_xdp的调用,并且在调用此函数后,驱动程序必须处理可能的返回值-有关这些值的说明,请参见下文。例如,bnxt_rx_pkt函数调用bnxt_rx_xdp,后者依次验证是否已为RX环加载了XDP程序,如果是,则设置元数据缓冲区并调用bpf_prog_run_xdp。bnxt_rx_pkt直接从设备轮询功能调用,因此通过net_rx_action调用,用于中断处理和轮询;简而言之,我们将尽快在接收代码路径中使用数据包。

  • 通用XDP,在分配了sk_buff之后从网络堆栈中调用XDP挂钩。通用XDP允许我们使用XDP的好处-尽管性能成本稍高-无需底层驱动程序支持。在这种情况下,bpf_prog_run_xdp是通过netdev的netif_receive_generic_xdp函数调用的;即在分配和设置skb之后。为了确保XDP处理能够正常工作,必须将skb线性化(使其连续而不是在数据片段中分块)-同样,这可能会降低性能。

XDP动作

XDP程序可以通过返回以下内容来表示所需的行为

  • XDP_DROP:使用XDP进行丢弃的速度很快,缓冲区只是回收到rx环形队列
  • XDP_PASS:可能在修改后传递到普通网络堆栈
  • XDP_TX:修改包后,发送相同的NIC包
  • XDP_REDIRECT:使用XDP程序中的XDP_REDIRECT操作,该程序可以将入口帧重定向到其他启用XDP的netdev

要为驱动程序添加对XDP的支持,需要添加调用bpf_prog_run_xdp的接收挂钩并处理各种结果,并添加将缓冲区环专用于XDP的设置/拆卸功能。

一个例子-xdping

通过以上操作,以及希望将每个数据包开销降至最低的愿望,我们可以看到诸如减少分布式拒绝服务和负载平衡之类的用例是有意义的。为了帮助说明XDP中的关键概念,在这里我们提供一个完整的示例。最近的bpf-next内核中提供了该示例;看到

https://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git/tree/tools/testing/selftests/bpf/xdping.c

...用于用户空间程序;

https://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git/tree/tools/testing/selftests/bpf/xdping.h

...用于共享标题;和

https://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git/tree/tools/testing/selftests/bpf/progs/xdping_kern.c

...用于BPF程序。

xdping是一个C程序,它使用XDP,BPF映射和ping程序以类似于ping的方式测量往返时间(RTT),但是使用xdping时,我们从XDP本身测量往返时间,而不是调用所有其他IP,ICMP层以及用户空间与内核的交互层。这个想法是,通过呈现在XDP中测量的往返时间与通过传统ping测量的往返时间,我们可以

  • 看看XDP中有多少处理流量直接可以在响应延迟方面为我们节省
  • 消除了由于附加处理层而造成的RTT变化

xdping可以在客户端或服务器模式下运行。

  • 作为客户端,它负责生成ICMP请求并接收ICMP答复,测量RTT并将结果保存在BPF映射中。它通过接收ping生成的ICMP答复,然后将其转换回ICMP请求,记录时间并发送来完成此任务。收到回复后,可以计算RTT
  • 作为服务器,它负责接收ICMP请求,并将其变回答复

注意,由于XDP是接收驱动的,因此上述方法是必需的。即XDP挂钩在接收代码路径中。使用AF_XDP-我们的下一个XDP博客文章的主题-也可以传输,但是这里我们坚持使用核心XDP。

让我们看看程序是什么样子!

 
1个
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ./xdping -I eth4 192.168.55.7
Setting up xdp for eth4, please wait...
Normal ping RTT data:
PING 192.168.55.7 (192.168.55.7) from 192.168.55.8 eth4: 56(84) bytes of data.
64 bytes from 192.168.55.7: icmp_seq=1 ttl=64 time=0.206 ms
64 bytes from 192.168.55.7: icmp_seq=2 ttl=64 time=0.165 ms
64 bytes from 192.168.55.7: icmp_seq=3 ttl=64 time=0.162 ms
64 bytes from 192.168.55.7: icmp_seq=8 ttl=64 time=0.470 ms
--- 192.168.55.7 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3065ms
rtt min/avg/max/mdev = 0.162/0.250/0.470/0.129 ms
XDP RTT data:
64 bytes from 192.168.55.7: icmp_seq=5 ttl=64 time=0.03003 ms
64 bytes from 192.168.55.7: icmp_seq=6 ttl=64 time=0.02665 ms
64 bytes from 192.168.55.7: icmp_seq=7 ttl=64 time=0.02453 ms
64 bytes from 192.168.55.7: icmp_seq=8 ttl=64 time=0.02633 ms

注意-与ping可选的不同-我们必须指定一个用于ping的接口;我们需要知道在哪里加载XDP程序。还要注意,来自XDP的RTT测量比ping报告的测量显着更快。现在ping支持时间戳记,网络堆栈处理可以使用IP时间戳记来获取更准确的数字,但并非所有系统都启用了时间戳记。

最后注意另一件事;每个ICMP回显数据包都有一个关联的序列号,我们在ping输出中看到了这些序列号。但是请注意,最终的icmp_seq = 8而不是我们期望的4。这是因为我们的XDP程序收到了第4条答复,将其重写为具有序列号5的请求并将其发送出去。然后,当收到答复并测量RTT时,它对序列号6再次执行相同操作,依此类推,直到得到第8条答复,才意识到它具有所需的所有编号(通过defalt我们执行了4个请求,可以更改使用“ -c count”选项进行xdping),而不是返回XDP_TX(“发送此修改后的数据包”),程序将返回XDP_PASS(“将此数据包传递到网络堆栈”)。因此,ping程序最终会看到ICMP答复数字8,即输出。

为了存储RTT,我们需要一个通用的数据结构来存储在BPF映射中,我们将使用目标(远程)IP地址进行密钥输入。xdping.h可以存储此信息,并包含在用户空间和内核程序中:

 
1个
2
3
4
5
6
7
8
9
10
11
12
13
/* SPDX-License-Identifier: GPL-2.0 */
/* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. */
 
#define XDPING_MAX_COUNT        10
#define XDPING_DEFAULT_COUNT    4
 
struct pinginfo {
        __u64   start;
        __be16  seq;
        __u16   count;
        __u32   pad;
        __u64   times[XDPING_MAX_COUNT];
};

我们存储要发出的ICMP请求的数量(“计数”),当前请求的开始时间(“开始”),当前序列号(“ seq”)和RTT(“时间”)。

接下来,这是BPF程序xdping_kern.c的ping客户端代码的实现:

 
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
SEC("xdpclient")
int xdping_client(struct xdp_md *ctx)
{
        void *data_end = (void *)(long)ctx->data_end;
        void *data = (void *)(long)ctx->data;
        struct pinginfo *pinginfo = NULL;
        struct ethhdr *eth = data;
        struct icmphdr *icmph;
        struct iphdr *iph;
        __u64 recvtime;
        __be32 raddr;
        __be16 seq;
        int ret;
        __u8 i;
 
        ret = icmp_check(ctx, ICMP_ECHOREPLY);
 
        if (ret != XDP_TX)
                return ret;
 
        iph = data + sizeof(*eth);
        icmph = data + sizeof(*eth) + sizeof(*iph);
        raddr = iph->saddr;
 
        /* Record time reply received. */
        recvtime = bpf_ktime_get_ns();
        pinginfo = bpf_map_lookup_elem(&ping_map, &raddr);
        if (!pinginfo || pinginfo->seq != icmph->un.echo.sequence)
                return XDP_PASS;
 
        if (pinginfo->start) {
#pragma clang loop unroll(full)
                for (i = 0; i < XDPING_MAX_COUNT; i++) {
                        if (pinginfo->times[i] == 0)
                                break;
                }
                /* verifier is fussy here... */
                if (i < XDPING_MAX_COUNT) {
                        pinginfo->times[i] = recvtime -
                                             pinginfo->start;
                        pinginfo->start = 0;
                       i++;
                }
                /* No more space for values? */
                if (i == pinginfo->count || i == XDPING_MAX_COUNT)
                        return XDP_PASS;
        }
 
        /* Now convert reply back into echo request. */
        swap_src_dst_mac(data);
        iph->saddr = iph->daddr;
        iph->daddr = raddr;
        icmph->type = ICMP_ECHO;
        seq = bpf_htons(bpf_ntohs(icmph->un.echo.sequence) + 1);
        icmph->un.echo.sequence = seq;
        icmph->checksum = 0;
        icmph->checksum = ipv4_csum(icmph, ICMP_ECHO_LEN);
 
        pinginfo->seq = seq;
        pinginfo->start = bpf_ktime_get_ns();
 
        return XDP_TX;
}

在完整程序中,有两个ELF部分:一种用于客户端模式(将回复转换为请求并发送请求,以RTT进行度量),另一种用于服务器模式(将请求转换为响应并发送出去)。

最后,用户空间程序加载XDP程序,初始化其使用的地图并开始ping。这是设置XDP并运行ping的main()函数:

 
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
int main(int argc, char **argv)
{
        __u32 mode_flags = XDP_FLAGS_DRV_MODE | XDP_FLAGS_SKB_MODE;
        struct addrinfo *a, hints = { .ai_family = AF_INET };
        struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
        __u16 count = XDPING_DEFAULT_COUNT;
        struct pinginfo pinginfo = { 0 };
        const char *optstr = "c:I:NsS";
        struct bpf_program *main_prog;
        int prog_fd = -1, map_fd = -1;
        struct sockaddr_in rin;
        struct bpf_object *obj;
        struct bpf_map *map;
        char *ifname = NULL;
        char filename[256];
        int opt, ret = 1;
        __u32 raddr = 0;
        int server = 0;
        char cmd[256];
 
        while ((opt = getopt(argc, argv, optstr)) != -1) {
                switch (opt) {
                case 'c':
                        count = atoi(optarg);
                        if (count < 1 || count > XDPING_MAX_COUNT) {
                                fprintf(stderr,
                                        "min count is 1, max count is %d\n",
                                        XDPING_MAX_COUNT);
                                return 1;
                        }
                        break;
                case 'I':
                        ifname = optarg;
                        ifindex = if_nametoindex(ifname);
                        if (!ifindex) {
                                fprintf(stderr, "Could not get interface %s\n",
                                        ifname);
                                return 1;
                        }
                        break;
                case 'N':
                        xdp_flags |= XDP_FLAGS_DRV_MODE;
                        break;
                case 's':
                        /* use server program */
                        server = 1;
                        break;
                case 'S':
                        xdp_flags |= XDP_FLAGS_SKB_MODE;
                        break;
                default:
                        show_usage(basename(argv[0]));
                        return 1;
                }
        }
 
        if (!ifname) {
                show_usage(basename(argv[0]));
                return 1;
        }
        if (!server && optind == argc) {
                show_usage(basename(argv[0]));
                return 1;
        }
 
        if ((xdp_flags & mode_flags) == mode_flags) {
                fprintf(stderr, "-N or -S can be specified, not both.\n");
                show_usage(basename(argv[0]));
                return 1;
        }
 
        if (!server) {
                /* Only supports IPv4; see hints initiailization above. */
                if (getaddrinfo(argv[optind], NULL, &hints, &a) || !a) {
                        fprintf(stderr, "Could not resolve %s\n", argv[optind]);
                        return 1;
                }
                memcpy(&rin, a->ai_addr, sizeof(rin));
                raddr = rin.sin_addr.s_addr;
                freeaddrinfo(a);
        }
 
        if (setrlimit(RLIMIT_MEMLOCK, &r)) {
                perror("setrlimit(RLIMIT_MEMLOCK)");
                return 1;
        }
 
        snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);
 
        if (bpf_prog_load(filename, BPF_PROG_TYPE_XDP, &obj, &prog_fd)) {
                fprintf(stderr, "load of %s failed\n", filename);
                return 1;
        }
 
        main_prog = bpf_object__find_program_by_title(obj,
                                                      server ? "xdpserver" :
                                                               "xdpclient");
        if (main_prog)
                prog_fd = bpf_program__fd(main_prog);
        if (!main_prog || prog_fd < 0) {
                fprintf(stderr, "could not find xdping program");
                return 1;
        }
 
        map = bpf_map__next(NULL, obj);
        if (map)
                map_fd = bpf_map__fd(map);
        if (!map || map_fd < 0) {
                fprintf(stderr, "Could not find ping map");
                goto done;
        }
 
        signal(SIGINT, cleanup);
        signal(SIGTERM, cleanup);
 
        printf("Setting up XDP for %s, please wait...\n", ifname);
 
        printf("XDP setup disrupts network connectivity, hit Ctrl+C to quit\n");
 
        if (bpf_set_link_xdp_fd(ifindex, prog_fd, xdp_flags) < 0) {
                fprintf(stderr, "Link set xdp fd failed for %s\n", ifname);
                goto done;
        }
 
        if (server) {
                close(prog_fd);
                close(map_fd);
                printf("Running server on %s; press Ctrl+C to exit...\n",
                       ifname);
                do { } while (1);
        }
        /* Start xdping-ing from last regular ping reply, e.g. for a count
         * of 10 ICMP requests, we start xdping-ing using reply with seq number
         * 10.  The reason the last "real" ping RTT is much higher is that
         * the ping program sees the ICMP reply associated with the last
         * XDP-generated packet, so ping doesn't get a reply until XDP is done.
         */
        pinginfo.seq = htons(count);
        pinginfo.count = count;
 
        if (bpf_map_update_elem(map_fd, &raddr, &pinginfo, BPF_ANY)) {
                fprintf(stderr, "could not communicate with BPF map: %s\n",
                        strerror(errno));
                cleanup(0);
                goto done;
        }
 
        /* We need to wait for XDP setup to complete. */
        sleep(10);
 
        snprintf(cmd, sizeof(cmd), "ping -c %d -I %s %s",
                 count, ifname, argv[optind]);
 
        printf("\nNormal ping RTT data\n");
        printf("[Ignore final RTT; it is distorted by XDP using the reply]\n");
 
        ret = system(cmd);
 
        if (!ret)
                ret = get_stats(map_fd, count, raddr);
 
        cleanup(0);
 
done:
        if (prog_fd > 0)
                close(prog_fd);
        if (map_fd > 0)
                close(map_fd);
 
        return ret;

结论

我们已经讨论过XDP程序;他们在哪里运行,可以做什么并提供了代码示例。我希望这能激发您玩XDP的乐趣!下次,我们将介绍AF_XDP,这是一种新的套接字类型,它使用XDP支持更完整的内核旁路功能。

请确保访问我们有关BPF的系列文章,并继续关注我们的下一篇博客文章!1. BPF程序类型 2. 这些程序的BPF辅助功能 3. BPF用户空间通信 4. BPF程序构建环境 5. BPF字节码和验证程序 6. BPF数据包转换

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值