网卡XDP驱动研究以及验证

XDP全程eXpress Data Path,即快速数据路径,XDP是linux网络处理流程中的一个ebpf钩子,能够挂载eBPF程序,它能够在网络数据包到达网卡驱动层时对其进行处理,打通linux网络处理的高速公路。其核心思想是在数据包到达网络驱动之前或者之中,在内核空间对网络数据包进行处理,以便快速处理数据包并决定其进一步的处理路径。

相比dpdk,XDP无需将网卡重新绑定到uio或者vfio,直接在linux内核网卡驱动处理即可,对于AF_XDP来说,网卡数据不再经过冗长的内核协议栈,而是直接由应用层处理,大大降低了内核处理的消耗。

运行的XDP程序可以通过XDP动作码来指定驱动对网络数据包的后续动作

XDP_ABORTED:意味着程序错误,会将数据包丢掉

XDP_DROP会在网卡驱动层直接将该数据包丢掉,无需再进一步处理,也就是无需再耗费任何额外的资源。

XDP_PASS会将该数据包继续送往内核的网络协议栈,和传统的处理方式一致。这也使得XDP可以在有需要的时候方便地使用传统的内核协议栈进行处理。

XDP_TX会将该数据包从同一块网卡返回。

XDP_REDIRECT则是将数据包重定向到其他的网卡或CPU,结合AF_XDP可以将数据包直接送往用户空间

1.STMMAC XDP 驱动研究

stmmac 网卡驱动源码均在linux5.15上,由于XDP相对来说较新,不同版本的XDP驱动变化较大。

对于XDP驱动,笔者分成两个方面进行,一方面是以上的XDP_DROP、XDP_ABORTED、XDOP_TX、XDP_PASS,这里我称之为普通XDP功能,另一方面则是通过XDP直接redirect到应用层,称为AF_XDP,或者XSK。

本质上,XDP的作用就是截取网卡驱动层的数据根据XDP的操作码,去对本该往协议栈发送的skb数据进行操作。当应用层调用类似 ip link set dev enp1s0f0 xdp obj xdp-drop.o sec drop_icmp的命令,此时经过linux层层调用,最终会调用到ndo_bpf回调函数进行xdp初始化。

1.1.stmmac_驱动普通XDP研究

XDP的接口主要有

.ndo_bpf = stmmac_bpf,
.ndo_xdp_xmit = stmmac_xdp_xmit,

stmmac_bpf主要用于初始化xdp,stmmac_xdp_xmit则用于AF_XDP下的数据发送。

XDP初始化流程如下

static int stmmac_bpf(struct net_device *dev, struct netdev_bpf *bpf)
{
        struct stmmac_priv *priv = netdev_priv(dev);

        switch (bpf->command) {
        case XDP_SETUP_PROG:
                return stmmac_xdp_set_prog(priv, bpf->prog, bpf->extack);
        case XDP_SETUP_XSK_POOL:
                return stmmac_xdp_setup_pool(priv, bpf->xsk.pool,
                                             bpf->xsk.queue_id);
        default:
                return -EOPNOTSUPP;
        }
}

1.调用stmmac_bpf,判断上层传入的参数bpf->command,根据该command确定当前是否是初始化xdp还是初始化xdp pool。对于非AF_XDP功能,bpf->command为XDP_SETUP_PROG,即调用stmamc_xdp_set_prog

2.stmamc_xdp_set_prog最主要的作用就是提供bpf_prog给priv->xdp_prog,这样就可以通过stmmac_xdp_is_enabled(priv)判断xdp已经被enable了。当然由于新添加了xdp功能,网卡的资源需要重新初始化,因此调用stmmac_xdp_release(dev);stmmac_xdp_open(dev);进行资源的释放和重新申请

int stmmac_xdp_set_prog(struct stmmac_priv *priv, struct bpf_prog *prog,
                        struct netlink_ext_ack *extack)
{
    ......
    if (if_running && need_update)
                stmmac_xdp_release(dev);
    old_prog = xchg(&priv->xdp_prog, prog);
    if (if_running && need_update)
                stmmac_xdp_open(dev);
    ......
}

