DPDK KNI接口与Linux网络协议栈互通 - 第3节

3.1 KNI 内核模块工作机制

KNI(Kernel NIC Interface)是 DPDK 提供的一套内核模块与用户态库,用于在 DPDK 应用程序与 Linux 内核网络协议栈之间建立一条高效的数据通道。其核心思想是:DPDK 接管物理网卡后,通过 KNI 虚拟出一个内核网络接口(如 kni0),使得内核协议栈能够像操作普通网卡一样收发数据包。

关键设计原则:KNI 不修改内核协议栈,而是利用 Linux 内核的 net_device 框架注册虚拟设备,通过内核线程轮询 FIFO 队列完成数据交换。

3.1.1 内核模块架构

KNI 内核模块(rte_kni.ko)由以下核心组件构成:

  • 虚拟网络设备(kni_dev):每个 KNI 设备对应一个 struct net_device,注册到内核网络子系统,支持标准的 dev_queue_xmit() 和 netif_rx() 接口。
  • FIFO 队列对:每个 KNI 设备维护两个 FIFO——tx_q(内核→用户态)和 rx_q(用户态→内核),使用无锁环形缓冲区(ring buffer)实现。
  • mbuf 池映射:用户态 DPDK 通过 mmap 将 mbuf 内存池映射到内核地址空间,内核模块可直接访问 mbuf 数据区。
  • 内核线程(kni_thread):每个 KNI 设备或一组设备绑定一个内核线程,负责轮询 FIFO 并调用内核协议栈接口。
性能要点:KNI 内核线程的轮询间隔可通过 thread_interval 参数调节(默认 10 μs),间隔越小延迟越低,但 CPU 占用越高。
KNI 内核模块架构图 KNI 内核模块架构 用户态 DPDK 应用程序 rte_kni 库 mbuf 内存池 FIFO 操作接口 mmap 映射 Linux 内核空间 rte_kni.ko 内核模块 kni_dev 虚拟设备 FIFO 队列对 内核线程 (kni_thread) mmap 共享内存 Linux 网络协议栈 TCP/IP 协议栈 netif_rx / dev_queue_xmit 协议栈调用 物理网卡 (NIC) DPDK PMD 驱动 直接接管 数据直通

3.1.2 数据流路径

KNI 的数据流分为两个方向,每个方向都经过 FIFO 队列和内核线程的协作:

方向 路径 关键操作
TX 方向(内核→用户态) 内核协议栈 → kni_dev → tx_q FIFO → DPDK 应用程序 内核线程调用 kni_net_tx() 将 sk_buff 转换为 mbuf 并入队
RX 方向(用户态→内核) DPDK 应用程序 → rx_q FIFO → kni_dev → 内核协议栈 内核线程调用 kni_net_rx() 将 mbuf 转换为 sk_buff 并调用 netif_rx()
注意事项:KNI 的 TX 方向(内核→用户态)通常用于处理内核协议栈主动发出的数据包(如 ARP 请求、ICMP 回应),而 RX 方向(用户态→内核)用于将 DPDK 收到的数据包注入内核协议栈。

3.2 mbuf 与 sk_buff 的转换

mbuf 和 sk_buff 分别是 DPDK 和 Linux 内核中描述数据包的核心结构体。KNI 必须在两者之间进行高效转换,这是整个互通机制的技术难点。

3.2.1 结构体对比

属性 DPDK mbuf (struct rte_mbuf) Linux sk_buff (struct sk_buff)
内存布局 连续内存块,头部固定,数据区紧随其后 链表结构,支持分片(frag_list)
数据指针 buf_addr + data_off head + data
长度字段 pkt_len(总长度) len(线性数据长度)+ data_len(分片长度)
头部预留 headroom(默认 128 字节) head 到 data 之间的预留空间
元数据 ol_flags、tx_offload 等硬件卸载标志 cb[] 控制块、sk_buff_data_t 等

3.2.2 转换流程

KNI 内核模块中的转换函数位于 kni_misc.c 和 kni_net.c 中,核心逻辑如下:

sk_buff → mbuf(TX 方向)

