惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Microsoft Azure Blog
Microsoft Azure Blog
大猫的无限游戏
大猫的无限游戏
月光博客
月光博客
V
V2EX
PCI Perspectives
PCI Perspectives
Latest news
Latest news
博客园 - 三生石上(FineUI控件)
C
CERT Recently Published Vulnerability Notes
W
WeLiveSecurity
Last Week in AI
Last Week in AI
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
P
Palo Alto Networks Blog
T
The Exploit Database - CXSecurity.com
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
WordPress大学
WordPress大学
V
Vulnerabilities – Threatpost
H
Heimdal Security Blog
Attack and Defense Labs
Attack and Defense Labs
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Hacker News: Ask HN
Hacker News: Ask HN
博客园 - 叶小钗
V
Visual Studio Blog
Jina AI
Jina AI
P
Proofpoint News Feed
罗磊的独立博客
SecWiki News
SecWiki News
J
Java Code Geeks
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
L
LINUX DO - 热门话题
Security Archives - TechRepublic
Security Archives - TechRepublic
The Hacker News
The Hacker News
Hugging Face - Blog
Hugging Face - Blog
N
News and Events Feed by Topic
NISL@THU
NISL@THU
T
Tailwind CSS Blog
T
Tenable Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Recent Announcements
Recent Announcements
H
Hacker News: Front Page
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
T
Tor Project blog
宝玉的分享
宝玉的分享
Help Net Security
Help Net Security
S
Security Affairs
Microsoft Security Blog
Microsoft Security Blog
Google DeepMind News
Google DeepMind News
F
Fortinet All Blogs
G
GRAHAM CLULEY

土法炼钢兴趣小组的算法知识备份