static inline bool stmmac_xdp_is_enabled(struct stmmac_priv *priv)
{
	return !!priv->xdp_prog;
}

那么对于非AF_XDP功能的XDP如何在驱动中进行drop,pass,以及tx呢?查看stmmac驱动的接收函数就一目了然了。

static int stmmac_rx(struct stmmac_priv *priv, int limit, u32 queue)
{
    .......
    unsigned int pre_len, sync_len;
    dma_sync_single_for_cpu(priv->device, buf->addr,
                                buf1_len, dma_dir);/*处理buf之前必须确保dma完成*/
    xdp_init_buff(&xdp, buf_sz, &rx_q->xdp_rxq);
    xdp_prepare_buff(&xdp, page_address(buf->page),/*这里就是将dma完成收到的buf地址赋值给xdp*/
                                buf->page_offset, buf1_len, false);
    pre_len = xdp.data_end - xdp.data_hard_start -buf->page_offset;
    skb = stmmac_xdp_run_prog(priv, &xdp);/*经历一次中转站,这里就决定了数据需要放到哪儿*/
      .......
}

static int __stmmac_xdp_run_prog(struct stmmac_priv *priv,
                                 struct bpf_prog *prog,
                                 struct xdp_buff *xdp)
{
        u32 act;
        int res;
        act = bpf_prog_run_xdp(prog, xdp);/*获取xdp的操作码*/
        switch (act) {
        case XDP_PASS:
                res = STMMAC_XDP_PASS;/*直接PASS,传入协议栈*/
                break;
        case XDP_TX:
                res = stmmac_xdp_xmit_back(priv, xdp);/*从该口接收之后然后从该口出*/
                break;
        case XDP_REDIRECT:
                if (xdp_do_redirect(priv->dev, xdp, prog) < 0)/*AF_XDP功能*/
                        res = STMMAC_XDP_CONSUMED;
                else
                        res = STMMAC_XDP_REDIRECT;
                break;
        default:
                bpf_warn_invalid_xdp_action(act);
                fallthrough;
        case XDP_ABORTED:
                trace_xdp_exception(priv->dev, prog, act);
                fallthrough;
        case XDP_DROP:
                res = STMMAC_XDP_CONSUMED;/**如果都不是上述操作码则丢弃即可/
                break;
        }

        return res;
}

从上面看出,stmmac的接收函数利用了xdp的结构体作一次传导,目的是如果使能xdp功能,利用__stmmac_xdp_run_prog函数来判断收到的包是否需要PASS,TX,DROP以及REDIRECT.如果未使能XDP功能,则会根据stmmac_xdp_is_enabled(priv)直接跳过该函数,也不影响rx方向的性能。

stmmac_xdp_xmit_back函数与stmmac_xdp_xmit函数,同stmmac_xmit函数流程基本一致,这里就不细讲,感兴趣的可以看一下stmmac驱动源码。

1.2.AF_XDP驱动研究

首先看一下AF_XDP rx方向的架构。RX方向的AF_XDP涉及两个生产者和消费者模型,其一应用层和内核的生产者和消费者模型,对于rx方向来说,由内核进行生产buffer,用户层消费buffer。而内核的buffer从哪儿获取呢,这是由网卡生产,通过dma传送给内核驱动。下图为AF_XDP的rx方向的数据流程。

1.应用层通过mmap获取一段地址空间,该空间与内核上的一段物理内存进行映射,然后通过setsocket函数将这段空间的相关信息传递给内核,从而内核和应用层均可以修改和查看这段内存;

2.户态程序通过 fill_ring 将可以用来承载报文的 UMEM frames的idx 传到内核,这样内核就能知道rx方向的每个容纳buffer的地址和长度;

3.在调用stmmac_up函数初始化驱动的相关资源时,stmmac下的每个queue会获取相应的xskb,并将xskb的dma物理地址写道描述符中;

4.xgmac收到包后,通过dma的方式将包写到描述符上的相应的地址上,同时将描述符写回,并触发rx中断。

5.内核驱动收到包后,会填写rxring下的描述符,通知应用层已经收到包,应用层开始消费这笔包。

对于tx方向也类似。

1.同样的,应用层通过mmap获取一段地址空间,该空间与内核上的一段物理内存进行映射,然后通过setsocket函数将这段空间的相关信息传递给内核;

2.用户层会在每一个umem的frames下构造需要发送的包,然后通过txring的描述符将包的地址和长度传递给内核驱动。

3.驱动获得包的信息后,会获取该包的dma的物理地址,然后写到tx方向的描述符,通过flush 描述符开始发送。

4.发送完成后,内核会填写completion ring的描述符,从而用户层读取描述符信息后,获知此次发送已完成,用户层则开始下一次的发送。

由于上文分析的是用户层,内核驱动以及网卡所有一起工作时的流程图,由于个人工作偏向驱动,这里再将AF_XDP在stmmac驱动中的流程详细说明一下。

1.由1.1可知,对于普通xdp会调用stmamc_xdp_set_prog去使能priv->prog参数,AF_XDP也会如此。

2.接着,上层传下来的bpf->command为XDP_SETUP_XSK_POOL,驱动层会根据poll是否已经定义而去使能或者disable xsk_pool。

int stmmac_xdp_setup_pool(struct stmmac_priv *priv, struct xsk_buff_pool *pool,
			  u16 queue)
{
	return pool ? stmmac_xdp_enable_pool(priv, pool, queue) :
		      stmmac_xdp_disable_pool(priv, queue);
}

3.这里分析一下stmmac_xdp_enable_pool函数,stmmac_xdp_disable_pool函数与之相反,该函数干了三件事,第一,将xsk_pool进行dma_map,从而获取相应的dma的物理地址;第二,设置priv->af_xdp_zc_qps,从而能知道当前是af_xdp模式;第三,重新以AF_XDP的模式初始化驱动

static int stmmac_xdp_enable_pool(struct stmmac_priv *priv,
				  struct xsk_buff_pool *pool, u16 queue)
{

    .....
    frame_size = xsk_pool_get_rx_frame_size(pool);
	/* XDP ZC does not span multiple frame, make sure XSK pool buffer
	 * size can at least store Q-in-Q frame.
	 */
	if (frame_size < ETH_FRAME_LEN + VLAN_HLEN * 2)
		return -EOPNOTSUPP;

	err = xsk_pool_dma_map(pool, priv->device, STMMAC_RX_DMA_ATTR);
	if (err) {
		netdev_err(priv->dev, "Failed to map xsk pool\n");
		return err;
	}
    .....
    set_bit(queue, priv->af_xdp_zc_qps);
    .....

}

4. 在初始化驱动过程中,这里重点看一下rx方向af_xdp的xskb的dma物理地址是如何填写到rx的描述符中的。通过下面的函数,则非常明显,通过stmmac_get_xsk_pool可获取rx_q->sxk_pool,使能af_xdp后,则肯定rx-q->xsk_pool不为空。则走stmmac_alloc_rx_buffers_zc分支,该函数作用是获取xsk的dma地址,并填到描述符中。

static struct xsk_buff_pool *stmmac_get_xsk_pool(struct stmmac_priv *priv, u32 queue)
{
	if (!stmmac_xdp_is_enabled(priv) || !test_bit(queue, priv->af_xdp_zc_qps))
		return NULL;

