通过学习 eBPF Talk: AF_XDP,我们掌握了 AF_XDP 的那些基础知识。

提问:对于如下高性能场景,在网卡收到网络包后,该网络包会被哪个 AF_XDP socket 处理呢?

AF_XDP high-performance model

  1. 该网卡独占一个 XDP 程序。
  2. 该 XDP 程序独占一个 BPF_MAP_TYPE_XSKMAP bpf map。
  3. 用户态应用程序为每个 queue 创建一对 UMEM 和 AF_XDP socket。
  4. 使用 ethtool ntuple 在网卡里进行 RSS

本文参考的用户态 Go 库为 asavie/xdp

创建 AF_XDP socket 及其 UMEM

略过 AF_XDP socket 创建过程,直接看下 UMEM 是如何创建出来的吧。

 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
// ${asavie/xdp}/xdp.go

func NewSocket(Ifindex int, QueueID int, options *SocketOptions) (xsk *Socket, err error) {
    //...
    xsk = &Socket{fd: -1, ifindex: Ifindex, options: *options}

    xsk.fd, err = syscall.Socket(unix.AF_XDP, syscall.SOCK_RAW, 0)
    // ...

    xsk.umem, err = syscall.Mmap(-1, 0, options.NumFrames*options.FrameSize,
        syscall.PROT_READ|syscall.PROT_WRITE,
        syscall.MAP_PRIVATE|syscall.MAP_ANONYMOUS|syscall.MAP_POPULATE)
    // ...

    xdpUmemReg := unix.XDPUmemReg{
        Addr:     uint64(uintptr(unsafe.Pointer(&xsk.umem[0]))),
        Len:      uint64(len(xsk.umem)),
        Size:     uint32(options.FrameSize),
        Headroom: 0,
    }

    var errno syscall.Errno
    var rc uintptr

    rc, _, errno = unix.Syscall6(syscall.SYS_SETSOCKOPT, uintptr(xsk.fd),
        unix.SOL_XDP, unix.XDP_UMEM_REG,
        uintptr(unsafe.Pointer(&xdpUmemReg)),
        unsafe.Sizeof(xdpUmemReg), 0)
    // ...

    sa := unix.SockaddrXDP{
        Flags:   DefaultSocketFlags,
        Ifindex: uint32(Ifindex),
        QueueID: uint32(QueueID),
    }
    if err = unix.Bind(xsk.fd, &sa); err != nil {
        xsk.Close()
        return nil, fmt.Errorf("syscall.Bind SockaddrXDP failed: %v", err)
    }
    // ...

    return xsk, nil
}

如上代码片段的主要处理逻辑如下:

  1. 创建 AF_XDP socket。
  2. 使用 mmap 创建出 UMEM 所使用的内存。
  3. 将 UMEM 绑定到 AF_XDP socket。
  4. 将 AF_XDP socket 绑定到网卡的指定队列(QueueID)。

UMEM 绑定 AF_XDP socket

将 UMEM 绑定 AF_XDP socket 的系统调用是: unix.Syscall6(syscall.SYS_SETSOCKOPT, uintptr(xsk.fd), unix.SOL_XDP, unix.XDP_UMEM_REG, uintptr(unsafe.Pointer(&xdpUmemReg)), unsafe.Sizeof(xdpUmemReg), 0)

直接看下内核源代码的处理逻辑吧。

1
2
3
4
5
6
7
8
xsk_setsockopt()                                    // ${KERNEL}/net/xdp/xsk.c
|-->umem = xdp_umem_create(&mr);                    // ${KERNEL}/net/xdp/xdp_umem.c
|   |-->umem = kzalloc(sizeof(*umem), GFP_KERNEL);
|   |-->xdp_umem_reg(umem, mr);
|       |-->xdp_umem_account_pages(umem);
|       |-->xdp_umem_pin_pages(umem, (unsigned long)addr);
|       |-->xdp_umem_addr_map(umem, umem->pgs, umem->npgs);
|-->WRITE_ONCE(xs->umem, umem);

如上代码片段的处理逻辑如下:

  1. 创建内核 umem
  2. 设置好 umem 的各个属性。
  3. 通过 xdp_umem_pin_pages()umemmmap() 分配的内存绑定起来。
  4. 将 AF_XDP socket 的 umem 属性指向 umem

AF_XDP socket 绑定网卡 queue

将 AF_XDP socket 绑定网卡 queue 的系统调用是: unix.Bind(xsk.fd, &sa)

直接看下内核源代码的 bind() 做了哪些处理吧。

 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
// ${KERNEL}/net/xdp/sxk.c

static const struct proto_ops xsk_proto_ops = {
    .family     = PF_XDP,
    // ...
    .bind       = xsk_bind,
    // ...
};

