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收到的包和发出的包是不断增加的