前面我们介绍了ethtool初始化网卡过程中涉及到的重要函数以及背后的实现原理,接下来我们介绍该例子中剩下的一些DPDK函数的底层实现,这些函数在编写DPDK应用时经常被使用到。
rte_lcore_count()
在前面的文章中我们提到DPDK有两个全局的变量,其中一个类型为struct rte_config,获取方式为调用rte_eal_get_configuration(),rte_lcore_count()则是返回其中lcore_count成员中的值。该值是在init()过程的第7步中被设置的。
rte_lcore_id()
该func的实现是通过宏定义RTE_PER_LCORE(_lcore_id)读取一个变量,该宏的定义为
#define RTE_PER_LCORE(name) (per_lcore_##name)
其读取的是一个per-lcore的变量,变量的定义方法为
#define RTE_DEFINE_PER_LCORE(type, name) \
__thread __typeof__(type) per_lcore_##name
RTE_DEFINE_PER_LCORE(unsigned int, _lcore_id) = LCORE_ID_ANY;
而对_lcore_id这个per-lcore设置的位置在
eal_thread_loop(),在init()过程的第30步中我们提到,该func是所有worker lcore的入口,也是在该func中执行用户自定义函数的,在执行用户自定义函数前,将_lcore_id等per-lcore变量提前设置好,方便之后的调用。
rte_eal_remote_launch()
该func在init()过程中第9节已经介绍过。
rte_eal_wait_lcore()
该func就是检查lcore_config[lcore_id].state的状态,当状态不是WAIT且不是FINISHED时,进入rte_pause(),rte_pause()通过C语言内联汇编实现。
对于网络数据包处理程序,用户自定义函数通常是个死循环,即无限期对数据包进行处理,故在此类应用中,rte_eal_wait_lcore()永远不会发生返回。
rte_spinlock_trylock() / rte_spinlock_unlock()
自旋锁是操作系统内核当中的一个概念,由于DPDK是用户态的,故在用户态实现自旋锁是通过C语言内联汇编实现的,该内容比较复杂,在此先不做讨论。
rte_eth_rx_burst()
该func是收包的基本入口函数,需要重点分析。
通过传入的port_id,在数组rte_eth_devices中找到对应的网卡对象,并调用dev->rx_pkt_burst。rx_pkt_burst具体指向哪个处理函数,是在第14节中提到的ixgbe_dev_start()中的ixgbe_dev_rx_init()->ixgbe_set_rx_function()中设置的。针对ixgbe类型的设备而言,可选的处理函数非常多,需要根据dev->data->dev_private指向的ixgbe_adaptor中的特性决定。此项内容相对复杂,涉及到众多硬件特性,今后有机会将单独做介绍,在此我们以ixgbe_recv_pkts()为例进行分析。
ixgbe_recv_pkts()可以一次收取最多nb_pkts个数据包,但在向底层获取数据包时,仍然是一个一个获取的。接收之前,需要获取到rx_queues[queue_id]->rx_ring这个接收队列,及rx_queues[queue_id]->sw_ring这个软件队列。首先调用rte_mbuf_raw_alloc()分配一块内存nmb,这块内存是用于存数据包的数据的;分配好之后调用rte_ixgbe_prefetch()读取到实际的数据,此时读取到的包是写入到sw_ring[idx+1]中的,也就是说是先预读取下一个数据包;下一个数据包读取数据完毕后,将nmb存入sw_ring[idx]中,将sw_ring[idx]原本指向的内容赋值给rxm进行进一步处理;接下来设置rxm中数据包长度,数据包数据部分长度,数据包类型,哈希情况相关的一些字段,即可以看作是对数据包的分析过程。最后将rxm作为结果返回。
从过程中可以看出,在某一次数据包接收处理时,仅对数据包进行分析,读取动作则针对的是下一个要接收的数据包,而本次处理的数据包,是在上一个数据包的处理过程中就读取好了。在这里sw_ring元素作为一个管理数据包存储空间的入口点,目的是为了实现数据包的一个预读取,而rx_ring元素则是存储预读取数据包的一些基本信息。预读取的实现是通过内联汇编实现的,实现直接对cache缓存的一个读取。
rte_eth_tx_burst()
同上,该func是发包的基本入口函数,实际调用dev->tx_pkt_burst。dev->tx_pkt_burst具体指向的处理函数是在ixgbe_dev_tx_queue_setup()->ixgbe_set_tx_function()中设置(第14节有介绍),相比rx,tx可选的处理函数少,我们以ixgbe_xmit_pkts()为例进行分析。
ixgbe_xmit_pkts()同样一次可以发送多个数据包。发送之前,需要获取到tx_queues[queue_id]->tx_ring这个接收队列,以及tx_queues[queue_id]->sw_ring这个软件队列。首先需要检查一下tx_queues[queue_id]中统计的空闲空间数量,如果数量不足,则调用ixgbe_xmit_cleanup()释放一部分空间。
在ixgbe_xmit_cleanup()中,首先计算出需要释放掉的最后一个描述符索引desc_to_clean_to,该值默认为上一次的释放描述符索引last_desc_cleaned+tx重置门限tx_rs_thresh,如果加和之后desc_to_clean_to大于当前队列中描述符的数量nb_tx_desc,则从desc_to_clean_to中减去nb_tx_desc。接下来根据desc_to_clean_to,找到sw_ring[desc_to_clean_to]在tx_ring中对应的描述符,确认该描述符的状态是否已完成,如果未完成则不可继续释放。接下来确定要释放掉的描述符的数量nb_tx_to_clean,即根据上一次释放的描述符索引last_desc_cleaned与需要释放掉的最后一个描述符索引desc_to_clean_to之间的差值进行计算,同时要考虑这两个索引的前后关系,确保差值不会出现负值。最后将nb_tx_to_clean直接加到tx_queues[queue_id]中统计的空闲空间数量nb_tx_free中,并更新tx_queues[queue_id]中last_desc_cleaned的值。从整个释放过程可以看出,释放描述符的过程是通过更改tx_queues[queue_id]记录的空闲描述符的的起始位置,数量等内容来实现的。
接下来就开始依次遍历要发送的数据包并发送了,对于每一个数据包tx_pkt,检查其中的标志位tx_pkt->ol_flags,判断是否开启offload,如果开启,则填充数据结构union ixgbe_tx_offload,主要填充的内容是数据包二、三、四层的大小,vlan,外层封装的二、三层大小等,最后根据offload信息,获取到一个上下文对象ctx,这个ctx对象可能是需要重建的,具体由标志位new_ctx标识。
接下来需要计算发送这个数据包需要的描述符数量nb_used,该值由数据包的段的数量tx_pkt->nb_segs决定,如果ctx需要一个新的(即new_ctx为true),则额外再增加一个。当nb_used的数量+队列已使用描述符数量nb_tx_used超过tx重置门限时,需要把上一次处理的最后一个tx_ring描述符txp的read.cmd_type_len中标识重置操作的标志位置位。
发送一个数据包所需要分配的描述符数量必须等于数据包的段数量,如果存在需要新的上下文对象的情况则再加1,据此决定要发送该数据包并为该数据包分配sw_ring中的描述符之后,最后一个描述符的索引tx_last(注意描述符是环状结构,需要处理回环的情况)为多少。
当所需要的描述符数量nb_used大于nb_tx_free时,则需要调用ixgbe_xmit_cleanup()释放部分描述符,当释放过程失败时,则直接终止所有包的发送过程,直接结束ixgbe_xmit_pkts()。释放过程正常结束之后,如果nb_tx_free仍然大于nb_tx_free,则多次调用ixgbe_xmit_cleanup()来释放描述符,直到nb_tx_free不再大于nb_tx_free为止;如果某次释放过程发生错误,则同样直接终止所有包的发送过程。
接下来如果该数据包开启了offload,且new_ctx为true,则调用ixgbe_set_xmit_ctx()设置tx_ring中第一个空闲的描述符(即索引为tx_queues[queue_id]->tx_tail描述符,并强行转换为struct ixgbe_adv_tx_context_desc类型),此外这里会消耗一个sw_ring中的元素。
接下来是循环处理数据包的中的段,每一个段的mbuf被赋值到sw_ring的元素,而每一sw_ring元素在tx_ring中对应的元素则存入段指向的内存空间的DMA地址,offload信息等。在处理当前的sw_ring元素txe时,会根据txe->next_id获取到下一个sw_ring元素txn,这里txn不仅要在循环末期赋值给txe用于下一次段处理,而且需要执行预发送操作(通过rte_prefetch0),这里和收包过程中的预读取是相同的,是为了刷新cache line。每个段处理完成之后,更新tx_queues[queue_id]中nb_tx_used和nb_tx_free的数量,如果更新nb_tx_used之后其值大于等于tx重置门限,则将重置标志位写于当前最后一个tx_ring描述符的read.cmd_type_len中,如果nb_tx_used没有超过tx重置门限,则只记录下本次处理的最后一个tx_ring描述符到txp当中,该变量在下一个数据包处理过程中可能会用到。
以上就是DPDK中最重要的两个过程发送数据包和接收数据包过程的分析,从ixgbe代码层面中可以看出,主要的处理过程是借助通过对收发队列中描述符的操作关联相关的mbuf资源,最终操作mbuf发送是通过内嵌的汇编语言进行的。涉及到汇编的操作随着对DPDK的深入了解将详细介绍。