static int xsk_bind(struct socket *sock, struct sockaddr *addr, int addr_len)
{
    // ...

    dev = dev_get_by_index(sock_net(sk), sxdp->sxdp_ifindex);
    // ...

    qid = sxdp->sxdp_queue_id;

    if (flags & XDP_SHARED_UMEM) {
        // ...
    } else if (!xs->umem || !xsk_validate_queues(xs)) {
        // ...
    } else {
        /* This xsk has its own umem. */
        xs->pool = xp_create_and_assign_umem(xs, xs->umem);
        if (!xs->pool) {
            err = -ENOMEM;
            goto out_unlock;
        }

        err = xp_assign_dev(xs->pool, dev, qid, flags);
        if (err) {
            xp_destroy(xs->pool);
            xs->pool = NULL;
            goto out_unlock;
        }
    }

    /* FQ and CQ are now owned by the buffer pool and cleaned up with it. */
    xs->fq_tmp = NULL;
    xs->cq_tmp = NULL;

    xs->dev = dev;
    xs->zc = xs->umem->zc;
    xs->queue_id = qid;
    xp_add_xsk(xs->pool, xs);

    return err;
}

xsk_bind() 的主要处理逻辑如下:

  1. 使用 umem 创建一个缓冲池。
  2. 将该缓冲池绑定到 qid 对应的 rxqtxq

XDP bpf prog 与 BPF_MAP_TYPE_XSKMAP bpf map

AF_XDP socket 及其 UMEM 准备好了之后,还需要拼图的另外两块碎片:XDP bpf prog 与 BPF_MAP_TYPE_XSKMAP bpf map, 才能够使用 UMEM 接收网络包。

XDP bpf prog

该 bpf prog 主要的作用就是用于分流:判断哪些网络包要 redirect 到 UMEM,其它网络包直接 PASS。

当要将网络包 redirect 到 UMEM 时,需要调用 bpf_redirect_map() 帮助函数进行 redirect。

 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
/**
 * long bpf_redirect_map(struct bpf_map *map, u32 key, u64 flags)
 *  Description
 *      Redirect the packet to the endpoint referenced by *map* at
 *      index *key*. Depending on its type, this *map* can contain
 *      references to net devices (for forwarding packets through other
 *      ports), or to CPUs (for redirecting XDP frames to another CPU;
 *      but this is only implemented for native XDP (with driver
 *      support) as of this writing).
 *
 *      The lower two bits of *flags* are used as the return code if
 *      the map lookup fails. This is so that the return value can be
 *      one of the XDP program return codes up to **XDP_TX**, as chosen
 *      by the caller. The higher bits of *flags* can be set to
 *      BPF_F_BROADCAST or BPF_F_EXCLUDE_INGRESS as defined below.
 *
 *      With BPF_F_BROADCAST the packet will be broadcasted to all the
 *      interfaces in the map, with BPF_F_EXCLUDE_INGRESS the ingress
 *      interface will be excluded when do broadcasting.
 *
 *      See also **bpf_redirect**\ (), which only supports redirecting
 *      to an ifindex, but doesn't require a map to do so.
 *  Return
 *      **XDP_REDIRECT** on success, or the value of the two lower bits
 *      of the *flags* argument on error.
 */

高性能场景下使用 bpf_redirect_map() 的参数如下:

  1. map 指向 BPF_MAP_TYPE_XSKMAP bpf map。
  2. key 使用 xdp->rxq->queue_indexxdp 指的是 XDP bpf prog 的上下文,类型 struct xdp_buff *)。
  3. flags 为 0.

也就是说,将当前网络包 redirect 到当前网卡 queue index 对应的 AF_XDP socket 的 UMEM 中。

BPF_MAP_TYPE_XSKMAP bpf map

不过,要成功将网络包 redirect 到 UMEM,还需要 BPF_MAP_TYPE_XSKMAP bpf map 中存有映射关系:queue index -> AF_XDP socket fd。

所以,在用户态应用程序中,在 NewSocket() 之后,还需要 map.Update(queueID, xsk.Fd()) 将它们的映射关系保存到 BPF_MAP_TYPE_XSKMAP bpf map 中。

否则,如果 BPF_MAP_TYPE_XSKMAP bpf map 中没有对应的 entry,XDP bpf prog 进行 redirect 最终会失败。

看下 map.Update() 保存的是什么(不是 AF_XDP socket fd,而是 AF_XDP socket):

 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
const struct bpf_map_ops xsk_map_ops = {
    // ...
    .map_update_elem = xsk_map_update_elem,
    // ...
};

static int xsk_map_update_elem(struct bpf_map *map, void *key, void *value,
                   u64 map_flags)
{
    struct xsk_map *m = container_of(map, struct xsk_map, map);
    struct xdp_sock __rcu **map_entry;
    struct xdp_sock *xs, *old_xs;
    u32 i = *(u32 *)key, fd = *(u32 *)value;
    // ...

    sock = sockfd_lookup(fd, &err);
    // ...

    xs = (struct xdp_sock *)sock->sk;

    map_entry = &m->xsk_map[i];
    node = xsk_map_node_alloc(m, map_entry);
    // ...
    xsk_map_sock_add(xs, node);
    rcu_assign_pointer(*map_entry, xs);
    // ...
    return 0;

out:
    // ...
    return err;
}

如上代码片段的主要处理逻辑如下:

  1. 查询 fd 对应的 AF_XDP socket。
  2. 将查询到的 AF_XDP socket 保存到 key 对应的 entry 里。

