作者:清潇静凌 | 来源:互联网 | 2023-09-12 19:16
DPDK是一个优秀的收发包kit,但它本身并不提供用户态协议栈,因此由将数据报文注入内核协议栈的需求,也就是KNI(KernelNICInterface)。作为用户态和内核的接口,
DPDK是一个优秀的收发包kit,但它本身并不提供用户态协议栈,因此由将数据报文注入内核协议栈的需求,也就是KNI(Kernel NIC Interface)。作为用户态和内核的接口,其因为没有系统调用和内存拷贝,因此比传统的tun/tap设备要更高效。
借用DPDK文档的一个KNI的结构图。
图1. kni结构图
毫无疑问,KNI必然要也需要内核模块的支持,即rte_kni.ko。其共有三个参数,分别是lo_mode,kthread_mode和carrier。
lo_mode可配置为lo_mode_none,lo_mode_fifo,和lo_mode_fifo_skb,默认为lo_mode_none。另外两个在实际产品中基本不会用到。
kthread_mode可配置为single和multiple,默认为single。
carrier可配置为off和on,默认为off。
模块初始化函数kni_init也非常简单。除了解析上面的参数配置外,比较重要的就是注册misc设备和配置lo_mode。
图2. kni_init
图3. kni_net_config_lo_mode
配置lo_mode,函数指针kni_net_rx_func指向不同的函数,默认是kni_net_rx_func。
通过register_pernet_subsys或者register_pernet_gen_subsys,注册了kni_net_ops,保证每个namespace都会调用kni_init_net进行初始化(初始化动作在此不介绍了)。
注册为misc设备后,其工作机制由注册的miscdevice决定,即
图4. kni_misc
先看open函数kni_open,
图5. kni_open
代码非常简单,检查保证一个namespace只能打开kni一次,打开后将kni基于namespace的私有数据赋值给打开的文件file->private_data,以便后面使用。
DPDK在初始化阶段会调用rte_kni_init,打开kni设备。
图6. rte_kni_init
如何使用kni设备呢?内核的kni模块,提供了ioctl的支持。
图7. kni_ioctl
一共两个有效的option,RTE_KNI_IOCTL_CREATE和RTE_KNI_IOCTL_RELEASE,分别对应DPDK用户态的rte_kni_alloc和rte_kni_release,即申请kni interface和释放kni interface。
在rte_kni_alloc中,关键的代码是kni_reserve_mz申请连续的物理内存,并用其作为各个ring。
图8. rte_kni_alloc
而在kni的内核实现中,
图9. kni_ioctl_create
通过phys_to_virt将ring的物理地址转成虚拟地址使用,这样就保证了KNI的用户态和内核态使用同一片物理地址,从而做到零拷贝。
然后就是注册是netdev,启动内核接收线程。
图10. kni_ioctl_create
进入kni_run_thread,
图11. kni_run_thread
如果KNI模块的参数指定了多线程模式,每创建一个kni设备,就创建一个内核线程。如果为单线程模式,则检查是否已经启动了kni_thread。没有的话,创建唯一的kni内核thread kni_single,有的话,则什么都不做。
不失一般性,可以看kni_thread_single的实现。
图12. kni_thread_single
在持有读锁的情况下,遍历所有的kni设备,执行接收动作。这时,根据rte_kni.ko加载时的模块参数lo_mode
的值不同,执行不同的动作。只关心实际使用的lo_mode_none模式,其处理函数为:
图13. kni_net_rx_normal(1)
检查释放队列是否还有空位,没有的话,意味着读取后的数据无法增加到释放队列,故直接返回。
从kni->rx_q读取数据到kni->pa中。没有任何报文,则直接返回。
图14. kni_net_rx_normal(2)
循环处理收到的kni数据,将数据复制到申请的skb中。
图15. kni_net_rx_normal(3)
设置skb相关参数,调用netif_rx_ni将skb传给内核协议栈处理。最后把读取的数据追加到释放队列中。
这是DPDK app向KNI设备写入数据,也就是发给内核的情况。当内核从KNI设备发送数据时,按照内核的流程处理,最终会调用到net_device_ops->ndo_start_xmit。对于KNI驱动来说,即kni_net_tx。
图16. kni_net_tx(1)
对skb报文长度做检查,不能超过mbuf的大小。然后检查发送队列tx_q是否还有空位,“内存队列”是否有剩余的mbuf。
图17. kni_net_tx(2)
从alloc_q取出一个内存块,将其转换为虚拟地址,然后将skb的数据复制过去,最后将其追加到发送队列tx_q中。
图18. kni_net_tx(3)
发送完成后,就直接释放skb并更新统计计数。
以上,是KNI在内核部分的实现,下面看看DPDK应用层如何使用KNI接口。DPDK提供了两个API rte_kni_rx_burst和rte_kni_tx_burst,用于从KNI接收报文和向KNI发送报文。
图19. rte_kni_rx_burst
接收报文时,从kni->tx_q直接取走所有报文。前面内核用KNI发送报文时,填充的就是这个fifo。当取走了报文后,DPDK应用层的调用kni_allocate_mbufs,负责给tx_q填充空闲mbuf,供内核使用。
rte_kni_tx_burst流程也很简单。
图20. rte_kni_tx_burst
先将要发送给KNI的报文地址转换为物理地址,然后enqueue到kni->rx_q中(内核的KNI实现也是从这个fifo中读取报文),最后调用kni_free_mbufs释放掉内核处理完的mbuf报文。
至此,DPDK的KNI原理分析完毕。