Post

DPDK 初学(7) Ip-fragmentation

到现在为止,有一点不理解,那就是使用vfio-pci驱动的网卡,怎么ifconfig看不到。在stackoverflow看到了一个解答,不知道对不对,留待后续验证。

关键函数

init_mem

进行一些内存初始化,这里第一次出现了rte_lcore_to_socket_id,函数原型如下

1
2
unsigned int rte_lcore_to_socket_id(unsigned int lcore_id)
Get the ID of the physical socket of the specified lcore

获取特定逻辑核的物理端口id,这里应该是对NUMA架构中的物理节点。NUMA架构可以简化的理解成,每一个或者一组CPU现在拥有了部分很靠近自己的内存,这能让它们在访问这部分内存时拥有更快的速度,同样的,如果出现了miss,那么跨结点访问内存也会花费更多的时间。上文中提到CPU+RAM的组合,被称为NUMA的一个node。如果想更详细的了解,可以阅读一下这里

然后对每一个socket建立所属的mbuf_pool,

1
2
3
4
5
6
7
8
socket = rte_lcore_to_socket_id(lcore_id);
mp = rte_pktmbuf_pool_create(buf, NB_MBUF, 32,
    0, RTE_MBUF_DEFAULT_BUF_SIZE, socket);
socket_direct_pool[socket] = mp;

mp = rte_pktmbuf_pool_create(buf, NB_MBUF, 32, 0, 0,
    socket);
socket_indirect_pool[socket] = mp;

初始化lpm和lpm6

1
2
3
4
5
6
7
lpm_config.max_rules = LPM_MAX_RULES;
lpm_config.number_tbl8s = 256;
lpm_config.flags = 0;

lpm = rte_lpm_create(buf, socket, &lpm_config);
socket_lpm[socket] = lpm;
lpm6 = rte_lpm6_create(buf, socket, &lpm6_config);

main

init_mem之后开始进行port的初始化,这里用到了一个rte_eth_conf结构,之前也有用这个结构配置对应的eth_dev,下面的配置中mq_mode没见过,搜索了一下,这是用来配置发送队列模式。在dpdk中有这么几个选项

  • RTE_ETH_MQ_TX_NONE

    最基本的情况,NIC 使用单个发送队列来处理所有传出的数据包。

  • RTE_ETH_MQ_TX_DCB

    数据中心桥接(DCB,Data Center Bridging)模式,适用于数据中心网络。在此模式下,NIC 根据 DCB 配置信息分配数据包到发送队列,以确保流量的优先级和服务质量(QoS)。

  • RTE_ETH_MQ_TX_VMDQ_DCB

    结合了 VMDq(Virtual Machine Device Queue)和 DCB 的模式,用于虚拟化环境,其中虚拟机需要特定的网络带宽和优先级。

  • RTE_ETH_MQ_TX_VMDQ_ONLY

    仅使用 VMDq 的模式,用于虚拟化环境,NIC 会根据虚拟机的 MAC 地址将数据包分配到不同的发送队列。

1
2
3
4
5
6
7
8
9
10
11
12
13
static struct rte_eth_conf port_conf = {
 .rxmode = {
  .mtu = JUMBO_FRAME_MAX_SIZE - RTE_ETHER_HDR_LEN -
   RTE_ETHER_CRC_LEN,
  .offloads = (RTE_ETH_RX_OFFLOAD_CHECKSUM |
        RTE_ETH_RX_OFFLOAD_SCATTER),
 },
 .txmode = {
  .mq_mode = RTE_ETH_MQ_TX_NONE,
  .offloads = (RTE_ETH_TX_OFFLOAD_IPV4_CKSUM |
        RTE_ETH_TX_OFFLOAD_MULTI_SEGS),
 },
};

以下代码对lcore_queue_conf进行初始化,实际上就是对每一个lcore上的每一个rx_queue进行设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
qconf = &lcore_queue_conf[rx_lcore_id];
while (rte_lcore_is_enabled(rx_lcore_id) == 0 ||
         qconf->n_rx_queue == (unsigned)rx_queue_per_lcore) {

   rx_lcore_id ++;
   if (rx_lcore_id >= RTE_MAX_LCORE)
    rte_exit(EXIT_FAILURE, "Not enough cores\n");

   qconf = &lcore_queue_conf[rx_lcore_id];
  }
socket = (int) rte_lcore_to_socket_id(rx_lcore_id);

rxq = &qconf->rx_queue_list[qconf->n_rx_queue];
rxq->portid = portid;
rxq->direct_pool = socket_direct_pool[socket];
rxq->indirect_pool = socket_indirect_pool[socket];
rxq->lpm = socket_lpm[socket];
rxq->lpm6 = socket_lpm6[socket];
qconf->n_rx_queue++;//设置完一个就++,为了上面的while能进入到下一个lcore

随后开始设置rte_eth_dev、mtu、rxd、txd、queue等部分。这里记得检查下自己的网卡到底支不支持static struct rte_eth_conf port_conf这个变量内定义的offlaods

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
ret = rte_eth_dev_configure(portid, 1, (uint16_t)n_tx_queue,
         &local_port_conf);

ret = rte_eth_dev_set_mtu(portid, local_port_conf.rxmode.mtu);

ret = rte_eth_dev_adjust_nb_rx_tx_desc(portid, &nb_rxd,
         &nb_txd);

