5

eBPF系列2 - XDP

 2 years ago
source link: https://yanhang.me/post/2020-11-17-ebpf-2-xdp/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

eBPF系列2 - XDP

2020-11-17 约 1590 字 预计阅读 4 分钟

XDP是指 eXpress data path, 基于 ebpf 技术上的 高性能 data path. 其主要的意图是在网络 packet 处理的早期(网卡驱动处, 在数据包到达RX queue之后, 用hook的方式),让用户可以编写 ebpf 程序来进行一些决策。 这个hook的位置早于所有的内存分配时期(sk_buffer),也没有上下文切换,系统调用等开销, 所以对于性能的提升是很明显的。测试数据表明,在普通的硬件上, XDP 可以 dop 大概 2600w packets per cpu.

一个网络包的处理流程大致如下:

  1. 网络包到达网卡
  2. 从 NIC queue 拷贝到内存(DMA-backed ring buffer)中
  3. 网卡驱动使用 NAPI look 触发 soft IRQs
  4. per cpu 特定线程处理网络包
  5. 分配 socket_buffer(sk_buffer), 作为网络包的基本数据结构
  6. kernel 填充 metadata, clone sk_buffer 并且交给上层网络处理层
  7. IP layer 做校验, netfilter hook 处理
  8. 如果 netfilter 未 drop 此包,交给更上层网络层处理
  9. 最终数据被 copy 到 userspace (recv,read,poll等网络调用获取)

其中, XDP hook 在数据到达 NIC RX queue 之后即触发(上面第二步之后).而 iptables 等的处理(上面第八步之后)非常靠后, 需要分配大量资源来处理网络包.

XDP 程序 attch 到一个网卡的时候,有如下三种模式:

  • Native XDP: 网卡驱动支持 XDP, 性能比较高
  • Offloaded XDP: 网卡直接加载 XDP 程序,处理过程中不需要CPU参与。这个也需要网卡支持
  • Generic XDP: 最通用的模式,加载到内核中作为正常网络路径的一部分

插入的 ebpf 程序可以修改 packet data, 当程序返回时,可以决定这个 packet 后续的处理路径:

  • XDP_PASS: 继续按原有路径处理
  • XDP_DROP: 丢弃.这个可以用来在早期处理DOS攻击,userspace的程序可以分析网络pattern并且不断更新处理策略。性能与易用性兼得。
  • XDP_ABORTED: 丢弃并抛出异常
  • XDP_TX: 返回到收到这个 packet 的 NIC 上
  • XDP_REDIRECT: 重定向到另一个NIC或者到 user space 的 socket (AF_XDP address family,绕过了正常的网络栈)

Python 示例程序

这个示例程序将发往 port 7999的包改为 7998

main.py

#!/usr/bin/env python3

from bcc import BPF
import time

device = "lo"
b = BPF(src_file="filter.c")
fn = b.load_func("udpfilter", BPF.XDP)
b.attach_xdp(device, fn, 0)

try:
  b.trace_print()
except KeyboardInterrupt:
  pass

b.remove_xdp(device, 0)
  • python 文件用于加载 bpf 程序,真正的 bpf 程序实在 filter.c 中。

  • BPF 程序被 attch 到了 lookback 接口上

  • trace_print 打印出 bpf 程序处理过程中的输出

  • remove_xdp发生在程序退出时,用户从 lookback 接口上移除加载的 xdp 程序

filter.c:

#define KBUILD_MODNAME "filter"
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <linux/udp.h>

int udpfilter(struct xdp_md *ctx) {
  bpf_trace_printk("got a packet\n");
  void *data = (void *)(long)ctx->data;
  void *data_end = (void *)(long)ctx->data_end;
  struct ethhdr *eth = data;
  if ((void*)eth + sizeof(*eth) <= data_end) {
    struct iphdr *ip = data + sizeof(*eth);
    if ((void*)ip + sizeof(*ip) <= data_end) {
      if (ip->protocol == IPPROTO_UDP) {
        struct udphdr *udp = (void*)ip + sizeof(*ip);
        if ((void*)udp + sizeof(*udp) <= data_end) {
          if (udp->dest == ntohs(7999)) {
            bpf_trace_printk("udp port 7999\n");
            udp->dest = ntohs(7998);
          }
        }
      }
    }
  }
  return XDP_PASS;
}
  • xdp_md结构体中包含了 packet data
  • bpf_trace_printk 的输出会在 main.py 里的 trace_print 中反应出来
  • 指针方式确保了我们时原地修改了 packet data

AF_XDP

上面提到了 AF_XDP,他可以让XDP 程序可以将一个 packet 重定向到 user space 的内存空间里.

有一个比较接近的案例是 AF_PACKET/DPDK, 他们是通过在kernel和userspace共享内存来提高性能。而AF_XDP直能绕过很多 kernel stack 的步骤, 用最快的方式将 packet 传到 userspace.另外,它也提供了一些 zero-copy等功能。

具体实现上来说,有一些比较独特的数据结构来的达成此目的。创建的时候,仍然是用 socket API, 使用 AF_XDP address family. 然后在 userspace的内存中创建一个叫 UMEM 的 array, 它是一块连续的内存空间,被分成相等的 frames ,每一个 frame 都可以用来存储一个 packet.

每个 frame 的 index 被叫做 descriptor, 程序创建一个叫 fill queue 的 circular buffer, 并通过 mmap 映射到 userspace. 之后程序可以要求 kernel 将一个 packet 放到一个指定的 frame中(将 frame 的 descriptor 放到 fill queue 中)

receive queue: 类似 fill Q 的创建和映射方式。当一个 packet 被放到一个 frame 之后,他的 descriptor 会被放到 receive queue 中,poll 这样的API就是 wait 的 receive queue.

上面是接收端的情况。发送端的情况类似。 一个 transmit queue, 通过将 descriptor 放到 tranmit queue表示要发送,complete queue 用于存放已经发送完的 packet .

通过这个数据结构,就实现了userspace 和 kernel 的 zero-copy. UMEM 数组还可以被多个进程共享,fill queue 和 compeltion queue 只能有一个,但 receive queue 和 tranmit queue 每个进程都需要有自己的,

AF_XDP最终的目的就是让user space code 可以高效地处理 packet, 尽量少的 kernel overhead.

Links


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK