理解并使用Linux内核中的XDP
1. 引言
什么是XDP
eXpress Data Path (XDP
) 是Linux内核
提供的一种高速数据包处理技术。它直接运行在驱动层,通过在网络数据包进入内核协议栈前处理数据,实现极低的延迟和高吞吐量。XDP
能够将数据包处理逻辑卸载到网络驱动甚至硬件中,这使得它特别适用于对性能要求极高的网络场景。
- 注意:
XDP
只会处理网卡收到的数据包, 不会处理从网卡发出的数据包
XDP
与netfilter
对比
特性 | XDP | netfilter |
---|---|---|
工作层级 | 驱动层 | 内核协议栈 |
性能 | 极高 | 较低 |
功能灵活性 | 处理逻辑简单 | 功能强大 |
应用场景 | DoS防护、负载均衡 | 防火墙、NAT |
XDP
与TC
对比
特性 | XDP | TC (Traffic Control) |
---|---|---|
工作层级 | 驱动层 | 网络协议栈后 |
性能 | 极高 | 较低 |
功能灵活性 | 处理逻辑简单 | 功能较强 |
适用场景 | 数据包过滤、速率限制 | 流量整形、QoS |
XDP
与DPDK
对比
特性 | XDP | DPDK |
---|---|---|
架构 | 内核态 | 用户态 |
性能 | 较高(与硬件支持有关) | 极高(与硬件紧耦合) |
开发难度 | 较低 | 较高 |
使用成本 | 免费,依赖Linux内核 | 需要特定硬件支持 |
XDP
的适用场景
XDP
特别适合以下场景:
- DoS攻击防御:快速丢弃恶意流量。
- 负载均衡:高性能地将流量分发到后端服务器。
- 数据包过滤:实现高效的ACL(访问控制列表)。
2. XDP的实现原理
数据包处理流程
XDP(eXpress Data Path)是一个基于eBPF的高性能数据包处理框架,能够在网络驱动层对数据包进行高效处理。其数据包处理流程如下:
- 数据包从网卡进入:网络接口卡(NIC)接收到网络数据包,并通过硬件中断将数据包传输到主机。
- 网卡驱动接收数据:网卡驱动通过中断或轮询的方式处理接收到的数据包。
- 调用XDP hook:驱动在数据包接收路径中调用XDP hook(即XDP钩子),将接收到的数据包封装成
xdp_buff
结构。 - eBPF程序处理数据包:
xdp_buff
结构传递给加载的eBPF程序进行处理,eBPF程序根据数据包的内容返回一个动作码。 - 根据动作码执行操作:根据eBPF程序返回的动作码(如
XDP_DROP
、XDP_PASS
等),驱动或内核执行相应的操作(丢弃、转发或发送数据包)。
XDP的关键组件
XDP的实现依赖于多个关键组件,它们共同构成了XDP的数据包处理机制:
-
eBPF程序:
eBPF
(extended Berkeley Packet Filter)是XDP的核心,它允许用户定义自己的字节码来处理网络数据包。- 程序在加载时会被编译成字节码,并通过内核的eBPF验证器进行安全性检查,确保程序不会对系统造成不安全的操作。
- 常见的
XDP
程序实现包括:- 流量过滤:通过XDP过滤掉恶意或不需要的流量。
- 防火墙:基于XDP实现的防火墙规则,例如丢弃特定源IP或端口的流量。
- 负载均衡:XDP可以基于流量的特征(如源IP、目的IP)进行负载均衡。
-
网络驱动支持:
- 网络驱动需要实现
ndo_bpf
回调函数,这使得驱动能够加载和执行XDP程序。具体来说,驱动需要在接收路径中嵌入XDP hook,负责在接收到数据包时调用XDP程序。 - 支持XDP的网卡驱动包括
ixgbe
、i40e
、b44
等,这些驱动通过实现XDP hook和ndo_bpf
回调,使得XDP能够在硬件和驱动之间高效地运行。
- 网络驱动需要实现
-
内核子系统:
- bpf系统调用:用户态通过
bpf
系统调用与内核交互,加载、管理eBPF程序,指定程序的执行目标(如XDP)。 - XDP hook:XDP hook是网络驱动接收路径中的钩子点,在数据包到达网卡后,驱动会调用这个钩子,触发eBPF程序的执行。
- bpf系统调用:用户态通过
内核中的运行机制
-
xdp_buff
结构:
xdp_buff
是XDP用于存储数据包的核心结构,它包含了数据包的起始地址、结束地址、元数据区域以及网络设备的引用等信息。xdp_buff
结构的定义如下:struct xdp_buff { void *data; // 数据包起始地址 void *data_end; // 数据包结束地址 void *data_meta; // 可选的元数据区域(例如eBPF程序可以使用此区域存储额外信息) struct net_device *dev; // 数据包接收的网络设备 };
xdp_buff
结构为eBPF程序提供了对数据包的直接访问,程序可以基于这些信息决定如何处理数据包。 -
bpf_prog_run_xdp
函数:
bpf_prog_run_xdp
是用来执行XDP程序的核心函数,它调用加载的eBPF程序并传递xdp_buff
结构供其处理。函数实现如下:static inline int bpf_prog_run_xdp(struct bpf_prog *prog, struct xdp_buff *xdp) { return prog->bpf_func(xdp); }
在实际执行时,
bpf_func
是eBPF程序的处理函数,它会根据程序的逻辑处理传入的xdp_buff
结构,并返回一个动作码。 -
XDP动作码:
eBPF程序返回的动作码决定了数据包的后续处理方式。常见的XDP动作码包括:XDP_DROP
: 丢弃数据包,释放数据包的内存。XDP_PASS
: 数据包将继续进入内核协议栈处理。XDP_TX
: 数据包直接从接收端口发送回网络,不经过协议栈。XDP_REDIRECT
: 将数据包转发到其他网络设备、队列或用户态程序。
3. XDP关键内核源代码解析
在这一节中,我们将通过 Linux ixgbe网卡驱动 为例,深入解析XDP程序的核心源代码逻辑。
3.1. XDP程序的加载与执行
XDP
(eXpress Data Path)使得数据包处理可以在网卡驱动层执行,从而实现更高效的网络数据处理。ixgbe网卡驱动是一个典型的支持XDP的驱动。
3.1.1 设置XDP程序
在ixgbe网卡驱动中,XDP程序是通过 ixgbe_xdp_setup()
函数设置的,该函数会将用户加载的eBPF程序绑定到网卡驱动中:
static int ixgbe_xdp_setup(struct net_device *dev)
{
...
// 绑定BPF程序到网络设备
dev->xdp_prog = prog;
...
}
通过这一过程,XDP程序会被挂载到网络设备(例如 eth0
)上,并在数据包接收时执行。
3.1.2 数据包处理:ixgbe_clean_rx_irq()
一旦网卡收到数据包,驱动会调用 ixgbe_clean_rx_irq()
函数来处理接收到的数据包,并通过 ixgbe_run_xdp()
执行XDP程序:
static void ixgbe_clean_rx_irq(struct ixgbe_adapter *adapter, ...)
{
...
ixgbe_run_xdp(adapter, rx_ring, xdp);
}
3.2. 核心代码解析:ixgbe_run_xdp()
在 ixgbe_clean_rx_irq()
中,调用了 ixgbe_run_xdp()
函数来实际执行XDP程序,该函数的核心代码如下:
// file: drivers/net/ethernet/intel/ixgbe/ixgbe_main.c
static struct sk_buff *ixgbe_run_xdp(struct ixgbe_adapter *adapter,
struct ixgbe_ring *rx_ring,
struct xdp_buff *xdp)
{
int err, result = IXGBE_XDP_PASS;
struct ixgbe_ring *ring;
struct xdp_frame *xdpf;
u32 act;
// 获取绑定到rx_ring的XDP程序
xdp_prog = READ_ONCE(rx_ring->xdp_prog);
if (!xdp_prog) goto xdp_out;
// 预取数据包头部以提高缓存命中率
prefetchw(xdp->data_hard_start); /* xdp_frame write */
// 执行BPF程序
act = bpf_prog_run_xdp(xdp_prog, xdp);
switch (act) {
case XDP_PASS:
// 数据包正常传递给协议栈
break;
case XDP_TX:
// 将数据包转换为xdp_frame,并通过指定的环发送
xdpf = xdp_convert_buff_to_frame(xdp);
ring = ixgbe_determine_xdp_ring(adapter);
result = ixgbe_xmit_xdp_ring(ring, xdpf);
if (result == IXGBE_XDP_CONSUMED) goto out_failure;
break;
case XDP_REDIRECT:
// 如果需要重定向数据包,则执行重定向操作
err = xdp_do_redirect(adapter->netdev, xdp, xdp_prog);
if (err) goto out_failure;
result = IXGBE_XDP_REDIR;
break;
default:
// 警告无效的XDP动作
bpf_warn_invalid_xdp_action(rx_ring->netdev, xdp_prog, act);
fallthrough;
case XDP_ABORTED:
out_failure:
// 记录异常并丢弃数据包
trace_xdp_exception(rx_ring->netdev, xdp_prog, act);
fallthrough; /* handle aborts by dropping packet */
case XDP_DROP:
// 丢弃数据包
result = IXGBE_XDP_CONSUMED;
break;
}
xdp_out:
return ERR_PTR(-result);
}
3.2.1 代码解析
-
获取XDP程序
xdp_prog = READ_ONCE(rx_ring->xdp_prog)
从接收队列(rx_ring
)获取当前绑定的eBPF程序。如果没有绑定程序,函数直接跳到xdp_out
处,结束数据包处理。 -
执行BPF程序
使用bpf_prog_run_xdp(xdp_prog, xdp)
执行已加载的XDP程序,并获取返回的动作值 (act
)。这个动作值决定了数据包的后续处理方式。 -
XDP动作解析
根据XDP程序返回的动作(act
),驱动采取不同的处理策略:XDP_PASS
:数据包通过协议栈正常处理,不做任何修改。XDP_TX
:数据包通过驱动发送回网络,通常用于环回操作或快速响应。XDP_REDIRECT
:数据包将被重定向到指定的网络接口或接收队列。XDP_DROP
:丢弃数据包,防止其进入协议栈。XDP_ABORTED
:在异常情况下丢弃数据包并记录异常。
-
预取与性能优化
prefetchw(xdp->data_hard_start)
预取数据包头部,提高CPU缓存命中率,从而优化数据包处理性能。 -
重定向与发送
如果XDP程序指示重定向数据包,使用xdp_do_redirect()
将数据包发送到另一个设备或接收队列。如果是发送数据包,使用ixgbe_xmit_xdp_ring()
将数据包传送到指定的发送队列。
3.2.2 错误处理
在处理过程中,ixgbe_run_xdp()
使用了多种错误处理机制:
bpf_warn_invalid_xdp_action()
:在遇到无效的XDP动作时,记录警告信息。trace_xdp_exception()
:当XDP程序异常或数据包丢弃时,记录追踪信息。ERR_PTR(-result)
:如果发生错误,返回错误指针,表示数据包处理失败。
3.3 小结
通过对 ixgbe 网卡驱动的XDP实现的详细解析,我们理解了XDP程序如何与网卡驱动交互,并如何在数据包接收和发送过程中灵活处理不同的动作。XDP为网络数据包处理提供了极大的性能提升,特别是在需要高效过滤、重定向和快速处理流量的场景中,XDP展示了其无与伦比的优势。
4. 使用XDP的示例代码
编写 eBPF
程序
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
/// @ifindex 1
/// @flags 0
/// @xdpopts {"old_prog_fd":0}
SEC("xdp")
int xdp_pass(struct xdp_md* ctx) {
void* data = (void*)(long)ctx->data;
void* data_end = (void*)(long)ctx->data_end;
int pkt_sz = data_end - data;
bpf_printk("packet size is %d", pkt_sz);
return XDP_PASS;
}
char __license[] SEC("license") = "GPL";
这是一段 C 语言实现的 eBPF
内核侧代码,它能够通过 xdp
捕获所有经过目标网络设备的数据包,计算其大小并输出到 trace_pipe
中。
在代码中我们使用了以下注释:
/// @ifindex 1
/// @flags 0
/// @xdpopts {"old_prog_fd":0}
这是由 eunomia-bpf 提供的功能,我们可以通过这样的注释告知 eunomia-bpf 加载器此 xdp 程序想要挂载的目标网络设备编号,挂载的标志和选项。
这些变量的设计基于 libbpf 提供的 API,可以通过 patchwork 查看接口的详细介绍。
SEC("xdp")
宏指出 BPF 程序的类型,ctx
是此 BPF 程序执行的上下文,用于包处理流程。
在程序的最后,我们返回了 XDP_PASS
,这表示我们的 xdp 程序会将经过目标网络设备的包正常交付给内核的网络协议栈。可以通过 XDP actions 了解更多 xdp 的处理动作。
编译运行
通过容器编译:
docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest
或是通过 ecc
编译:
$ ecc xdp.bpf.c
Compiling bpf object...
Packing ebpf object and config into package.json...
并通过 ecli
运行:
sudo ecli run package.json
可以通过如下方式查看程序的输出:
$ sudo cat /sys/kernel/tracing/trace_pipe
node-1939 [000] d.s11 1601.190413: bpf_trace_printk: packet size is 177
node-1939 [000] d.s11 1601.190479: bpf_trace_printk: packet size is 66
ksoftirqd/1-19 [001] d.s.1 1601.237507: bpf_trace_printk: packet size is 66
node-1939 [000] d.s11 1601.275860: bpf_trace_printk: packet size is 344
5. 总结
XDP
(eXpress Data Path)是Linux内核提供的高性能数据包处理技术,它直接在网络驱动层操作数据包,在进入内核协议栈前完成处理,因此具备低延迟和高吞吐量的特点。XDP适用于DoS攻击防御、负载均衡及高效的数据包过滤等场景。与netfilter、TC、DPDK相比,XDP以更高的性能和更简单的逻辑著称,但功能相对有限。
XDP
有四种运行模式:DROP(丢弃数据包)、PASS(将数据包传给内核协议栈)、TX(从接收网卡重发数据包)和REDIRECT(转发数据包)。其核心组件包括基于eBPF编写的程序、支持XDP的网络驱动以及相关内核子系统。数据包处理流程为:网卡接收到数据后,通过驱动调用XDP hook,由eBPF程序处理并返回动作码决定后续操作。
在ixgbe网卡驱动中,XDP程序通过ixgbe_xdp_setup()
设置,并在ixgbe_clean_rx_irq()
中执行。核心函数ixgbe_run_xdp()
负责实际执行XDP程序,根据不同的动作码采取相应措施如传递、发送或丢弃数据包。错误处理机制确保了异常情况下的稳定性和安全性。
参考资源与进一步学习