	return xsk_get_pool_from_qid(priv->dev, queue);
}

static int __init_dma_rx_desc_rings(struct stmmac_priv *priv, u32 queue, gfp_t flags)
{

    ......    
    rx_q->xsk_pool = stmmac_get_xsk_pool(priv, queue);
    if (rx_q->xsk_pool) {
		/* RX XDP ZC buffer pool may not be populated, e.g.
		 * xdpsock TX-only.
		 */
		stmmac_alloc_rx_buffers_zc(priv, queue);
	} else {
		ret = stmmac_alloc_rx_buffers(priv, queue, flags);
		if (ret < 0)
			return -ENOMEM;
	}
    ......

}

static int stmmac_alloc_rx_buffers_zc(struct stmmac_priv *priv, u32 queue)
{
    ....
    for (i = 0; i < priv->dma_rx_size; i++) {
        buf = &rx_q->buf_pool[i];

		buf->xdp = xsk_buff_alloc(rx_q->xsk_pool);
		if (!buf->xdp)
			return -ENOMEM;

		dma_addr = xsk_buff_xdp_get_dma(buf->xdp);
		stmmac_set_desc_addr(priv, p, dma_addr);
    }
    ......
}

 5.对于tx方向来说,只需要初始化描述符相关资源即可,由于skb是由上层传递下来的,只需要在发送的函数中将skb的dma物理地址填到描述符中再flush 描述符到硬件即可。

6.AF_XDP的发送和接收函数相比普通包的接收和发送函数来说更简单。这里具体流程就没有必要详细说。

2.STAMMAC XDP驱动验证

常用的一般为XDP_DROP、XDP_PASS、XDP_TX、XDP_REDIRECT.XDP_REDIRECT略微复杂一点。

2.1.XDP_PASS和XDP_DROP

代码示例

#include <linux/bpf.h>
#include <linux/in.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#define SEC(NAME) __attribute__((section(NAME), used))

SEC("drop_icmp")
int drop_icmp_func(struct xdp_md *ctx) {
  int ipsize = 0;

  void *data = (void *)(long)ctx->data;
  void *data_end = (void *)(long)ctx->data_end;

  struct ethhdr *eth = data;

  ipsize = sizeof(*eth);

  struct iphdr *ip = data + ipsize;
  ipsize += sizeof(struct iphdr);
  if (data + ipsize > data_end) {
    // not an ip packet, too short. Pass it on
    return XDP_PASS;
  }
  // technically, we should also check if it is an IP packet by
  // checking the ethernet header proto field ...
  if (ip->protocol == IPPROTO_ICMP) {
    return XDP_DROP;
  }
  return XDP_PASS;
}
char __license[] SEC("license") = "GPL";

上述代码的原理比较简单,就是根据传下来的xdp_md结构体进行解析,如果数据包格式为icmp协议,则丢弃,否则则通过。

采用clang工具对该xdp程序进行编译

clang -g -c -O2 -target bpf -c xdp-drop.c -o xdp-drop.o

未加载xdp程序到网卡之前,网卡ping和nc能够正常工作

然后加载程序到网卡

ip link set dev enp1s0f0 xdp obj xdp-drop.o sec drop_icmp

此时再分别测试网卡ping功能以及nc功能,发现仅nc功能正常工作。

2.2.XDP_TX

XDP_TX会将网卡数据从同一个网卡中发送出去,因此如果需要正常发送出网卡数据,则需要交换网卡数据中的源和目的的mac地址以及源IP和目的IP,这样才能正确的发送出去。

示例如下:

#include <linux/bpf.h>
#include <linux/in.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/types.h>
#define SEC(NAME) __attribute__((section(NAME), used))
// 直接操作包数据(direct packet data access),交换 MAC 地址
static void swap_src_dst_mac(void *data)
{
    unsigned short *p = data;
    unsigned short dst[3];
    dst[0] = p[0]; dst[1] = p[1]; dst[2] = p[2];
    p[0] = p[3]; p[1] = p[4]; p[2] = p[5];
    p[3] = dst[0]; p[4] = dst[1]; p[5] = dst[2];
}

static void swap_src_dst_ip(void *data, unsigned int nh_off, void *data_end)
{
    struct iphdr *iph = data + nh_off;
    if (iph + 1 > data_end)
        return;

    unsigned int dst = iph->daddr;

    iph->daddr = iph->saddr;
    iph->saddr = dst;
}

#define swab16(x) ((unsigned short)(                \
    (((unsigned short)(x) & (unsigned short)0x00ffU) << 8) |            \
    (((unsigned short)(x) & (unsigned short)0xff00U) >> 8)))

