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 占用越高。
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 支持。
3.3 KNI 设备创建与销毁流程
KNI 设备的生命周期由用户态 DPDK 应用程序通过 rte_kni 库接口控制,内核模块负责响应这些控制命令。
3.3.1 设备创建流程
创建 KNI 设备涉及以下步骤:
- 用户态调用
rte_kni_alloc():传入配置参数(设备名称、mbuf 池、FIFO 大小等)。 - 通过 ioctl 向内核模块发送
RTE_KNI_IOCTL_CREATE命令:携带struct rte_kni_device_info结构体。 - 内核模块处理创建请求:
- 分配
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()将设备注册到内核网络子系统。
- 分配
- 用户态获取设备句柄:返回
struct rte_kni指针,后续通过该句柄操作设备。
配置建议:FIFO 大小(
buf_size)建议设置为 1024 或 2048,过小会导致丢包,过大会浪费内存。mbuf 池大小应至少为 FIFO 大小的 2 倍。
3.3.2 设备销毁流程
销毁 KNI 设备是创建流程的逆操作:
- 用户态调用
rte_kni_release():传入要销毁的设备句柄。 - 通过 ioctl 发送
RTE_KNI_IOCTL_RELEASE命令。 - 内核模块处理销毁请求:
- 停止内核线程对设备的轮询。
- 调用
unregister_netdev()注销网络设备。 - 释放 FIFO 队列内存。
- 解除 mbuf 池的 mmap 映射。
- 释放
struct kni_dev结构体。
- 用户态清理资源:释放
struct rte_kni句柄。
常见问题:如果销毁 KNI 设备时仍有数据包在 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 |
| 设备销毁 |