理解并使用Linux内核中的XDP

理解并使用Linux内核中的XDP

1. 引言

什么是XDP

eXpress Data Path (XDP)Linux内核提供的一种高速数据包处理技术。它直接运行在驱动层,通过在网络数据包进入内核协议栈前处理数据,实现极低的延迟和高吞吐量。XDP能够将数据包处理逻辑卸载到网络驱动甚至硬件中,这使得它特别适用于对性能要求极高的网络场景。

  • 注意: XDP只会处理网卡收到的数据包, 不会处理从网卡发出的数据包

XDPnetfilter对比

特性XDPnetfilter
工作层级驱动层内核协议栈
性能极高较低
功能灵活性处理逻辑简单功能强大
应用场景DoS防护、负载均衡防火墙、NAT

XDPTC对比

特性XDPTC (Traffic Control)
工作层级驱动层网络协议栈后
性能极高较低
功能灵活性处理逻辑简单功能较强
适用场景数据包过滤、速率限制流量整形、QoS

XDPDPDK对比

特性XDPDPDK
架构内核态用户态
性能较高(与硬件支持有关)极高(与硬件紧耦合)
开发难度较低较高
使用成本免费,依赖Linux内核需要特定硬件支持

XDP的适用场景

XDP特别适合以下场景:

  • DoS攻击防御:快速丢弃恶意流量。
  • 负载均衡:高性能地将流量分发到后端服务器。
  • 数据包过滤:实现高效的ACL(访问控制列表)。

2. XDP的实现原理

数据包处理流程

XDP(eXpress Data Path)是一个基于eBPF的高性能数据包处理框架,能够在网络驱动层对数据包进行高效处理。其数据包处理流程如下:

  1. 数据包从网卡进入:网络接口卡(NIC)接收到网络数据包,并通过硬件中断将数据包传输到主机。
  2. 网卡驱动接收数据:网卡驱动通过中断或轮询的方式处理接收到的数据包。
  3. 调用XDP hook:驱动在数据包接收路径中调用XDP hook(即XDP钩子),将接收到的数据包封装成 xdp_buff 结构。
  4. eBPF程序处理数据包xdp_buff 结构传递给加载的eBPF程序进行处理,eBPF程序根据数据包的内容返回一个动作码。
  5. 根据动作码执行操作:根据eBPF程序返回的动作码(如 XDP_DROPXDP_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的网卡驱动包括 ixgbei40eb44 等,这些驱动通过实现XDP hook和ndo_bpf回调,使得XDP能够在硬件和驱动之间高效地运行。
  • 内核子系统

    • bpf系统调用:用户态通过 bpf 系统调用与内核交互,加载、管理eBPF程序,指定程序的执行目标(如XDP)。
    • XDP hook:XDP hook是网络驱动接收路径中的钩子点,在数据包到达网卡后,驱动会调用这个钩子,触发eBPF程序的执行。

内核中的运行机制

  • 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程序,根据不同的动作码采取相应措施如传递、发送或丢弃数据包。错误处理机制确保了异常情况下的稳定性和安全性。

参考资源与进一步学习

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值