国密算法与国密 TLS 系列索引 【系统架构设计】架构质量属性:不只是"高可用高性能" 【系统架构设计百科】告警策略:如何避免"狼来了" 【系统架构设计】CQRS:读写分离的架构哲学 【系统架构设计】空间架构:极端扩展场景的解法 【系统架构设计】微服务架构深度审视:优势、代价与适用边界 【系统架构设计】扩展性原理:水平、垂直与对角扩展 【系统架构设计】无状态设计:扩展的第一步也是最难的一步 【系统架构设计】缓存架构:从本地到分布式的多级缓存体系 【系统架构设计】管道与过滤器:Unix 哲学的架构表达 【系统架构设计】复杂性管理:架构的核心战场 【系统架构设计】消息队列架构:异步解耦的设计与陷阱 【系统架构设计】CDN 架构:全球加速的设计原理 【系统架构设计】连接池设计:被忽视的性能杀手 【系统架构设计】弹性设计模式:熔断器、舱壁与超时 【系统架构设计】高可用设计模式:冗余、故障转移与仲裁 【系统架构设计】容量规划:从拍脑袋到数据驱动 【系统架构设计】数据库扩展:分库分表的工程实践与替代方案 【系统架构设计】SLO 工程:可靠性的量化管理 【系统架构设计】性能建模:用数学思维分析系统瓶颈 【系统架构设计】混沌工程:主动验证系统的韧性 【系统架构设计】零拷贝与内存映射:数据搬运的极致优化 【系统架构设计】线程模型:从 thread-per-request 到协程 【系统架构设计】容灾架构:多活与灾备设计 【系统架构设计】数据库性能模式:索引、查询与连接管理 【系统架构设计】数据建模:从关系范式到文档模型的真实权衡 【系统架构设计】吞吐量优化:批处理、流水线与并发模型 【系统架构设计】流处理架构:从批处理到实时的范式迁移 【系统架构设计】搜索引擎架构:倒排索引之上的系统设计 【系统架构设计】时序数据架构:监控与 IoT 的存储设计 【系统架构设计】数据迁移与版本化:在线不停机的数据演进 【系统架构设计】数据湖与数据仓库:分析架构的演进路线 【系统架构设计】API 网关设计:入口层的职责边界 【系统架构设计】应用层数据一致性模式:在正确性与性能之间走钢丝 【系统架构设计】多模数据库选型:Polyglot Persistence 的工程实践 【系统架构设计】服务发现与注册:动态拓扑的基础设施 【系统架构设计】配置管理架构:从配置文件到配置中心 【系统架构设计】全链路压测:大规模系统的性能验证 【系统架构设计】幂等性设计:分布式环境下的安全重试 【系统架构设计】契约测试与 Schema 演进:服务间的信任协议 【系统架构设计】长连接与推送架构:WebSocket、SSE 与 MQTT 【系统架构设计】延迟分析:从 P50 到 P999 的全链路追踪 【系统架构设计百科】DDD 战术模式:聚合、实体与值对象 【系统架构设计百科】防腐层与开放主机服务:系统集成的 DDD 方案 【系统架构设计百科】领域事件与事件风暴:从业务到架构的桥梁 【系统架构设计百科】CQRS + Event Sourcing 完整实战:从领域建模到部署 【系统架构设计百科】DDD 与微服务:用领域模型划分服务边界 【系统架构设计】DDD 战略设计:限界上下文与上下文映射 【系统架构设计百科】认证架构:从 Session 到 JWT 到 OIDC 【系统架构设计】API 设计哲学:REST vs GraphQL vs gRPC 的真实权衡 排序算法专题:从 TimSort 到并行排序 【密码学百科】国密算法体系:SM2/SM3/SM4/SM9 全景解读 【密码学百科】承诺方案:Pedersen 承诺、向量承诺与多项式承诺 【密码学百科】不经意传输与隐私信息检索:OT、OT 扩展与 PIR 【密码学百科】门限密码学:门限签名、门限解密与分布式密钥生成 完美哈希:从理论到 gperf 实践 【密码学百科】安全多方计算:从 Yao 的混淆电路到实用 MPC 【密码学百科】同态加密:从 Paillier 到全同态加密(FHE) 【密码学百科】零知识证明系统:zk-SNARKs、zk-STARKs 与 Bulletproofs 【密码学百科】概率论与密码分析:生日攻击、差分分析与线性分析 【密码学百科】计算复杂性与归约:密码安全性证明的基石 【密码学百科】秘密共享:Shamir 方案、VSS 与安全多方计算入口 【密码学百科】椭圆曲线代数:Weierstrass 方程、点群运算与曲线选择 【密码学百科】离散对数与配对密码学:从 DLP 到 BLS 签名 【密码学百科】格密码数学基础:SVP、LWE 与格基约化 【密码学百科】抽象代数:群、环、域的密码学视角 【密码学百科】有限域算术:GF(2^n) 运算与在 AES/ECC 中的应用 【密码学百科】数论进阶:二次剩余、椭圆曲线上的 Weil 配对 【密码学百科】密码学简史:从凯撒密码到量子时代 【密码学百科】威胁模型与安全目标:CIA 三要素之外 【密码学百科】Kerckhoffs 原则与现代密码设计哲学 【密码学百科】随机性:密码学的基石 【密码学百科】信息论入门:熵、完美保密与 Shannon 定理 【密码学百科】分组密码原理:Feistel 网络与 SPN 结构 【密码学百科】AES 逐步拆解:SubBytes 到 MixColumns 的数学 【密码学百科】分组密码工作模式全览:ECB/CBC/CTR/OFB/CFB 【密码学百科】流密码:RC4 的兴衰与 ChaCha20 的崛起 【密码学百科】密码学哈希函数:MD5→SHA-2→SHA-3 的进化之路 【密码学百科】MAC 与 HMAC:消息认证的正确姿势 【密码学百科】认证加密(AEAD):GCM、ChaCha20-Poly1305 与 OCB 【密码学百科】密钥派生函数:HKDF、PBKDF2、Argon2 与密码存储 【密码学百科】公钥密码的数论基础:模运算、群、原根 【密码学百科】RSA 从原理到攻击:教科书 RSA 为什么不安全 【密码学百科】Diffie-Hellman 密钥交换与离散对数问题 【密码学百科】椭圆曲线密码学(ECC):从几何直觉到点群运算 【密码学百科】数字签名:ECDSA、EdDSA 与 Schnorr 签名 【密码学百科】现代密钥交换:X25519、ECDHE 与前向保密 【密码学百科】混合加密与 KEM/DEM 范式:ECIES 与 HPKE 【密码学百科】填充方案:PKCS#1 v1.5、OAEP 与 PSS 【密码学百科】TLS 协议全解析:从握手到 0-RTT 【密码学百科】PKI 与数字证书:信任链的构建与崩塌 【密码学百科】密码认证协议:从 SRP 到 OPAQUE 【密码学百科】零知识证明入门:如何证明你知道而不泄露 【密码学百科】安全信道构造:Noise 协议框架与 Signal 协议 【密码学百科】密钥管理工程:HSM、KMS 与密钥生命周期 【密码学百科】侧信道攻击:从时序攻击到功耗分析 【密码学百科】密码学实现陷阱:三层漏洞分类、审计工具链与系统性预防 密码敏捷性:如何设计可升级的密码系统 【密码学百科】OpenSSL/BoringSSL 架构剖析:ENGINE、Provider 与 FIPS 模块 排序基准测试:用数据说话
【Linux 网络子系统深度拆解】XDP 内核实现:在驱动层重编程网络栈
2025-07-21 · via 土法炼钢兴趣小组的算法知识备份

