DPDK Kernel NIC Interface (KNI)接口允许DPDK用户程序访问Linux控制平面。
使用DPDK KNI的有点如下:
相较现存的Linux TUN/TAP接口更快的速度(消除了系统调用以及copy_to_user()/copy_from_user()内存拷贝的消耗)
允许标准Linux网络工具管理DPDK接口,如ethtool, ifconfig 和 tcpdump
提供到内核协议栈接口
使用DPDK的内核KNI虚拟接口的应用程序组件如下图所示。
KNI内核可加载模块rte_kni为DPDK应用提供内核接口。
当rte_kni模块加载时,创建/dev/kni设备节点(rte_kni模块创建kni杂项设备,文件系统节点/dev/kni需要手动或者通过udev机制创建),藉此节点,DPDK KNI应用可控制和与内核rte_kni模块交互。
在内核模块rte_kni加载时,可指定一些可选的参数以控制其行为:
# modinfo rte_kni.ko
lo_mode: KNI loopback mode (default=lo_mode_none):
lo_mode_none Kernel loopback disabled
lo_mode_fifo Enable kernel loopback with fifo
lo_mode_fifo_skb Enable kernel loopback with fifo and skb buffer
kthread_mode: Kernel thread mode (default=single):
single Single kernel thread mode enabled.
multiple Multiple kernel thread mode enabled.
carrier: Default carrier state for KNI interface (default=off):
off Interfaces will be created with carrier state set to off.
on Interfaces will be created with carrier state set to on.
典型的情况是,在加载rte_kni模块时不指定任何参数,DPDK应用可由内核网络协议栈获取和向其发送报文。不指定任何参数,意味着仅创建一个内核线程处理所有的KNI虚拟设备在内核侧的报文接收,并且禁用回环模式,KNI接口的默认链路状态为关闭off。
# insmod kmod/rte_kni.ko
以测试为目的,在加载rte_kni模块式可指定lo_mode参数:
# insmod kmod/rte_kni.ko lo_mode=lo_mode_fifo
lo_mode_fifo回环模式将在内核空间中操作FIFO环队列,由函数kni_fifo_get(kni->rx_q,...)和kni_fifo_put(kni->tx_q,...)实现从rx_q接收队列读取报文,再写入发送队列tx_q来实现回环操作。
# insmod kmod/rte_kni.ko lo_mode=lo_mode_fifo_skb
lo_mode_fifo_skb回环模式在以上lo_mode_fifo的基础之上,增加了sk_buff缓存的相关拷贝操作。具体包括将rx_q接收队列的数据拷贝到分配的接收skb缓存中。以及分配发送skb缓存,将之前由rx_q队列接收数据拷贝到发送skb缓存中,使用函数kni_net_tx(skb, dev)发送skb缓存数据。最终将数据报文拷贝到mbuf结构中,使用kni_fifo_put函数加入到tx_q发送队列。可见此回环测试模式,更加接近真实的使用场景。
如果没有指定lo_mode参数,回环模式将禁用。
为了提供性能的灵活性,内核模块rte_kni在加载时刻指定kthread_mode参数。rte_kni模块支持两个选项:单内核线程模式和多内核线程模式。
如下,使能单内核线程模式:
# insmod kmod/rte_kni.ko kthread_mode=single
此模式为所有的KNI虚拟接口创建唯一的内核线程在内核侧接收数据。默认情况下,此内核线程不绑定在特定的核心上,但是,用户可在创建第一个KNI虚拟接口时通过指定结构体rte_kni_conf的core_id和force_bind成员参数,设置此线程的亲核性。
为达到更高性能,内核线程绑定的核心应当与应用中DPDK的核心在同一个socket上。
KNI内核模块也可配置成为每个DPDK应用创建的KNI虚拟接口启动一个单独的内核线程。以下,使能多内核线程模式:
# insmod kmod/rte_kni.ko kthread_mode=multiple
此模式为每个KNI虚拟接口创建一个单独的内核线程在内核侧接收数据。内核线程的亲核性通过每个KNI虚拟接口创建时的结构体rte_kni_conf成员core_id和force_bind变量参数指定。
如果系统中由足够的未使用核心,多内核线程模式可提供具有扩展性的高性能。
如果kthread_mode参数未指定,使用单内核线程模式。
内核模块rte_kni创建的KNI虚拟接口的链路状态,可通过模块加装时的carrier选项控制。
如果指定了carrier=off,当接口管理使能时,内核模块将接口的链路状态设置为关闭。DPDK应用可通过函数rte_kni_update_link设置KNI虚拟接口的链路状态。这对于需要KNI虚拟接口状态与对应的物理接口实际状态一致的应用是有用的。
如果指定了carrier=on,当接口管理使能时,内核模块将自动设置接口的链路状态为启用。这对于仅将KNI接口作为纯虚拟接口,而不对应任何物理硬件;或者并不想通过rte_kni_update_link函数显示设置接口链路状态的DPDK应用是有用的。对于物理口为连接任何链路而进行的回环模式测试也是有用的。
以下,设置默认的链路状态为启用:
# insmod kmod/rte_kni.ko carrier=on
以下,设置默认的链路状态为关闭:
# insmod kmod/rte_kni.ko carrier=off
如果carrier参数没有指定,KNI虚拟接口的默认链路状态为关闭。
在任何KNI虚拟接口创建之前,rte_kni内核模块必须加装到内核,并且经由rte_kni_init函数配置(获取/dev/kni设备节点文件句柄)。
int rte_kni_init(unsigned int max_kni_ifaces __rte_unused)
{
/* Check FD and open */
if (kni_fd < 0) {
kni_fd = open("/dev/" KNI_DEVICE, O_RDWR);
if (kni_fd < 0) {
RTE_LOG(ERR, KNI,
"Can not open /dev/%s\n", KNI_DEVICE);
return -1;
}
}
KNI虚拟接口由DPDK应用通过rte_kni_alloc函数动态的创建。
结构体rte_kni_conf包含的成员字段,允许用户指定KNI虚拟接口的名称、设置MTU值大小、设置明确的或者随机的MAC地址,以及控制内核接收线程的亲核性(单线程和多线程模式)。默认情况下,KNI示例程序由对应的物理接口获取MTU值,但是对于KNI PMD程序,其由mbuf的缓存长度决定MTU值。
struct rte_kni_conf {
/*
* KNI name which will be used in relevant network device. Let the name as short as possible, as it will be part of memzone name.
*/
char name[RTE_KNI_NAMESIZE];
uint32_t core_id; /* Core ID to bind kernel thread on */
uint16_t group_id; /* Group ID */
unsigned mbuf_size; /* mbuf size */
struct rte_pci_addr addr;
struct rte_pci_id id;
__extension__
uint8_t force_bind : 1; /* Flag to bind kernel thread */
char mac_addr[ETHER_ADDR_LEN]; /* MAC address assigned to KNI */
uint16_t mtu;
};
结构体rte_kni_ops包含的指针,执行处理来自rte_kni内核模块请求的函数。当KNI虚拟接口被一些控制命令(工具如ifconfig或者ethtool等)或者应用之外的函数操作时,这些rte_kni_ops指针函数允许DPDK应用执行一些注册好的操作。
struct rte_kni_ops {
uint16_t port_id; /* Port ID */
/* Pointer to function of changing MTU */
int (*change_mtu)(uint16_t port_id, unsigned int new_mtu);
/* Pointer to function of configuring network interface */
int (*config_network_if)(uint16_t port_id, uint8_t if_up);
/* Pointer to function of configuring mac address */
int (*config_mac_address)(uint16_t port_id, uint8_t mac_addr[]);
/* Pointer to function of configuring promiscuous mode */
int (*config_promiscusity)(uint16_t port_id, uint8_t to_on);
};
例如,当DPDK应用想在用户通过ip link set [up|down] dev
当前有4个回调可供用户注册应用函数:
config_network_if:
当KNI虚拟接口的管理状态改变时调用。例如,用户运行命令ip link set [up|down] dev
change_mtu:
当用户改变KNI虚拟接口的MTU值时调用。例如,当用户运行命令ip link set mtu
config_mac_address:
当用户改变KNI虚拟接口的MAC地址时调用。例如,当用户执行命令ip link set address
config_promiscusity:
当用户修改KNI虚拟接口的混杂模式时调用。例如,当用户运行命令ip link set promisc [on|off] dev
为了处理这些回调,DPDK应用必须周期性的运行rte_kni_handle_request()函数。由于注册的用户回调函数直接由rte_kni_handle_request()调用,所以必须注意避免死锁,以及避免阻塞DPDK快速路径的任务。典型的,使用这些回调的DPDK应用需要创建单独的线程或者子进程去定期调用rte_kni_handle_request()函数。
DPDK应用可使用函数rte_kni_release()删除KNI虚拟接口。所有没有被显示删除的kNI虚拟接口在/dev/kni节点关闭的时候一同被删除,可以显示的由rte_kni_close函数触发,或者当DPDK应用关闭时。
void rte_kni_close(void)
{
if (kni_fd < 0)
return;
close(kni_fd);
kni_fd = -1;
}
为最大限度的减小在内核空间运行的DPDK代码,mbuf内存池仅在用户空间管理。内核模块知道mbuf的存在,但是所有mbuf的分配和释放操作都仅由DPDK应用程序处理。
下图显示了典型的报文在两个方向的发送情况:
在DPDK接收侧,mbuf由PMD在接收线程上下文分配。此线程将mbuf加入对接收FIFO队列rx_q中。内核KNI线程poll轮询所有的活动KNI虚拟接口的接收队列rx_q。如果将mbuf移出队列,需要将其转换成sk_buff结构,通过netif_rx发送到Linux网络协议栈。出队列的mbuf必须被释放,所以其被发送回free_q释放队列的FIFO中,最终由DPDK应用释放。
DPDK接收线程中,位于相同的主循环中,轮询释放队列free_q FIFO,在mbuf移出队列后,进行释放。
为实现报文发送,DPDK应用必须先行入队列一些mbuf,以便内核侧获得mbuf缓存。
通过调用kni_net_tx函数,内核将报文发送出去。mbuf由alloc_q队列移出(由于之前的mbuf缓存此处无需等待),填充上sk_buff中的数据。sk_buff结构随后被释放,而mbuf缓存通过tx_q表示的FIFO队列发送到DPDK应用。
DPDK发送线程将mbuf移出队列,通过rte_eth_tx_burst()函数发送给PMD。随后,将mbuf回送到alloc_q中,以供内核侧发送使用。
Ethtool是Linux特有的工具,需要内核中每个网络设备注册自身的回调以支持其相应的操作。当前的ethtool实现使用了修改过的igb/ixgbe内核驱动。i40e网卡和VM(VF或者EM设备)不支持Ethtool工具。
END