XDP技术简介
1. XDP程序的运行位置
XDP(eXpress Data Path)提供了一个内核态、高性能、可编程 BPF 包处理框架。这个框架在软件中最早可以处理包的位置(即网卡驱动收到包的 时刻)运行 BPF 程序。如下图所示:
NAPI poll 机制不断调用驱动实现的 poll 方法,后者处理 RX 队列内的包,并最终将包送到正确的程序,也就是我们所说的 XDP 程序。所以很明显这需要网卡驱动的支持,如果驱动支持 XDP ,那 XDP 程序将在 poll 机制内执行。如果不支持,那 XDP 程序将只能在更后面的位置被执行,即上图中的receive_skb中。这其中经历了哪些步骤呢?
- 创建skb,如果不支持XDP,poll机制会将报文送给 clean_rx(),该函数会创建一个skb,并skb进行一些硬件校验何检查,然后较给 gro_receive() 函数;
- 分片重组,GRO可以理解为LRO的软件实现,相比LRO只针对TCP报文,GRO可以处理更多其他类型的报文,总之在 gro_receive() 函数中,如果是分片报文则进行分片重组然后交给 receive_skb() 函数,如果不是分片报文,则直接交给 receive_skn() 函数进行处理;
2. XDP的三种工作模式
上面提到XDP程序可以运行在不同位置,每个位置即对应XDP的一种工作模式:
- Native XDP,即运行在网卡驱动实现的的 poll() 函数中,需要网卡驱动的支持;
- Generic XDP,即上面提到的如果网卡驱动不支持XDP,则可以运行在 receive_skb() 函数中;
- Offloaded XDP,这种模式是指将XDP程序offload到网卡中,这需要网卡硬件的支持,JIT编译器将BPF代码翻译成网卡原生指令并在网卡上运行。
3. XDP程序的返回码
XDP程序执行结束后会返回一个结果,告诉调用者接下来如何处理这个包:
- XDP_DROP,丢弃这个包,主要用于报文过滤的安全场景;
- XDP_PASS,将这个包“交给/还给”内核,继续走正常的内核处理流程;
- XDP_TX,从收到包的网卡上再将这个包发出去(即hairpin模式),主要用于负载均衡场景;
- XDP_REDIRECT,何XDP_TX类似,但是是通过另外一个网卡将包发出去。除此之外还可以实现将报文重定向到其他的CPU处理,类似于XDP_PASS继续走内核处理流程,但是由其他的CPU处理,当前CPU继续处理后续的报文接收;
- XDP_ABORTED,表示程序产生异常,行为类似XDP_DROP,但是会通过一个tracepoint打印日志义工追踪;
下面是 Mellanox mlx5 驱动中关于XDP的处理,如果该函数返回 true,则说明报文被XDP处理了,不用再走内核协议栈了,如果返回 false 则创建SKB然后继续走内核协议栈:
/* returns true if packet was consumed by xdp */
bool mlx5e_xdp_handle(struct mlx5e_rq *rq, struct mlx5e_dma_info *di,
u32 *len, struct xdp_buff *xdp)
{
struct bpf_prog *prog = rcu_dereference(rq->xdp_prog);
u32 act;
int err;
if (!prog)
return false;
act = bpf_prog_run_xdp(prog, xdp);
switch (act) {
case XDP_PASS:
*len = xdp->data_end - xdp->data;
return false;
case XDP_TX:
if (unlikely(!mlx5e_xmit_xdp_buff(rq->xdpsq, rq, di, xdp)))
goto xdp_abort;
__set_bit(MLX5E_RQ_FLAG_XDP_XMIT, rq->flags); /* non-atomic */
return true;
case XDP_REDIRECT:
if (xdp->rxq->mem.type != MEM_TYPE_XSK_BUFF_POOL) {
page_ref_sub(di->page, di->refcnt_bias);
di->refcnt_bias = 0;
}
/* When XDP enabled then page-refcnt==1 here */
err = xdp_do_redirect(rq->netdev, xdp, prog);
if (unlikely(err))
goto xdp_abort;
__set_bit(MLX5E_RQ_FLAG_XDP_XMIT, rq->flags);
__set_bit(MLX5E_RQ_FLAG_XDP_REDIRECT, rq->flags);
if (xdp->rxq->mem.type != MEM_TYPE_XSK_BUFF_POOL)
mlx5e_page_dma_unmap(rq, di);
rq->stats->xdp_redirect++;
return true;
default:
bpf_warn_invalid_xdp_action(act);
fallthrough;
case XDP_ABORTED:
xdp_abort:
trace_xdp_exception(rq->netdev, prog, act);
fallthrough;
case XDP_DROP:
rq->stats->xdp_drop++;
return true;
}
}
疑问?
如果我们相对报文执行 redirect,那么我们在BPF程序中需要执行 bpf_redirect() / bpf_redirect_map(),但是从上面的代码中看,从我们的BPF程序返回后,驱动程序也调用了一个叫做 xdp_do_redirect() 的函数。那么问题来了,报文的 redirect 到底是在什么时候执行的呢?答案后面揭晓。
接着分析:
case XDP_REDIRECT:
/* When XDP enabled then page-refcnt==1 here */
err = xdp_do_redirect(rq->netdev, &xdp, prog);
if (unlikely(err))
goto xdp_abort;
__set_bit(MLX5E_RQ_FLAG_XDP_XMIT, rq->flags);
__set_bit(MLX5E_RQ_FLAG_XDP_REDIRECT, rq->flags);
if (!xsk)
mlx5e_page_dma_unmap(rq, di);
rq->stats->xdp_redirect++;
return true;
XDP程序返回后,驱动会根据XDP程序的返回码去真正执行 action。我们以 XDP_REDIRECT 为例,继续跟踪 xdp_do_redirect() 函数:
// >> net/core/filter.c
xdp_do_redirect(netdev, xdp_buff, xdp_prog) =>
xdp_do_redirect_map(netdev, xdp_buff, xdp_prog, bpf_map, bpf_redirect_info) =>
__bpf_tx_xdp_map(netdev, fwd, bpf_map, xdp_buff, index) =>
// fwd即xdp_sock;
// >> kernel/bpf/xskmap.c
__xsk_map_redirect(bpf_map, xdp_buff, xdp_sock) =>
// >> net/xdp/xsk.c
xsk_rcv(xdp_sock, xdp_buff)
__xsk_rcv(xdp_sock, xdp_buff, len)
我们主要看下 xsk_rck() 和 __xsk_rcv() 两个函数:
int xsk_rcv(struct xdp_sock *xs, struct xdp_buff *xdp)
{
u32 len;
if (!xsk_is_bound(xs))
return -EINVAL;
// AF_XDP技术详解中曾介绍过,AF_XDP socket是跟具体的网卡RX队列绑定的,这里再真正执行
// 收包前做了依次判断(虽然XDP程序中也有判断,但毕竟不是强制的)
if (xs->dev != xdp->rxq->dev || xs->queue_id != xdp->rxq->queue_index)
return -EINVAL;
len = xdp->data_end - xdp->data;
return (xdp->rxq->mem.type == MEM_TYPE_ZERO_COPY) ?
__xsk_rcv_zc(xs, xdp, len) : __xsk_rcv(xs, xdp, len);
}
static int __xsk_rcv(struct xdp_sock *xs, struct xdp_buff *xdp, u32 len)
{
u64 offset = xs->umem->headroom;
u64 addr, memcpy_addr;
void *from_buf;
u32 metalen;
int err;
// 从 FILL RING中获取可以承载报文数据的desc
if (!xskq_peek_addr(xs->umem->fq, &addr, xs->umem) ||
len > xs->umem->chunk_size_nohr - XDP_PACKET_HEADROOM) {
xs->rx_dropped++;
return -ENOSPC;
}
if (unlikely(xdp_data_meta_unsupported(xdp))) {
from_buf = xdp->data;
metalen = 0;
} else {
from_buf = xdp->data_meta;
metalen = xdp->data - xdp->data_meta;
}
// 执行报文数据的copy,该函数时非zero copy模式下的执行函数
memcpy_addr = xsk_umem_adjust_offset(xs->umem, addr, offset);
__xsk_rcv_memcpy(xs->umem, memcpy_addr, from_buf, len, metalen);
offset += metalen;
addr = xsk_umem_adjust_offset(xs->umem, addr, offset);
// 插入到 RX RING中
err = xskq_produce_batch_desc(xs->rx, addr, len);
if (!err) {
xskq_discard_addr(xs->umem->fq);
xdp_return_buff(xdp);
return 0;
}
xs->rx_dropped++;
return err;
}
结论:
bpf_redirect() 和 bpf_redirect_map() 应该只是填充bpf_redirect_info结构(即redirect的target相关的数据),真正的redirect操作仍由驱动在 XDP程序返回后执行。
// >> include/linux/filter.h
struct bpf_redirect_info {
u32 flags;
u32 tgt_index;
void *tgt_value;
struct bpf_map *map;
struct bpf_map *map_to_flush;
u32 kern_flags;
};
// >> net/core/filter.c:
int xdp_do_redirect(struct net_device *dev, struct xdp_buff *xdp,
struct bpf_prog *xdp_prog)
{
struct bpf_redirect_info *ri = this_cpu_ptr(&bpf_redirect_info);
struct bpf_map *map = READ_ONCE(ri->map);
if (likely(map))
return xdp_do_redirect_map(dev, xdp, xdp_prog, map, ri);
return xdp_do_redirect_slow(dev, xdp, xdp_prog, ri);
}
分析的没错,bpf_redirect_map()函数定义如下:
// >> net/core/filter.c
BPF_CALL_3(bpf_xdp_redirect_map, struct bpf_map *, map, u32, ifindex,
u64, flags)
{
struct bpf_redirect_info *ri = this_cpu_ptr(&bpf_redirect_info);
/* Lower bits of the flags are used as return code on lookup failure */
if (unlikely(flags > XDP_TX))
return XDP_ABORTED;
ri->tgt_value = __xdp_map_lookup_elem(map, ifindex);
if (unlikely(!ri->tgt_value)) {
/* If the lookup fails we want to clear out the state in the
* redirect_info struct completely, so that if an eBPF program
* performs multiple lookups, the last one always takes
* precedence.
*/
WRITE_ONCE(ri->map, NULL);
return flags;
}
ri->flags = flags;
ri->tgt_index = ifindex;
WRITE_ONCE(ri->map, map);
return XDP_REDIRECT;
}