// 简化后的转换逻辑
static int kni_net_tx(struct sk_buff *skb, struct net_device *dev) {
    struct kni_dev *kni = netdev_priv(dev);
    struct rte_kni_mbuf *m = NULL;

    // 1. 从 KNI 的 mbuf 池中获取一个空闲 mbuf
    if (kni_fifo_get(kni->free_q, (void **)&m, 1) != 1) {
        goto drop;  // 无可用 mbuf,丢包
    }

    // 2. 复制 sk_buff 数据到 mbuf 数据区
    m->data_off = KNI_HEADROOM_SIZE;
    m->nb_segs = 1;
    m->pkt_len = skb->len;
    m->data_len = skb->len;

    // 3. 内存拷贝(skb->data → mbuf 数据区)
    memcpy(m->buf_addr + m->data_off, skb->data, skb->len);

    // 4. 将 mbuf 放入 tx_q FIFO,等待用户态读取
    kni_fifo_put(kni->tx_q, (void **)&m, 1);

    // 5. 释放 sk_buff
    dev_kfree_skb(skb);
    return NETDEV_TX_OK;

drop:
    dev_kfree_skb(skb);
    kni->stats.tx_dropped++;
    return NETDEV_TX_OK;
}

mbuf → sk_buff(RX 方向)

// 简化后的转换逻辑
static int kni_net_rx(struct kni_dev *kni) {
    struct rte_kni_mbuf *m = NULL;
    struct sk_buff *skb = NULL;

    // 1. 从 rx_q FIFO 获取用户态放入的 mbuf
    if (kni_fifo_get(kni->rx_q, (void **)&m, 1) != 1) {
        return 0;  // 无数据
    }

    // 2. 分配 sk_buff,预留足够的头部空间
    skb = dev_alloc_skb(m->pkt_len + KNI_HEADROOM_SIZE);
    if (!skb) {
        goto drop;
    }

    // 3. 调整 sk_buff 数据指针,跳过头部预留
    skb_reserve(skb, KNI_HEADROOM_SIZE);

    // 4. 从 mbuf 数据区拷贝数据到 sk_buff
    memcpy(skb_put(skb, m->pkt_len),
           m->buf_addr + m->data_off,
           m->pkt_len);

    // 5. 设置 sk_buff 协议类型和设备
    skb->protocol = eth_type_trans(skb, kni->net_dev);
    skb->dev = kni->net_dev;

    // 6. 将 sk_buff 送入内核协议栈
    netif_rx(skb);

    // 7. 将 mbuf 放回 free_q,供用户态复用
    kni_fifo_put(kni->free_q, (void **)&m, 1);
    return 1;

drop:
    kni_fifo_put(kni->free_q, (void **)&m, 1);
    return 0;
}
性能优化点:上述转换涉及一次 memcpy,这是 KNI 的主要性能开销。对于追求极致性能的场景,可考虑使用 零拷贝 KNI(通过 KNI_USE_VFIO 或 KNI_USE_IOVA 配置),但需要硬件 IOMMU 支持。
mbuf 与 sk_buff 转换流程 mbuf ↔ sk_buff 转换流程 sk_buff 内核协议栈发出 memcpy 拷贝 数据区复制 mbuf 入队 tx_q TX 方向(内核 → 用户态) mbuf 出队 rx_q memcpy 拷贝 数据区复制 sk_buff netif_rx 送入协议栈 RX 方向(用户态 → 内核) 关键说明 每次转换均涉及一次 memcpy,这是 KNI 的主要性能瓶颈。零拷贝模式需硬件 IOMMU 支持。

3.3 KNI 设备创建与销毁流程

KNI 设备的生命周期由用户态 DPDK 应用程序通过 rte_kni 库接口控制,内核模块负责响应这些控制命令。

3.3.1 设备创建流程

创建 KNI 设备涉及以下步骤:

  1. 用户态调用 rte_kni_alloc():传入配置参数(设备名称、mbuf 池、FIFO 大小等)。
  2. 通过 ioctl 向内核模块发送 RTE_KNI_IOCTL_CREATE 命令:携带 struct rte_kni_device_info 结构体。
  3. 内核模块处理创建请求:
    • 分配 struct kni_dev 结构体。
    • 注册 struct net_device,设置回调函数(ndo_open、ndo_stop、ndo_start_xmit)。
    • 创建 FIFO 队列对(tx_q、rx_q、free_q、alloc_q)。
    • 通过 mmap 建立用户态 mbuf 池到内核的映射。
    • 启动内核线程(或绑定到已有线程)。
    • 调用 register_netdev() 将设备注册到内核网络子系统。
  4. 用户态获取设备句柄:返回 struct rte_kni 指针,后续通过该句柄操作设备。