传统网络栈处理一个包的完整路径——从驱动到 socket——需要约 2-5 微秒。对于 DDoS 防护、负载均衡、包过滤等场景,大部分包在入口就应该被丢弃或转发,走完整协议栈是巨大的浪费。XDP(eXpress Data Path)把 eBPF 程序注入到网卡驱动的 NAPI 轮询中,在 sk_buff 分配之前就决定包的命运。这使得单核包处理速度从传统路径的 1-2 Mpps 提升到 24+ Mpps。

但 XDP 不是”旁路”——它是内核网络栈的一部分,与 page pool、NAPI、GRO、TC 紧密集成。理解 XDP 的内核实现,才能正确使用它而不引入诡异的 bug。

本文基于 Linux 6.6/6.8 源码,拆解 XDP 的完整内核实现。

XDP 内核实现全景

一、核心数据结构

1.1 xdp_buff:XDP 的包描述符

XDP 程序操作的不是 sk_buff,而是更轻量的 xdp_buff

// include/net/xdp.h
struct xdp_buff {
    void *data;            // 包数据起始(L2 头)
    void *data_end;        // 包数据结束
    void *data_meta;       // 元数据区域(data 之前)
    void *data_hard_start; // 帧缓冲区物理起始(含 headroom)
    struct xdp_rxq_info *rxq; // 接收队列信息
    struct xdp_txq_info *txq; // 发送队列信息(可选)
    u32 frame_sz;          // 帧总大小
    u32 flags;             // XDP_FLAGS_HAS_FRAGS 等
};

sk_buff(约 256 字节,包含路由缓存、conntrack 引用、socket 指针等)相比,xdp_buff 仅 40 字节,这是 XDP 性能优势的结构基础。

内存布局:

data_hard_start                    data_hard_end
│                                       │
▼                                       ▼
┌──────────┬──────────┬────────────┬──────────────────┐
│ headroom │ metadata │  数据 (L2→) │ skb_shared_info  │
└──────────┴──────────┴────────────┴──────────────────┘
            ▲          ▲            ▲
            data_meta  data         data_end
  • headroomdata_hard_startdata_meta 之间,供 bpf_xdp_adjust_head() 扩展头部
  • metadatadata_metadata 之间,供驱动传递硬件信息(时间戳、RSS 哈希)
  • skb_shared_info:帧尾部,用于多缓冲区的 fragment 链表

1.2 xdp_frame:传输用帧描述符

当 XDP 决定转发(XDP_TX/XDP_REDIRECT)时,xdp_buff 被转换为 xdp_frame,存储在 headroom 中:

// include/net/xdp.h
struct xdp_frame {
    void *data;               // 数据指针
    u16 len;                  // 数据长度
    u16 headroom;             // headroom 大小
    u32 metasize;             // 元数据大小(低 8 位)
    struct xdp_mem_info mem;  // 内存模型(page pool/UMEM 等)
    struct net_device *dev_rx; // 原始接收设备
    u32 frame_sz;             // 帧大小
    u32 flags;                // 标志
};

转换通过 xdp_convert_buff_to_frame() 完成,将 xdp_frame 结构体内联写入 headroom 区域,避免额外内存分配。

1.3 xdp_rxq_info:接收队列上下文

// include/net/xdp.h
struct xdp_rxq_info {
    struct net_device *dev;    // 接收设备
    u32 queue_index;           // 队列编号
    u32 reg_state;             // 注册状态
    struct xdp_mem_info mem;   // 内存模型
    unsigned int napi_id;      // NAPI 实例 ID
    u32 frag_size;             // fragment 大小
} ____cacheline_aligned;       // cache line 对齐(性能关键)

XDP 程序通过 ctx->rxq->dev 获取接收设备信息,通过 ctx->rxq->queue_index 获取队列编号(用于 RSS 感知路由)。

1.4 内存模型枚举

// include/net/xdp.h
enum xdp_mem_type {
    MEM_TYPE_PAGE_SHARED = 0, // 页引用计数模型
    MEM_TYPE_PAGE_ORDER0,     // 独占 order-0 页
    MEM_TYPE_PAGE_POOL,       // page pool 管理
    MEM_TYPE_XSK_BUFF_POOL,   // AF_XDP UMEM
};

page pool 模型(MEM_TYPE_PAGE_POOL)是现代驱动的首选:XDP_DROP 时页直接回到 page pool,无需走伙伴系统释放。

2.1 三种模式对比

模式 钩子位置 sk_buff 性能 兼容性
Native 驱动 NAPI poll 未分配 最高(24+ Mpps) 需驱动支持
Generic netif_receive_skb() 已分配 较低(3-5 Mpps) 所有设备
Offloaded NIC 固件 不涉及 线速 SmartNIC

2.2 Native XDP:驱动级钩子

Native XDP 通过 ndo_bpf 回调注册:

// include/linux/netdevice.h
struct net_device_ops {
    // ...
    int (*ndo_bpf)(struct net_device *dev, struct netdev_bpf *bpf);
    // ...
};

驱动在 NAPI 轮询中执行 XDP 程序:

// 驱动 NAPI poll(概念性,以 ixgbe 风格为例)
static int driver_napi_poll(struct napi_struct *napi, int budget)
{
    while (budget > 0) {
        // 1. 从 ring buffer 取出 DMA 完成的帧
        struct xdp_buff xdp;
        xdp.data = page_address(page) + offset;
        xdp.data_end = xdp.data + len;
        xdp.data_hard_start = page_address(page);
        xdp.rxq = &rx_ring->xdp_rxq;
        xdp.frame_sz = PAGE_SIZE;

        // 2. 执行 XDP 程序
        u32 act = bpf_prog_run_xdp(xdp_prog, &xdp);

        // 3. 根据返回值处理
        switch (act) {
        case XDP_DROP:
            // 直接回收页到 page pool
            page_pool_recycle_direct(pool, page);
            break;
        case XDP_PASS:
            // 分配 sk_buff,走正常协议栈
            skb = xdp_build_skb_from_frame(...);
            napi_gro_receive(napi, skb);
            break;
        case XDP_TX:
            // 从同一设备发出
            driver_xdp_xmit(dev, &xdp);
            break;
        case XDP_REDIRECT:
            // 队列化重定向
            xdp_do_redirect(dev, &xdp, xdp_prog);
            break;
        case XDP_ABORTED:
            trace_xdp_exception(dev, xdp_prog, act);
            page_pool_recycle_direct(pool, page);
            break;
        }
        budget--;
    }

    // NAPI 轮询结束时 flush 重定向批次
    xdp_do_flush();
    return processed;
}

关键点:

  • XDP 在 sk_buff 分配之前 执行,这是性能优势的根本来源
  • XDP_DROP 时仅做页回收(~24ns),比 iptables DROP(~2000ns)快 80 倍
  • bpf_prog_run_xdp() 直接执行 JIT 编译后的机器码

2.3 bpf_prog_run_xdp:程序执行入口

// include/net/xdp.h
static __always_inline u32
bpf_prog_run_xdp(const struct bpf_prog *prog, struct xdp_buff *xdp)
{
    u32 act = __bpf_prog_run(prog, xdp, BPF_DISPATCHER_FUNC(xdp));

    // 特殊处理:bonding 设备的 XDP_TX
    if (static_branch_unlikely(&bpf_master_redirect_enabled_key)) {
        if (act == XDP_TX && netif_is_bond_slave(xdp->rxq->dev))
            act = xdp_master_redirect(xdp);
    }
    return act;
}

__bpf_prog_run() 调用 JIT 编译后的本地代码。在 x86_64 上,一个简单的 XDP_DROP 程序编译为约 10 条机器指令。

2.4 Generic XDP:通用兼容模式

Generic XDP 在 netif_receive_skb() 之后执行,此时 sk_buff 已经分配:

// net/core/dev.c (概念性)
static int netif_receive_skb_internal(struct sk_buff *skb)
{
    // ... 已分配 sk_buff ...

    // Generic XDP 执行点
    if (static_branch_unlikely(&generic_xdp_needed_key)) {
        struct bpf_prog *xdp_prog = rcu_dereference(skb->dev->xdp_prog);
        if (xdp_prog) {
            // 从 sk_buff 构建临时 xdp_buff
            // 执行 XDP 程序
            // 根据返回值决定继续或丢弃
        }
    }
    // ... 继续正常路径 ...
}