XDP redirect

进行 redirect 的前置条件皆已准备好,接下来直接看看一个网络包是怎么从 XDP 到 AF_XDP 的。

关于 XDP redirect,请学习 eBPF Talk: 揭秘 XDP 转发网络包

真实的 redirect 处理分两步走:

  1. bpf_redirect_map()
  2. 内核对 XDP_REDIRECT 的处理。

bpf_redirect_map()

 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
// ${KERNEL}/net/core/filter.c

BPF_CALL_3(bpf_xdp_redirect_map, struct bpf_map *, map, u32, ifindex,
       u64, flags)
{
    return map->ops->map_redirect(map, ifindex, flags);
}

// ${KERNEL}/net/xdp/xskmap.c

const struct bpf_map_ops xsk_map_ops = {
    // ...
    .map_redirect = xsk_map_redirect,
};

static int xsk_map_redirect(struct bpf_map *map, u32 ifindex, u64 flags)
{
    return __bpf_xdp_redirect_map(map, ifindex, flags, 0,
                      __xsk_map_lookup_elem);
}

// ${KERNEL}/include/linux/filter.h

static __always_inline int __bpf_xdp_redirect_map(struct bpf_map *map, u32 ifindex,
                          u64 flags, const u64 flag_mask,
                          void *lookup_elem(struct bpf_map *map, u32 key))
{
    struct bpf_redirect_info *ri = this_cpu_ptr(&bpf_redirect_info);
    const u64 action_mask = XDP_ABORTED | XDP_DROP | XDP_PASS | XDP_TX;

    /* Lower bits of the flags are used as return code on lookup failure */
    if (unlikely(flags & ~(action_mask | flag_mask)))
        return XDP_ABORTED;

    ri->tgt_value = lookup_elem(map, ifindex);
    // ...

    ri->tgt_index = ifindex;
    ri->map_id = map->id;
    ri->map_type = map->map_type;

    // ...

    return XDP_REDIRECT;
}

// ${KERNEL}/net/xdp/xskmap.c

/* Elements are kept alive by RCU; either by rcu_read_lock() (from syscall) or
 * by local_bh_disable() (from XDP calls inside NAPI). The
 * rcu_read_lock_bh_held() below makes lockdep accept both.
 */
static void *__xsk_map_lookup_elem(struct bpf_map *map, u32 key)
{
    struct xsk_map *m = container_of(map, struct xsk_map, map);

    if (key >= map->max_entries)
        return NULL;

    return rcu_dereference_check(m->xsk_map[key], rcu_read_lock_bh_held());
}

如上代码片段的处理逻辑如下:

  1. 从 BPF_MAP_TYPE_XSKMAP bpf map 中使用指定 key (即 xdp->rxq->queue_index)查询出对应的值(即 AF_XDP socket)。
  2. 将查找到的 AF_XDP socket 保存到 this_cpu_ptr(&bpf_redirect_info),后面步骤会从这里取出 AF_XDP socket。

内核对 XDP_REDIRECT 的处理

参考 eBPF Talk: 揭秘 XDP 转发网络包

1
2
3
4
5
6
7
xdp_do_generic_redirect_map()               // ${KERNEL}/kernel/core/filter.c
|-->xsk_generic_rcv()                       // ${KERNEL}/net/xdp/xsk.c
    |-->__xsk_rcv()
    |   |-->xsk_xdp = xsk_buff_alloc(xs->pool);
    |   |-->xsk_copy_xdp(xsk_xdp, xdp, len);
    |   |-->__xsk_rcv_zc(xs, xsk_xdp, len);
    |-->xsk_flush()

如上代码片段的主要处理逻辑如下:

  1. 从 UMEM 缓冲池里分配一块内存。
  2. 将网络包复制到该内存中。
  3. xsk_flush() release 一下 fill queue

总结

对着 AF_XDP 一番研究,仅仅是研究了高性能 AF_XDP 的冰山一角。

  1. 复杂的用户态应用程序处理。
  2. 网卡独占的 XDP bpf prog 和 BPF_MAP_TYPE_XSKMAP bpf map。

仅仅是如上两个步骤,还不足以成为高性能 AF_XDP,还需要:

  1. 使用类 DPDK 的 PMD 模式去设计用户态应用程序。
  2. 使用 ethtool ntuple 在网卡里配置 RSS 进行网络包分发。

其中 PMD 模式就涉及 CPU 亲和性和 NUMA 配置,为了内核处理逻辑、用户态应用程序处理逻辑都在同一个 CPU 上处理同一个网络包,使得网络包所在内存的 CPU 亲和性最大化(最大程度降低 CPU cache miss)。

合适的 ethtool ntuple 配置,会按需将网络包分发到对应的网卡 queue 中;比如,同一个源地址的网络包只会分发到同一个 queue 中。

除了高性能 AF_XDP 外,AF_XDP 本身的实现机制也值得探索一番;毕竟这是一个使用 mmap() 实现内核、用户程序进行高性能编程的工业级案例。