配置建议:FIFO 大小(buf_size)建议设置为 1024 或 2048,过小会导致丢包,过大会浪费内存。mbuf 池大小应至少为 FIFO 大小的 2 倍。

3.3.2 设备销毁流程

销毁 KNI 设备是创建流程的逆操作:

  1. 用户态调用 rte_kni_release():传入要销毁的设备句柄。
  2. 通过 ioctl 发送 RTE_KNI_IOCTL_RELEASE 命令。
  3. 内核模块处理销毁请求:
    • 停止内核线程对设备的轮询。
    • 调用 unregister_netdev() 注销网络设备。
    • 释放 FIFO 队列内存。
    • 解除 mbuf 池的 mmap 映射。
    • 释放 struct kni_dev 结构体。
  4. 用户态清理资源:释放 struct rte_kni 句柄。
常见问题:如果销毁 KNI 设备时仍有数据包在 FIFO 中未处理,这些数据包会被丢弃。建议在销毁前先调用 rte_kni_handle_request() 确保所有待处理请求完成。
KNI 设备创建与销毁流程 KNI 设备创建与销毁流程 创建流程 ① rte_kni_alloc() 调用 ② ioctl CREATE 命令 ③ 内核模块处理 ④ register_netdev() ⑤ 返回设备句柄 销毁流程 ① rte_kni_release() 调用 ② ioctl RELEASE 命令 ③ 停止内核线程轮询 ④ unregister_netdev() ⑤ 释放内存与映射 注意事项 销毁前确保 FIFO 队列已清空,否则未处理的数据包会丢失。建议先调用 rte_kni_handle_request() 完成收尾。

3.3.3 关键数据结构

KNI 设备在内核中的核心数据结构 struct kni_dev 定义如下:

struct kni_dev {
    struct list_head list;           // 内核模块全局链表
    struct net_device *net_dev;      // 关联的 net_device
    struct task_struct *kni_thread;  // 内核线程

    struct rte_kni_fifo *tx_q;       // TX FIFO(内核→用户态)
    struct rte_kni_fifo *rx_q;       // RX FIFO(用户态→内核)
    struct rte_kni_fifo *free_q;     // 空闲 mbuf 队列
    struct rte_kni_fifo *alloc_q;    // 分配 mbuf 队列

    struct rte_kni_mbuf *pkt_mbuf;   // 当前正在处理的 mbuf
    void *mbuf_va;                   // mbuf 池虚拟地址
    void *mbuf_phys;                 // mbuf 池物理地址
    int mbuf_size;                   // 单个 mbuf 大小
    int mbuf_count;                  // mbuf 池数量

    char name[RTE_KNI_NAMESIZE];     // 设备名称
    struct net_device_stats stats;   // 统计信息
};
面试重点:KNI 设备创建时,free_q 和 alloc_q 用于管理 mbuf 的分配与回收。用户态从 free_q 获取空闲 mbuf,填充数据后放入 rx_q;内核处理完数据后将 mbuf 放回 free_q。这种设计避免了内核和用户态之间的频繁内存分配。

3.4 本章小结

KNI 内核模块通过虚拟网络设备、FIFO 队列对和内核线程,实现了 DPDK 与 Linux 内核协议栈之间的数据互通。mbuf 与 sk_buff 的转换是核心机制,涉及一次 memcpy 拷贝,这是性能的关键瓶颈。设备创建与销毁流程通过 ioctl 接口控制,内核模块负责完整的生命周期管理。

知识点 核心要点 实践建议
内核模块架构 kni_dev + FIFO + 内核线程 调整 thread_interval 平衡延迟与 CPU
mbuf ↔ sk_buff memcpy 拷贝,支持零拷贝模式 零拷贝需 IOMMU 支持
设备创建 ioctl CREATE → register_netdev FIFO 大小设为 1024~2048
设备销毁