1.导读
一个网络报文从网卡接收到被应用处理,中间主要需要经历两个阶段:
- 阶段一:网卡通过其DMA硬件将收到的报文写入到收包队列中(入队)
- 阶段二:应用从收包队列中读取报文(出队)
下面以ixgbe网卡在dpdk框架下工作为例,分别介绍下收包队列的构造、启动和收包三个流程。
2.构造
收包队列的构造主要是通过调用网卡队列设置函数rte_eth_rx_queue_setup(dpdk rte_ethdev.h)来完成。
收包队列的结构体为ixgbe_rx_queue,该结构体里包含两个重要的环形队列rx_ring和sw_ring,rx_ring和sw_ring的关系可以简单如下认为。
- rx_ring主要存储报文数据的物理地址,物理地址供网卡DMA使用,也称为DMA地址(硬件使用物理地址,将报文copy到报文物理位置上)。
- sw_ring主要存储报文数据的虚拟地址,虚拟地址供应用使用(软件使用虚拟地址,读取报文)。
其中,报文数据的物理地址可以由报文数据的虚拟地址转化得到。下面详细介绍下rx_ring与sw_ring两个队列的构成。
2.1.rx_ring
ixgbe_adv_rx_desc
ixgbe_adv_rx_desc
......
ixgbe_adv_rx_desc
rx_ring是由一个动态申请的数组构建的环形队列,队列的元素是ixgbe_adv_rx_desc类型,队列的长度为(4096+4-1)。
- pkt_addr:报文数据的物理地址,网卡DMA将报文数据通过该物理地址写入对应的内存空间。
- hdr_addr:报文的头信息,hdr_addr的最后一个bit为DD位,因为是union结构,即status_error的最后一个bit也对应DD位。
DD位(Descriptor Done Status)用于标志标识一个描述符buf是否可用。
- 网卡每次来了新的数据包,就检查rx_ring当前这个buf的DD位是否为0,如果为0那么表示当前buf可以使用,就让DMA将数据包copy到这个buf中,然后设置DD为1。如果为1,那么网卡就认为rx_ring队列满了,直接会将这个包给丢弃掉,记录一次imiss。(0->1)
- 对于应用而言,DD位使用恰恰相反,在读取数据包时,先检查DD位是否为1,如果为1,表示网卡已经把数据包放到了内存中,可以读取,读取完后,再放入一个新的buf并把对应DD位设置为0。如果为0,就表示没有数据包可读。(1->0)
2.2.sw_ring
ixgbe_rx_entry
ixgbe_rx_entry
……
ixgbe_rx_entry
sw_ring是由一个动态申请的数组构建的环形队列,队列的元素是ixgbe_rx_entry类型,队列的大小可配,一般最大可配4096。
- mbuf:报文mbuf结构指针,mbuf用于管理一个报文,主要包含报文相关信息和报文数据。
3.启动
收包队列的启动主要是通过调用rte_eth_dev_start(dpdk rte_ethdev.h)函数完成,收包队列初始化的核心流程如下。
循环从mbuf pool中申请mbuf,从mbuf中得到报文数据对应的物理地址,物理地址存入rx_ring中,mbuf指针存入sw_ring中。其中通过rxd->read.hdr_addr = 0,完成了DD位设置为0。
一切ok后,就可以开始收包了。
3.收包
收包由网卡入队和应用出队两个操作完成。
3.1入队
入队的操作是由网卡DMA来完成的,DMA(Direct Memory Access,直接存储器访问)是系统和网卡(外设)打交道的一种方式,该种方式允许在网卡(外部设备)和系统内存之间直接读写数据,这样能有效减轻CPU的工作。
网卡收到报文后,先存于网卡本地的buffer-Rx(Rx FIFO)中,然后由DMA通过PCI总线将报文数据写入操作系统的内存中,即数据报文完成入队操作。(PS:PCIe总线可能成为网卡带宽的瓶颈)
3.2.出队
应用调用rte_eth_rx_burst(dpdk rte_ethdev.h)函数开始批量收包,最大收包数量由参数nb_pkts决定(比如设置为64)。其核心流程由ixgbe_recv_pkts(dpdk ixgbe_rxtx.c)实现,从收包队列rx_tail位置开始收,循环读取一个报文、填空一个报文(空报文数据),读取64个后,重新标记rx_tail的位置,完成出队操作,将收取的报文作返回供应用处理。代码简化如下。
-------------------------------------------------
struct rte_mbuf *rxm;
//从队列的tail位置开始取包
rx_id = rxq->rx_tail;
//循环获取nb_pkts个包
while (nb_rx < nb_pkts)
{
......
rxdp = &rx_ring[rx_id];
//检查DD位是否为1,是1则说明该位置已放入数据包,否则表示没有报文,退出
staterr=rxdp->
wb.upper.status_error;
if(!(staterr
&rte_cpu_to_le_32(IXGBE_RXDADV_STAT_DD)))
break;
rxd = *rxdp;
//申请一个mbuf(nmb),用于交换
nmb = rte_mbuf_raw_alloc(rxq->mb_pool);
//从sw_ring中读取一个报文mbuf(存入rxm)
rxe = &sw_ring[rx_id];
rxm = rxe->mbuf;
//往sw_ring中填空一个新报文mbuf(nmb)
rxe->mbuf = nmb;
//新mbuf对应的报文数据物理地址填入rx_ring对应位置,并将hdr_addr置0(DD位置0)
dma_addr =rte_cpu_to_le_64
(rte_mbuf_data_dma_addr_default(nmb));
rxdp->read.hdr_addr = 0;
rxdp->read.pkt_addr = dma_addr;
//对读取mbuf的报文信息进行初始化
rxm->pkt_len = pkt_len;
rxm->data_len = pkt_len;
rxm->port = rxq->port_id;
......
//读取的报文mbuf存入rx_pkts
rx_pkts[nb_rx++] = rxm;
}
//重新标记rx_tail位置
rxq->rx_tail = rx_id;
-------------------------------------------------
推荐阅读
- 栈 vs 队列
- Intel 82599网卡光模块使用的坑