// rx_queue这里是每个port设置一个
rxq_conf = dev_info.default_rxconf;
rxq_conf.offloads = local_port_conf.rxmode.offloads;
ret = rte_eth_rx_queue_setup(portid, 0, nb_rxd,
        socket, &rxq_conf,
        socket_direct_pool[socket]);
// tx_queue则是每一个port要设置多个
//使用nb_tx_queues的原因是之前的代码设置rte_eth_dev时确认了队列的可用数量,而这个数量实际上和我们的参数 -l保持一致。这意味着我们使用n个lcores,就会在每一个port上设置相应数量的tx_queue
queueid = 0;
for (lcore_id = 0; lcore_id < RTE_MAX_LCORE; lcore_id++) {
 if (rte_lcore_is_enabled(lcore_id) == 0)
  continue;

 if (queueid >= dev_info.nb_tx_queues)
  break;

 ...

 txconf = &dev_info.default_txconf;
 txconf->offloads = local_port_conf.txmode.offloads;
 ret = rte_eth_tx_queue_setup(portid, queueid, nb_txd,
         socket, txconf);
 ...
 qconf = &lcore_queue_conf[lcore_id];
 qconf->tx_queue_id[portid] = queueid;
 queueid++;
}

然后老操作了,启动port,开混杂模式。随即检查port能不能支持ipv4/6的ptype,顺便注册一个回调。这个check_ptype就不展开了,内容很简单。我的vmxnet3不支持IPV6 ptype。

1
2
3
4
5
if (check_ptype(portid) == 0) {
   rte_eth_add_rx_callback(portid, 0, cb_parse_ptype, NULL);
   printf("Add Rx callback function to detect L3 packet type by SW :"
    " port = %d\n", portid);
  }

init_routing_table

函数倒是简明,但是没理解为啥能注册成功。这里的l3fwd_ipv4_route_array[i].if_out从0-7,在rte_lpm_addr函数原型中,最后一个参数是next_hop,我的port哪里有8个,一共才2个。甚至启动参数sudo ./build/ip_fragmentation -l 0-3 -n 3 -- -p 3 -q 2规定了使用4lcores、0,1ports,2tx_queues。

1
2
3
4
ret = rte_lpm_add(lpm,
  l3fwd_ipv4_route_array[i].ip,
  l3fwd_ipv4_route_array[i].depth,
  l3fwd_ipv4_route_array[i].if_out);

main_loop

整体结构和之前的l2fwd类似,参考这篇。只是最后调整了转发函数,这里使用的是l3fwd_simple_forward

l3fwd_simple_forward

main_loop里面,首先是从read packet这个动作进入的。把收到的mbuf放到pkts_burst中,开始处理这些包。

移除Ethernet头

1
rte_pktmbuf_adj(m, (uint16_t)sizeof(struct rte_ether_hdr));

如果是IPV4包就开始进行最长前缀匹配。因为它是先累积到一起后面再send_burst,所以这里面会有一个len的问题,可能你之前放进去的包都还没发,要记着这一点。

1
2
3
4
5
6
7
8
9
if (RTE_ETH_IS_IPV4_HDR(m->packet_type)) {
    ...
    if (rte_lpm_lookup(rxq->lpm, ip_dst, &next_hop) == 0 &&
    (enabled_port_mask & 1 << next_hop) != 0) {
   port_out = next_hop;

   /* Build transmission burst for new port */
   len = qconf->tx_mbufs[port_out].len;
  }

接着就开始处理分片的问题。不需要分片是最好的,直接把mbuf放到发送队列的mbufs去,否则就有个分片的过程了。下面还有个IPV6版本的,过程差不多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (likely (IPV4_MTU_DEFAULT >= m->pkt_len)) {
   qconf->tx_mbufs[port_out].m_table[len] = m;
   len2 = 1;
  } else {
    //下面这个函数就是分片函数啦,还是很好理解的,指定好pkt_in和pkt_out就行,空间位置留够,返回一个分片后的pkt_cnt
   len2 = rte_ipv4_fragment_packet(m,
    &qconf->tx_mbufs[port_out].m_table[len],
    (uint16_t)(MBUF_TABLE_SIZE - len),
    IPV4_MTU_DEFAULT,
    rxq->direct_pool, rxq->indirect_pool);

   /* Free input packet */
   rte_pktmbuf_free(m);

   /* request HW to regenerate IPv4 cksum */
   ol_flags |= (RTE_MBUF_F_TX_IPV4 | RTE_MBUF_F_TX_IP_CKSUM);

   /* If we fail to fragment the packet */
   if (unlikely (len2 < 0))
    return;
  }

最后修改一下包头,大功告成~

1
2
3
4
5
6
7
struct rte_ether_hdr *eth_hdr = (struct rte_ether_hdr *)
   rte_pktmbuf_prepend(m,
    (uint16_t)sizeof(struct rte_ether_hdr));
m->ol_flags |= ol_flags;//看上面,交给硬件重算一次chsum
m->l2_len = sizeof(struct rte_ether_hdr);
rte_ether_addr_copy(&ports_eth_addr[port_out],
    &eth_hdr->src_addr);

关键流程

流程大体上和l2fwd比较接近,只是多了一个mbuf_pool的过程。还是没理解怎么给些个port设置ip才能让包进来,现在虽然能build出来跑,但是都是没有包进入的状态。

This post is licensed under CC BY 4.0 by the author.