Generic XDP 的局限:

  • 性能:sk_buff 已分配,失去了 XDP 的核心优势
  • 功能:部分 helper 函数不可用(如 bpf_xdp_adjust_tail()
  • 适用场景:驱动不支持 native XDP 时的降级方案

三、五种 XDP 动作路径

3.1 XDP_DROP:极速丢包

case XDP_DROP:
    page_pool_recycle_direct(pool, page);
    // 或 page_frag_free(data);

这是 XDP 最常见的用途——DDoS 防护。页直接回收到 page pool,不触发任何协议栈代码。

性能基准:单核约 24 Mpps(对比 iptables DROP ~3 Mpps)。

3.2 XDP_PASS:递交协议栈

case XDP_PASS:
    // xdp_buff → xdp_frame → sk_buff
    struct sk_buff *skb = xdp_build_skb_from_frame(xdpf, dev);
    if (skb)
        napi_gro_receive(napi, skb);

xdp_build_skb_from_frame() 的开销约 200-500ns,包括 sk_buff 分配和头部初始化。如果 XDP 程序只是做统计而不过滤,XDP_PASS 的额外开销使得 native XDP 不如不挂载 XDP 程序。

3.3 XDP_TX:原设备回发

case XDP_TX:
    // 从同一网卡的 TX 队列发出
    xdp_convert_buff_to_frame(&xdp);  // 转换为 xdp_frame
    driver_xdp_xmit(dev, 1, &xdpf, 0);

典型场景:负载均衡器修改 MAC/IP 头后原路返回。注意 XDP_TX 使用接收设备的 TX 队列,可能与正常发包路径竞争锁。

3.4 XDP_REDIRECT:重定向

case XDP_REDIRECT:
    xdp_do_redirect(dev, &xdp, xdp_prog);
    // 帧暂存在 per-CPU 批次队列中
    // NAPI 结束时 xdp_do_flush() 批量发送

XDP_REDIRECT 是最灵活的动作,通过 BPF map 指定目标:

Map 类型 目标 用途
BPF_MAP_TYPE_DEVMAP 其他网卡 跨设备转发
BPF_MAP_TYPE_DEVMAP_HASH 其他网卡(哈希索引) 灵活转发
BPF_MAP_TYPE_CPUMAP 其他 CPU 负载分摊
BPF_MAP_TYPE_XSKMAP AF_XDP socket 用户态收包

3.5 XDP_ABORTED:异常处理

case XDP_ABORTED:
    trace_xdp_exception(dev, xdp_prog, act);
    // 触发 tracepoint,便于调试
    page_pool_recycle_direct(pool, page);

XDP_ABORTED 在 BPF 程序返回无效值或调用 bpf_throw() 时触发。与 XDP_DROP 的区别是会触发 xdp:xdp_exception tracepoint。

四、重定向机制详解

4.1 批量重定向架构

XDP_REDIRECT 不是立即发送,而是暂存后批量 flush:

// include/net/xdp.h
#define XDP_BULK_QUEUE_SIZE 16

struct xdp_frame_bulk {
    int count;
    void *xa;                         // 当前目标设备
    void *q[XDP_BULK_QUEUE_SIZE];     // 帧缓冲区
};

流程:

包 1 → xdp_do_redirect() → 暂存到 bulk queue
包 2 → xdp_do_redirect() → 暂存到 bulk queue
...
包 N → NAPI 轮询结束
     → xdp_do_flush()
       → 对每个目标设备批量调用 ndo_xdp_xmit()

批量发送的优势:

  • 减少 TX 锁竞争:一次锁获取发送多个帧
  • 提高 DMA 效率:批量 DMA 映射
  • 减少门铃(doorbell)写入:一次通知网卡多个帧

4.2 DEVMAP:跨设备转发

// BPF 程序中
SEC("xdp")
int xdp_redirect_prog(struct xdp_md *ctx)
{
    // 查找目标设备
    return bpf_redirect_map(&devmap, ctx->rx_queue_index, 0);
}

// 用户态创建 devmap
struct bpf_map_def devmap = {
    .type = BPF_MAP_TYPE_DEVMAP,
    .key_size = sizeof(u32),
    .value_size = sizeof(u32),  // 目标 ifindex
    .max_entries = 64,
};

DEVMAP 支持附加 XDP 程序:每个 map 条目可以关联一个 XDP 程序,在重定向到目标设备前执行(用于修改头部)。

4.3 CPUMAP:跨 CPU 分发

CPUMAP 把 XDP 帧从一个 CPU 转移到另一个 CPU 处理,适用于:

  • 单队列网卡需要多核处理
  • 重平衡 RSS 分配不均的流
// CPUMAP 内部使用 per-CPU FIFO 队列
// 源 CPU:xdp_do_redirect() → 放入目标 CPU 的队列
// 目标 CPU:kthread 或 NAPI 轮询 → 从队列取帧
//   → 可选:执行附加的 XDP 程序
//   → xdp_build_skb_from_frame() → 进入协议栈

CPUMAP 目标 CPU 上的帧会被转换为 sk_buff 后进入正常协议栈。这意味着 CPUMAP 主要用于把 XDP 前置过滤与后续协议栈处理分摊到不同核。

4.4 XSKMAP:AF_XDP 通道

XSKMAP 把帧重定向到 AF_XDP socket,实现内核到用户态的零拷贝传输。

// include/net/xdp_sock.h
struct xsk_map {
    struct bpf_map map;
    spinlock_t lock;
    atomic_t count;
    struct xdp_sock __rcu *xsk_map[];
};

五、AF_XDP:用户态高速收包

AF_XDP 是 XDP 生态中最具革命性的组件——它提供了一条从网卡到用户态的零拷贝数据通道,性能可达 100+ Mpps。

5.1 核心架构

// include/net/xdp_sock.h
struct xdp_sock {
    struct sock sk;              // 内嵌 socket
    struct xsk_queue *rx;        // 接收描述符环
    struct net_device *dev;      // 绑定设备
    struct xdp_umem *umem;       // 用户内存区域
    struct xsk_buff_pool *pool;  // 缓冲区池
    u16 queue_id;                // 绑定队列
    bool zc;                     // 零拷贝模式
    bool sg;                     // 散列聚集
    struct xsk_queue *tx;        // 发送描述符环
    u64 rx_dropped;              // 丢包计数
    u64 rx_queue_full;           // 队列满计数
};

5.2 UMEM:共享内存区域

// include/net/xdp_sock.h
struct xdp_umem {
    void *addrs;          // 内存映射地址
    u64 size;             // 总大小
    u32 headroom;         // 每帧预留 headroom
    u32 chunk_size;       // 每帧大小(通常 4096 或 2048)
    u32 chunks;           // 帧总数
    u32 npgs;             // 页数
    bool zc;              // 零拷贝标志
    struct page **pgs;    // 物理页数组
};

UMEM 是用户态和内核共享的内存区域。用户态通过 mmap() 映射,内核(驱动)直接 DMA 写入。

5.3 四环架构

AF_XDP 使用四个无锁环形缓冲区在用户态和内核之间传递帧描述符:

                  用户态                        内核
              ┌───────────┐               ┌───────────┐
     应用填充 │ Fill Ring │ ──空帧描述符─→ │  驱动 RX   │
              └───────────┘               └───────────┘
              ┌───────────┐               ┌───────────┐
     应用消费 │  RX Ring  │ ←─已填充帧──── │  驱动 RX   │
              └───────────┘               └───────────┘
              ┌───────────┐               ┌───────────┐
     应用提交 │  TX Ring  │ ──待发送帧──→ │  驱动 TX   │
              └───────────┘               └───────────┘
              ┌───────────┐               ┌───────────┐
     应用回收 │Comp. Ring │ ←─已发送帧──── │  驱动 TX   │
              └───────────┘               └───────────┘
  • Fill Ring:用户态告诉内核”这些帧可以接收数据”
  • RX Ring:内核告诉用户态”这些帧已收到数据”
  • TX Ring:用户态告诉内核”请发送这些帧”
  • Completion Ring:内核告诉用户态”这些帧已发送完”

5.4 零拷贝 vs 拷贝模式

// 零拷贝模式
// 驱动直接 DMA 到 UMEM 页面
// 用户态读取时无数据拷贝
// 要求:驱动实现 ndo_xsk_wakeup
// 性能:100+ Mpps

// 拷贝模式
// 驱动 DMA 到普通页,内核拷贝到 UMEM
// 兼容所有设备
// 性能:20-50 Mpps

零拷贝模式需要驱动实现 ndo_xsk_wakeup() 回调,目前支持的主流驱动包括 i40e、ice、mlx5、ixgbe 等。

5.5 AF_XDP 选项

// include/uapi/linux/if_xdp.h
#define XDP_SHARED_UMEM     (1 << 0)  // 多 socket 共享 UMEM
#define XDP_COPY            (1 << 1)  // 强制拷贝模式
#define XDP_ZEROCOPY        (1 << 2)  // 强制零拷贝
#define XDP_USE_NEED_WAKEUP (1 << 3)  // 唤醒标志优化
#define XDP_USE_SG          (1 << 4)  // 散列聚集(多缓冲区)

XDP_USE_NEED_WAKEUP 是重要的功率优化:用户态在 Fill Ring 空时不盲目 poll,而是通过 need_wakeup 标志判断是否需要调用 sendto() 唤醒内核。

六、多缓冲区支持

6.1 问题背景

早期 XDP 要求整个包在一个连续帧中(通常 4KB)。这限制了 XDP 无法处理:

  • Jumbo frame(9KB+)
  • GRO 聚合后的大包
  • TSO 产生的大包

6.2 XDP multi-buffer 实现

// include/net/xdp.h
// 通过 flags 标志启用
#define XDP_FLAGS_HAS_FRAGS  BIT(0)

// 检测多缓冲区
static inline bool xdp_buff_has_frags(struct xdp_buff *xdp)
{
    return !!(xdp->flags & XDP_FLAGS_HAS_FRAGS);
}

// 获取完整长度(含 fragment)
static inline u32 xdp_get_buff_len(struct xdp_buff *xdp)
{
    u32 len = xdp->data_end - xdp->data;
    if (xdp_buff_has_frags(xdp)) {
        struct skb_shared_info *sinfo = xdp_get_shared_info_from_buff(xdp);
        len += sinfo->xdp_frags_size;
    }
    return len;
}

// fragment 存储在帧尾部的 skb_shared_info 中
#define xdp_data_hard_end(xdp) \
    ((xdp)->data_hard_start + (xdp)->frame_sz - \
     SKB_DATA_ALIGN(sizeof(struct skb_shared_info)))

多缓冲区的 XDP 包由头部帧 + N 个 fragment 组成,fragment 信息复用 skb_shared_info 结构体。BPF 程序通过 helper 函数访问各 fragment 的数据。

七、XDP 元数据

7.1 驱动到 BPF 的元数据传递

驱动可以在 data_meta 区域存储硬件信息,供 XDP 程序读取:

// 驱动设置元数据
xdp.data_meta = xdp.data - sizeof(struct my_metadata);
struct my_metadata *meta = xdp.data_meta;
meta->rx_timestamp = hw_timestamp;
meta->rx_hash = hw_rss_hash;

// BPF 程序读取
SEC("xdp")
int prog(struct xdp_md *ctx) {
    void *data = (void *)(long)ctx->data;
    void *meta = (void *)(long)ctx->data_meta;
    if (meta + sizeof(struct my_metadata) > data)
        return XDP_PASS;
    struct my_metadata *m = meta;
    // 使用 m->rx_timestamp, m->rx_hash
}

7.2 标准元数据操作

// include/net/xdp.h
struct xdp_metadata_ops {
    int (*xmo_rx_timestamp)(const struct xdp_md *ctx, u64 *timestamp);
    int (*xmo_rx_hash)(const struct xdp_md *ctx, u32 *hash,
                       enum xdp_rss_hash_type *rss_type);
    int (*xmo_rx_vlan_tag)(const struct xdp_md *ctx, __be16 *vlan_proto,
                           u16 *vlan_tci);
};

6.x 内核标准化了元数据接口,驱动通过 kfunc 向 BPF 程序暴露硬件信息。BPF 程序使用 bpf_xdp_metadata_rx_timestamp() 等 kfunc 获取数据,无需了解底层驱动细节。

7.3 BPF 头部调整

// XDP 程序可以调整包头部和元数据区域
bpf_xdp_adjust_head(xdp, delta);  // 扩展/收缩头部
bpf_xdp_adjust_meta(xdp, delta);  // 扩展/收缩元数据
bpf_xdp_adjust_tail(xdp, delta);  // 扩展/收缩尾部

bpf_xdp_adjust_head() 是封装/解封装的基础:负 delta 扩展 headroom(添加外层头),正 delta 收缩(剥离头部)。

八、XDP 与内核子系统的集成

8.1 XDP 与 page pool

现代 XDP 驱动使用 page pool 管理帧页面:

page_pool 分配 → DMA 映射 → 网卡 ring buffer
  → DMA 完成 → XDP 程序执行
    → XDP_DROP: page_pool_recycle_direct()    // 直接回收
    → XDP_PASS: 页转移给 sk_buff(skb_mark_for_recycle)
    → XDP_TX:   页在 TX 完成后回收
    → XDP_REDIRECT: 页转移给目标设备

XDP_DROP + page pool 组合是性能最优路径:无 sk_buff 分配、无 DMA 解映射、无伙伴系统调用。

8.2 XDP 与 GRO 的关系

时间线:
  驱动 NAPI poll
    → 每个包执行 XDP 程序(逐包处理)
    → XDP_PASS → napi_gro_receive()(GRO 聚合)
    → XDP_DROP → 直接回收(跳过 GRO)

XDP 在 GRO 之前执行,这意味着 XDP 程序看到的是原始小包,不是 GRO 聚合后的大包。这既是优势(可以精确过滤单个包)也是限制(无法利用 GRO 的聚合减少处理次数)。

8.3 XDP 与 TC 的关系

收包路径:
  XDP(驱动层)→ GRO → TC ingress(qdisc 层)→ Netfilter → 协议栈

发包路径:
  协议栈 → Netfilter → TC egress → 驱动 xmit

XDP 在 TC 之前,两者可以配合使用:XDP 做快速过滤,TC BPF 做精细分类。

九、可观测性实战

9.1 XDP tracepoint

# 追踪 XDP 动作分布
bpftrace -e '
tracepoint:xdp:xdp_redirect* {
    @redirect[probe] = count();
}
tracepoint:xdp:xdp_exception {
    @exception[args->act] = count();
}
interval:s:5 {
    print(@redirect);
    print(@exception);
    clear(@redirect);
    clear(@exception);
}
'

9.2 追踪 XDP 性能

# 测量 XDP 程序的执行延迟
bpftrace -e '
kprobe:bpf_prog_run_xdp {
    @start[tid] = nsecs;
}
kretprobe:bpf_prog_run_xdp /@start[tid]/ {
    @xdp_latency_ns = hist(nsecs - @start[tid]);
    delete(@start[tid]);
}
interval:s:10 { print(@xdp_latency_ns); clear(@xdp_latency_ns); }
'

9.3 AF_XDP 监控

# 查看 AF_XDP socket 统计
cat /proc/net/xsk

# 追踪 AF_XDP 丢包
bpftrace -e '
kprobe:xsk_rcv_check {
    @xsk_check = count();
}
kprobe:xsk_drop {
    @xsk_drop = count();
}
interval:s:5 {
    print(@xsk_check);
    print(@xsk_drop);
}
'

9.4 XDP 错误诊断

# 追踪 XDP_ABORTED(程序错误)
bpftrace -e '
tracepoint:xdp:xdp_exception {
    printf("XDP exception: dev=%s act=%d prog_id=%d\n",
           str(args->dev->name), args->act, args->prog_id);
    @exceptions = count();
}
'

# 查看 XDP 程序信息
bpftool prog show type xdp
bpftool net show

9.5 perf 分析 XDP CPU 开销

# XDP 与协议栈的 CPU 对比
perf record -g -C 0 -- sleep 10
perf report --no-children | head -30
# 观察 bpf_prog_run_xdp 与 netif_receive_skb 的比例

十、调优参数速查

参数 说明
XDP 模式选择 ip link set dev eth0 xdp obj prog.o(native)
ip link set dev eth0 xdpgeneric obj prog.o(generic)
AF_XDP 队列 ethtool -L eth0 combined N 调整队列数
AF_XDP chunk size UMEM chunk_size(2048 或 4096,影响内存效率)
Fill Ring 大小 建议 ≥ 2 × batch_size(避免饥饿)
批量大小 AF_XDP poll 返回批次(默认 64)
need_wakeup 开启 XDP_USE_NEED_WAKEUP(减少无效系统调用)
IRQ 亲和 XDP 处理 CPU = 中断 CPU

性能调优建议

# 1. 确保 native 模式
ip link show dev eth0 | grep xdp
# 应显示 xdp 而非 xdpgeneric

# 2. 绑定 XDP 处理到正确 CPU
# 通过 RSS/IRQ affinity 控制

# 3. 确保 page pool 启用
ethtool -S eth0 | grep page_pool

# 4. AF_XDP 零拷贝确认
# 创建 socket 时指定 XDP_ZEROCOPY
# 如果失败检查驱动支持

# 5. 批量大小优化
# 调整 NAPI weight 和 AF_XDP batch

十一、总结

XDP 的性能优势来自三个架构决策:

决策 传统路径 XDP
包描述符 sk_buff(256B) xdp_buff(40B)
分配时机 每包分配 sk_buff 仅 XDP_PASS 时分配
处理位置 协议栈全路径 驱动 NAPI 轮询

XDP 的五种动作覆盖了网络入口的所有需求:

  • XDP_DROP:DDoS 防护(24+ Mpps)
  • XDP_PASS:监控/统计后递交协议栈
  • XDP_TX:负载均衡器原路返回
  • XDP_REDIRECT + DEVMAP:跨设备 L2 转发
  • XDP_REDIRECT + XSKMAP:AF_XDP 零拷贝用户态收包

AF_XDP 的四环零拷贝架构进一步打通了内核到用户态的数据通道,使得纯用户态网络应用(如 DPDK 风格但无需绕过内核)成为可能。

参考文献

  1. Linux 内核源码 include/net/xdp.h(XDP 核心结构体与函数)
  2. Linux 内核源码 include/net/xdp_sock.h(AF_XDP socket 实现)
  3. Linux 内核源码 include/net/xsk_buff_pool.h(XSK 缓冲区池)
  4. Jesper Dangaard Brouer, Toke Høiland-Jørgensen, “XDP — Scalable Packet Processing”, Netdev 2.1, 2017
  5. Björn Töpel, Magnus Karlsson, “AF_XDP — Scalable User-Space Networking”, Linux Plumbers Conference, 2018
  6. Toke Høiland-Jørgensen et al., “The eXpress Data Path: Fast Programmable Packet Processing in the Operating System Kernel”, CoNEXT, 2018

上一篇网络子系统内存管理:sk_buff 分配、page pool 与 NUMA

下一篇eBPF 网络钩子全景:TC/XDP/socket/cgroup

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2025-07-22 · linux / networking

【Linux 网络子系统深度拆解】eBPF 网络钩子全景:TC/XDP/socket/cgroup

从内核源码全面拆解 eBPF 在网络子系统中的所有挂载点:TC BPF direct-action 模式与 bpf_mprog 多程序链、XDP 驱动级钩子回顾、socket ops 回调与 TCP 生命周期事件、cgroup BPF 策略控制、sk_msg/sk_skb 的 sockmap 重定向引擎、struct_ops 实现自定义拥塞控制,以及 bpftrace 可观测实战。