static int parse_ipv4(void *data, unsigned int nh_off, void *data_end)
{
    struct iphdr *iph = data + nh_off;
    if (iph + 1 > data_end)
        return 0;

    return iph->protocol;
}

SEC("xdp_tx") // marks main eBPF program entry point
int xdp_prog1(struct xdp_md *ctx)
{
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    struct ethhdr *eth = data;
    unsigned short h_proto; unsigned int nh_off; unsigned int ipproto;

    nh_off = sizeof(*eth);
    if (data + nh_off > data_end)
        return XDP_PASS;

    h_proto = eth->h_proto;

    if (h_proto == swab16(ETH_P_IP))
        ipproto = parse_ipv4(data, nh_off, data_end);
    else
        ipproto = 0;

    /* swap MAC addrs for UDP packets, transmit out this interface */
    if (ipproto == IPPROTO_UDP) {
        swap_src_dst_mac(data);
        swap_src_dst_ip(data, nh_off, data_end);
        return XDP_TX;
    }
    return XDP_PASS;
}

按照上述编译,安装前使用nc工具发送udp数据

可见对端发送udp数据,本端能够正常接收,并且接收的数据直接发往协议栈。

加载xdp程序后,对端发送udp数据,本端直接发送出网卡,这导致协议栈无法受到数据,而对端却收到数据。

2.3 普通XDP 的XDP_REDIRECT

由1部分图所示,XDP_REDIRECT功能主要有两个,第一是将接收到的数据通过另一个网口发送出去;第二,是将接收到的数据直接发送到应用层。

首先研究XDP_REDIRECT第一个功能,转发到另一个端口

示例代码

#include <linux/bpf.h>
#include <linux/in.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#define SEC(NAME) __attribute__((section(NAME), used))

__section("prog")
int xdp_ip_filter(struct xdp_md *ctx)
{
    void *end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    int ip_src;
    int ip_dst;
    long int offset;
    short int eth_type;
    char info_fmt1[] = "Dst Addr: xx.xx.%d.%d";
    char info_fmt2[] = "Src Addr: xx.xx.%d.%d";
    char info_fmt3[] = "------------------";

    static int i = 0;
    static int j = 0;
    unsigned char *saddrpoint = 0;
    unsigned char *daddrpoint = 0;

    struct ethhdr *eth = data;
    offset = sizeof(*eth);

    unsigned int indexofeth0 = 5;
    unsigned int indexofeth1 = 6;


    if (data + offset > end) {
    return XDP_ABORTED;
    }
    eth_type = eth->h_proto;

    struct iphdr *iph = data + offset;
    offset += sizeof(struct iphdr);
    /*在读取之前,确保你要读取的子节在数据包的长度范围内  */
    if (iph + 1 > end) {
        return XDP_PASS;
    }
    /*ip_src = iph->saddr;*/
    ip_dst = iph->daddr;

    /*saddrpoint = (unsigned char*)(&ip_src);
    daddrpoint =(unsigned char*)(&ip_dst);*/
    
    /* 发往10.0.0.10的直接通过enp1s0f0发送出去 */
    if(ip_dst == 0xa00000a)
    {
        return bpf_redirect(indexofeth0,0);
    }

    /* 发往10.0.0.4的直接通过enp1s0f1发送出去 */
    if(ip_dst == 0x400000a)
    {
        return bpf_redirect(indexofeth1,0);
    }
    /* 其它报文上送内核协议栈处理 */
    return XDP_PASS;
}

char __license[] __section("license") = "GPL";

 indexofeth0 可通过以下应用程序编译获得

#include <stdio.h>
#include <linux/bpf.h>
#include <net/ethernet.h>
#include <linux/if_vlan.h>
#include <netinet/in.h>
#include <linux/ip.h>
#include <net/if.h>

int main(struct xdp_md *ctx)
{
   unsigned int indexofeth0 = 0;
   unsigned int indexofeth1 = 0;
   
   indexofeth0 = if_nametoindex("enp1s0f0");
   indexofeth1 = if_nametoindex("enp1s0f1");

   printf("%d, %d", indexofeth0, indexofeth1);
   return 1;
}

 2.4 AF_XDP 的redirect

AF_XDP涉及两部分程序:

XDP程序,需要挂载到我们使用的网卡接口上

用户程序,用于创建umem,xsk,处理各个ring收发包,处理数据包等

AF_XDP示例如下

// SPDX-License-Identifier: GPL-2.0
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include "xdpsock.h"

/* This XDP program is only needed for the XDP_SHARED_UMEM mode.
 * If you do not use this mode, libbpf can supply an XDP program for you.
 */

struct {
    __uint(type, BPF_MAP_TYPE_XSKMAP);
    __uint(max_entries, MAX_SOCKS);
    __uint(key_size, sizeof(int));
    __uint(value_size, sizeof(int));
} xsks_map SEC(".maps");

static unsigned int rr;

SEC("xdp_sock") int xdp_sock_prog(struct xdp_md *ctx)
{
    rr = (rr + 1) & (MAX_SOCKS - 1);

    return bpf_redirect_map(&xsks_map, rr, XDP_DROP);
}

struct SEC("maps") xsks_map: 定义了一个BPF_MAP_TYPE_XSKMAP类型的映射表,当采用SEC("maps")方式来显示定义时,将在生成的bpf目标文件的ELF格式中看到相关描述,当BPF程序被加载到内核时,会自动创建名为“xsks_map”的描述符, 用户态可通过查找“xsks_map”来获取该map的描述符,这样用户态和内核BPF程序就可以共同访问该map。

MAX_SOCKS:指定map最多存储MAX_SOCKS个元素。

SEC("xdp_sock"):指定prog函数符号,应用层可通过查找"xdp_sock"加载该prog,并绑定到指定网卡。

bpf_redirect_map(&xsks_map, rr, 0):bpf_redirect_map函数作用就是重定向,比如:将数据重定向到某个网卡,CPU, Socket等等;当bpf_redirect_map函数的第一个参数的map类型为BPF_MAP_TYPE_XSKMAP时,则表示将数据重定向到XDP Scoket。

该bpf程序实现的功能就是:将收到的数据包重定向到xsks_map中指定的XDP Socket。

用户程序可直接查看linux docement中示例程序linux_user.c即可。

这里简要说明一下原理

首先用户空间mmap映射一段空间。

 bufs = mmap(NULL, NUM_FRAMES * opt_xsk_frame_size,
            PROT_READ | PROT_WRITE,
            MAP_PRIVATE | MAP_ANONYMOUS | opt_mmap_flags, -1, 0);

这块内存被划分为多个大小相等的块(chunks),用于存储网络数据包。随后,用户空间程序需要使用setsockopt系统调用,通过XDP_UMEM_REG选项将UMEM的内存区域注册到内核中。这一步是内核能够处理UMEM内存区域的前提。

 ret = xsk_socket__create(&xsk->xsk, opt_if, opt_queue, umem->umem,
                 rxr, txr, &cfg);

AF_XDP socket通过直接操作ring来实现报文收发。包括4种ring,FILL RING,COMPLETION RING, RX RING TX RING.

fill_ring 的生产者是用户态程序,消费者是内核态中的XDP程序;completion_ring 的生产者是XDP程序,消费者是用户态程序;rx_ring的生产者是XDP程序,消费者是用户态程序;tx_ring的生产者是用户态程序,消费者是XDP程序;

过这四个ring的协同工作,AF_XDP可以实现高性能的网络数据传输,以及在用户空间实现网络协议栈的功能。用户空间程序可以通过Fill Ring为Rx Ring生成新的接收数据描述符,然后使用Tx Ring将处理过的数据发送出去。这就与1.2驱动对应起来。

以内核提供的XDP程序进行验证,linux_user.c有三种模式,分别是drop,tx_only,转发。所谓drop功能是通过AF_XDP收到的包直接丢弃,tx_only功能只通过AF_XDP发送包不接收,转发功能则是将收到的包发送出去。

rx_drop

设置本对端物理口ip,ping 能正常ping通

执行命令 ./linux_user -i enp1s0f0 -r -p.此时仍执行ping操作,但无法ping通,且AF_XDP收到的包是不断增加的。

tx_only

执行命令 ./linux_user -i enp1s0f0 -t

l2_fw(转发)

执行命令 ./linux_user -i enp1s0f0 -r -p.此时仍执行ping操作,但无法ping通,且AF_XDP收到的包和发出的包是不断增加的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值