功能简介

将一个物理网口收到的包立刻从另一个物理网口发出去(通常用于测试网卡吞吐量)

经典的DPDK 二层转发(Layer 2 Forwarding)示例程序,展示了DPDK应用的基础骨架:轮询模式驱动(PMD)、无中断收发包、零拷贝。

l2fwd 演示了 DPDK 的 Run-to-Completion 模型:
一个 CPU 核心拿着网卡队列,死循环 -> 抓一把包 -> 简单处理 -> 扔到另一个网卡队列 -> 继续抓包。

相关问题

  • MAC地址改写(关键点):交换机转发包是不改MAC的,但这里是网卡到网卡。为了让对端设备(或另一台电脑)能识别并接收,它会把源MAC改成发出端口自己的MAC,把目的MAC改成一个伪造的组播/单播地址(02:00:00:00:00:TX_PORT_ID)。

    • 源MAC不改的话,网卡硬件可能拒发,回包也无法路由回来

    • 源MAC修改后的数据包收发流程

      • 服务器B 发包(源MAC = BB:BB:BB:BB:BB:BB)→ 到达 Port 0

      • l2fwd 转发时,把源MAC改成 Port 1 的MAC(11:11:11:11:11:01)|普通的不会改,还是BB)

      • 从port1发出后,对端设备收到包,看到源MAC = 11:11:11:11:11:01 |(BB)

      • 对端设备回复时:

        “我要回复给 11:11:11:11:11:01,这个地址就是我眼前这根网线的对端啊!”

      • 回包顺着原路发回来 → 到达 Port 1

      • l2fwd 再次转发,从 Port 0 出去,回到服务器B

      至此 双向通路建立成功。

    • 目的MAC修改流程

      • 用 l2fwd 做"回环测试"时(Port 0 和 Port 1 直连,没有外部设备)
      • 抓包教学演示
  • 为什么不做下面这个流程【到处发(洪泛 Flood)→ 看到回包 → 记录 MAC 和端口的对应关系 → 填表 → 下次直接转发】

    • 洪泛学习有代价,I2fwd是DPDK基础性能测试基准,追求线速;
    • I2fwd经典用法之一是串接在网络中间做透明监控或加解密。通过强制修改源MAC,使两边被物理隔离,不会产生直接的ARP对话,但逻辑上是通的
  • 收包和发包没有锁

    • Per-Core Queue:代码中给每个逻辑核分配了独立的 lcore_queue_conf。
    • 逻辑核 Core 1 只操作端口0的收队列0 和 端口1的发队列0。
    • 逻辑核 Core 2 只操作端口1的收队列0 和 端口0的发队列0。
    • 结果:两个核心永远不会争抢同一个队列的锁。这就是 DPDK 的 无锁数据通道。

命令行解析

* 参数输入 ./l2fwd -c 0x3 -n 4 -- -p 3 -q 1
* -c 为十六进制的分配的逻辑内核数量
* -n 为十进制的内存通道数量,EAL参数和程序参数用--分开
* -q 为分配给每个核心的收发队列数量(端口数量)
* -p为十六进制的分配的端口数
* -t 为可选默认10s打印时间间隔参数

命令行

代码解读

1. main函数部分

  • 第一阶段:DPDK起手

    ret = rte_eal_init(argc, argv);
    
    1. 解析 EAL 参数(比如 -l 0-3 指定用哪些 CPU 核,-a 00:04.0 指定用哪个 PCIe 网卡),在DPDK中--用来分隔EAL参数和应用参数。
    2. 初始化大页内存:从系统预留的大页中分配内存,建立映射表
    3. 设置 CPU 亲缘性:把当前主线程绑定到某个核上
    4. 扫描 PCI 总线:找到我们指定的网卡设备
    argc -= ret;//剩余参数个数
    argv += ret;//移动指针到后续还未解析的参数
    
  • 第二阶段:信号处理与应用参数解析

        force_quit = false;
        signal(SIGINT, signal_handler);     // Ctrl+C
        signal(SIGTERM, signal_handler);    // kill 命令
    
        /* parse application arguments (after the EAL ones) */
        ret = l2fwd_parse_args(argc, argv);
        if (ret < 0)
            rte_exit(EXIT_FAILURE, "Invalid L2FWD arguments\n");
    
    • 信号注册:当用户按 Ctrl+C 时,signal_handler 会把 force_quit 设为 true。各个核上的死循环看到这个标志就会优雅退出。
    • l2fwd_parse_args进行参数解析,支持短参数(-p 0x3)与长参数(--mac-updating),其使用getopt_long
      • 是 Linux 命令行参数解析器,自动把用户在命令行输入的 -p 0x3、--help、--file=test.txt 这类东西,转换成C 语言代码里能直接用的变量和值。
  • 第三阶段:参数校验和预处理阶段

    • 确保用户给的参数是合法且可执行
    printf("MAC updating %s\n", mac_updating ? "enabled" : "disabled");
    
    /* convert to number of cycles */
    timer_period *= rte_get_timer_hz(); //把秒转换成CPU时钟周期数
    
    • 为什么把秒转换时钟周期,转发循环里用的是 rte_rdtsc() 读取 CPU 时间戳(从 CPU 启动到现在的时钟周期数),这样可以直接比较,避免每次都调用耗时的时间函数。、
    nb_ports = rte_eth_dev_count_avail();
    if (nb_ports == 0)
        rte_exit(EXIT_FAILURE, "No Ethernet ports - bye\n");
    
    • 检查可用网口数量,查询DPDK接管了几个网口,如果没有则报错退出。
    if (port_pair_params != NULL) {
        if (check_port_pair_config() < 0)
            rte_exit(EXIT_FAILURE, "Invalid port pair config\n");
    }
    
    • 检查端口配对配置(新版l2fwd功能):如果用 --portmap 参数手动指定了端口配对关系(比如 “(0,2),(1,3)”),这里会检查配置是否合法。
    /* check port mask to possible port mask */
    if (l2fwd_enabled_port_mask & ~((1 << nb_ports) - 1))
        rte_exit(EXIT_FAILURE, "Invalid portmask; possible (0x%x)\n",
                 (1 << nb_ports) - 1);
    
    /* Initialization of the driver. 8< */
    
    • 检查端口掩码是否越界,确保用户 -p 参数指定的端口号不超过实际可用端口范围
  • 第四阶段:确定端口配对关系

    /* reset l2fwd_dst_ports */
       for (portid = 0; portid < RTE_MAX_ETHPORTS; portid++)
               l2fwd_dst_ports[portid] = 0;
       last_port = 0;
    
    • 清空目的端口表,索引是收包端口号,值是发包端口号
    • RTE_MAX_ETHPORTS是DPDK框架级的宏定义,定义在DPDK的头文件里,表示其最多能支持多少个以太网端口
    /* populate destination port details */
       if (port_pair_params != NULL) {
               uint16_t idx, p;
    
               for (idx = 0; idx < (nb_port_pair_params << 1); idx++) {
                       p = idx & 1;
                       portid = port_pair_params[idx >> 1].port[p];
                       l2fwd_dst_ports[portid] =
                               port_pair_params[idx >> 1].port[p ^ 1];
               }
       }
    
    • 填充目的端口表的第一种情况:用户手动指定了端口配对
    • port_pair_params通过用户使用--portmap参数手动指定配对 eg../l2fwd... --portmap="(0, 2), (1, 3)"
    • nb_port_pair_params 通过端口配对解析函数l2fwd_parse_port_pair_config 获得
    • 循环迭代进行双向端口设置
      1. idx & 1 判断idx是偶数还是奇数,& 1只保留二进制的最低位
      2. idx >> 1 把idx除以2(整数除法),没两次迭代映射到同一对端口
      3. nb_port_pair_params << 1 把nb_port_pair_params乘2,每对端口需要两次迭代来进行双向设置
      4. p ^ 1异或运算0 ^ 1 = 1,1 ^ 1 = 0,把 0 变成 1,把 1 变成 0(翻转)
    else {
        RTE_ETH_FOREACH_DEV(portid) {
            /* skip ports that are not enabled */
            if ((l2fwd_enabled_port_mask & (1 << portid)) == 0)//跳过未启用端口
                continue;
    
            if (nb_ports_in_mask % 2) {
                l2fwd_dst_ports[portid] = last_port;
                l2fwd_dst_ports[last_port] = portid;
            } else {
                last_port = portid;
            }
    
            nb_ports_in_mask++;
        }
        if (nb_ports_in_mask % 2) {
            printf("Notice: odd number of ports in portmask.\n");
            l2fwd_dst_ports[last_port] = last_port;
        }
    }
    /* >8 End of initialization of the driver. */
    
    • 填充目的端口表的第二种情况:用户没有手动指定,使用默认自动配对
    • 如果总数是奇数个端口,最后一个端口自己发给自己,
    • 如果总数是偶数个端口,每两个端口双向链接。
    • RTE_ETH_FOREACH_DEV是DPDK框架自带的宏,遍历所有被DPDK接管的、可用的网口,定义在头文件<rte_ethdev.h>下,RTE_ETH_FOREACH_DEV 遍历的是被 DPDK 接管的有效端口,所以:
      • 如果你的系统有 4 个物理网口,但只通过 -a 参数绑定了端口 0 和端口 2,那么 portid 的值会是:0 → 2 → 结束。
      • 如果启动命令里没有用 -a 指定网口,那么 DPDK 会接管所有它识别到的、且没有被内核驱动占用的网口。
    • l2fwd_enabled_port_mask是用户-p参数指定的掩码,如果当前端口不在掩码里就跳过eg.-p 0x5 二进制0101启用端口0、2
    • nb_ports_in_mask初始为0,用来统计用户启用了几个端口
    • last_port用来记住上一个遍历的端口号,初始为0
  • 第五阶段:分配逻辑核与端口

    • 建立多对多映射:哪些 CPU 核负责从哪些网口收包

    • 背景:代码中数据结构定义

      #define MAX_RX_QUEUE_PER_LCORE 16
      struct __rte_cache_aligned lcore_queue_conf {
          unsigned n_rx_port;//该核负责几个端口
          unsigned rx_port_list[MAX_RX_QUEUE_PER_LCORE]; //具体是哪几个端口
      };
      struct lcore_queue_conf lcore_queue_conf[RTE_MAX_LCORE]; 
      
      • RTE_MAX_LCORE DPDK 框架级的宏定义,表示 DPDK 最多能支持多少个逻辑核 lcore。
      • lcore_queue_conf索引是lcore_id,每个元素记录了这个lcore负责从哪些端口收包
    rx_lcore_id = 0; 
    qconf = NULL;
    
    • rx_lcore_id:当前正在分配的逻辑核编号,从 0 开始
    • qconf:指向当前逻辑核的配置结构体,初始为空
    /* Initialize the port/queue configuration of each logical core */
            RTE_ETH_FOREACH_DEV(portid) {
                    /* skip ports that are not enabled */
                    if ((l2fwd_enabled_port_mask & (1 << portid)) == 0)
                            continue;
    
                    /* get the lcore_id for this port */
                    while (rte_lcore_is_enabled(rx_lcore_id) == 0 ||
                           lcore_queue_conf[rx_lcore_id].n_rx_port ==
                           l2fwd_rx_queue_per_lcore) {
                            rx_lcore_id++;
                            if (rx_lcore_id >= RTE_MAX_LCORE)
                                    rte_exit(EXIT_FAILURE, "Not enough cores\n");
                    }
    
                    if (qconf != &lcore_queue_conf[rx_lcore_id]) {
                            /* Assigned a new logical core in the loop above. */
                            qconf = &lcore_queue_conf[rx_lcore_id];
                            nb_lcores++;
                    }
    
                    qconf->rx_port_list[qconf->n_rx_port] = portid;
                    qconf->n_rx_port++;
                    printf("Lcore %u: RX port %u TX port %u\n", rx_lcore_id,
                           portid, l2fwd_dst_ports[portid]);
            }
    
    
    • l2fwd_rx_queue_per_lcore全局变量,表示每个lcore最多负责几个收包端口。
    • nb_lcores在main函数内定义的局部变量,统计时机分配了收包任务的lcore数量。
  • 第六阶段:创建内存池(Mempool)

    在传统 Linux 网络栈中,每收到一个包,内核要 kmalloc 一块内存,用完再 kfree。频繁的分配释放会:

    • 导致内存碎片
    • 锁竞争严重
    • Cache 污染

    DPDK 的做法是:程序启动时,一次性申请一大块内存,切好,分给不同的池子。

            nb_mbufs = RTE_MAX(nb_ports * (nb_rxd + nb_txd + MAX_PKT_BURST +
                    nb_lcores * MEMPOOL_CACHE_SIZE), 8192U);
    
            /* Create the mbuf pool. 8< */
            l2fwd_pktmbuf_pool = rte_pktmbuf_pool_create("mbuf_pool", nb_mbufs,
                    MEMPOOL_CACHE_SIZE, 0, RTE_MBUF_DEFAULT_BUF_SIZE,
                    rte_socket_id());
            if (l2fwd_pktmbuf_pool == NULL)
                    rte_exit(EXIT_FAILURE, "Cannot init mbuf pool\n");
            /* >8 End of create the mbuf pool. */
    
    • RTE_MAX 是DPDK内部的通用工具宏,取两者中较大值,头文件 <rte_common.h>

      变量来源含义示例值
      nb_portsrte_eth_dev_count_avail() 返回DPDK 实际接管的网口数量4
      nb_rxd文件开头定义 static uint16_t nb_rxd = 1024每个收队列的描述符数量1024
      nb_txd文件开头定义 static uint16_t nb_txd = 1024每个发队列的描述符数量1024
      MAX_PKT_BURST文件开头 #define MAX_PKT_BURST 32一次最多收/发多少个包(突发收发包)32
      nb_lcores上一阶段统计得到实际参与转发的 lcore 数量2
      MEMPOOL_CACHE_SIZE文件开头 #define MEMPOOL_CACHE_SIZE 256每个 lcore 的 mbuf 缓存大小256
      8192U硬编码常量最小 mbuf 数量,保证内存池不会太小8192
      • nb_lcores * MEMPOOL_CACHE_SIZE:每个 lcore 有自己的本地缓存,避免频繁访问共享内存池
      • * nb_ports:每个端口都需要这么多
    • rte_pktmbuf_pool_create 在大页内存中创建mbuf内存池

      参数值含义
      "mbuf_pool"字符串内存池的名字,调试时用
      nb_mbufs10368池子里有多少个 mbuf
      MEMPOOL_CACHE_SIZE256每个 lcore 的本地缓存大小
      00私有数据大小,不需要就是 0
      RTE_MBUF_DEFAULT_BUF_SIZE宏(通常 2048 + 128)定义在rte_mbuh.h每个 mbuf 的数据区大小
      rte_socket_id()函数返回值当前cpu核心所在的 NUMA 节点编号
    • l2fwd_pktmbuf_pool 是一个指向 struct rte_mempool 的指针。后续网卡收包时,会从这个池子里取 mbuf 来装数据。

    • 为什么要指定 NUMA 节点?

      • 多路服务器上,不同 CPU 访问不同内存条的延迟不同
      • 把内存池分配在本地 NUMA 节点上,访问速度最快
    • nb_mbufs 计算公式:网卡描述符数量(1024收+1024发)+ 突发缓存(32)+ 每核缓存(256)

  • 第七阶段:初始化每个网口(⭐)

    /* Initialise each port */
    RTE_ETH_FOREACH_DEV(portid) {
        struct rte_eth_rxconf rxq_conf;
        struct rte_eth_txconf txq_conf;
        struct rte_eth_conf local_port_conf = port_conf;
        struct rte_eth_dev_info dev_info;
        ...
    }
    

    分成多段来看:

    1. 首先第一段,跳过未启用的端口,同上阶段的代码,并统计实际可用的端口数量 nb_ports_available。

    2. 打印初始化信息并获取网卡能力

      /* init port */
      printf("Initializing port %u... ", portid);
      fflush(stdout);
      
      • 强制刷新输出缓冲区,让文字立刻显示在屏幕上
      ret = rte_eth_dev_info_get(portid, &dev_info);
      if (ret != 0)
       rte_exit(EXIT_FAILURE,
                "Error during getting device (port %u) info: %s\n",
                portid, strerror(-ret));
      
      • rte_eth_dev_info_get 用于查询指定网口的硬件能力和配置信息(填充在dev_info中),声明在头文件<rte_ethdev.h>下,成功返回0;
      • dev_info是一个信息结构体,包含了网卡的各种能力,比如:驱动名称;最大收发队列数;支持的 offload 功能;速度能力(10G/25G/100G)
      • offload功能指是指把原本需要 CPU 软件处理 的网络包操作,交给 网卡硬件 去完成,从而释放 CPU 资源
      if (dev_info.tx_offload_capa & RTE_ETH_TX_OFFLOAD_MBUF_FAST_FREE)
       	local_port_conf.txmode.offloads |=
       	RTE_ETH_TX_OFFLOAD_MBUF_FAST_FREE;
      
      • 开启MBUF_FAST_FREE功能

        • dev_info.tx_offload_capa网卡硬件支持的发送 offload 能力(位掩码);

        • RTE_ETH_TX_OFFLOAD_MBUF_FAST_FREE一个标志位,表示"快速释放 mbuf"功能,通过&运算检查该网卡硬件是否支持该功能,网卡硬件自动完成buf 的释放,不经过软件。减少 CPU 开销,提高小包转发性能。

        • local_port_conf.txmode.offloads要配置给网卡的发送 offload 设置,local_port_conf等于文件开头定义的默认端口配置|=位或赋值,表示把这个标志位加到配置里

    3. 配置网口:1个收队列,1个发队列

      /* Configure the number of queues for a port. */
      ret = rte_eth_dev_configure(portid, 1, 1, &local_port_conf);
      if (ret < 0)
       rte_exit(EXIT_FAILURE, 
                "Cannot configure device: err=%d, port=%u\n",
                ret, portid);
      /* >8 End of configuration of the number of queues for a port. */
      
    • rte_eth_dev_configure 声明在DPDK<rte_ethdev.h>头文件下,用于配置以太网设备。
      • l2fwd 是一个简单转发程序,每个端口只用一个收队列和一个发队列。
    1. 调整描述符数量:因为不同网卡对描述符的数量又不同的限制

      ret = rte_eth_dev_adjust_nb_rx_tx_desc(portid, &nb_rxd, &nb_txd);
      if (ret < 0)
          rte_exit(EXIT_FAILURE,
                   "Cannot adjust number of descriptors: err=%d, port=%u\n",
                   ret, portid);
      
      • rte_eth_dev_adjust_nb_rx_tx_desc声明在DPDK<rte_ethdev.h>头文件下,用于根据网卡硬件限制,调整描述符数量到合法范围。
      • nb_rxd,nb_txd使文件开头定义的全局静态变量(1024),分别表示每个收队列的描述符数量和每个发队列的描述符数量。
    2. 获取并存放网口MAC地址:在转发时,l2fwd 会修改包的源MAC地址,改成发出端口自己的MAC地址。

      ret = rte_eth_macaddr_get(portid,
                                &l2fwd_ports_eth_addr[portid]);
      if (ret < 0)
          rte_exit(EXIT_FAILURE,
                   "Cannot get MAC address: err=%d, port=%u\n",
                   ret, portid);
      
      • rte_eth_macaddr_get声明在DPDK<rte_ethdev.h>头文件下,用于读取网卡的MAC地址,并把MAC地址存到l2fwd_ports_eth_addr[portid]里
      • l2fwd_ports_eth_addr是文件开头定义的全局数组,保存每个端口的MAC地址,后面转发时会用来改写包的源MAC。
    3. 设置网口的收队列

      /* init one RX queue */
      fflush(stdout);//刷新输出缓冲区
      rxq_conf = dev_info.default_rxconf;
      rxq_conf.offloads = local_port_conf.rxmode.offloads;
      /* RX queue setup. 8< */
      ret = rte_eth_rx_queue_setup(portid, 0, nb_rxd,
                                   rte_eth_dev_socket_id(portid),
                                   &rxq_conf,	
                                   l2fwd_pktmbuf_pool);
      if (ret < 0)
          rte_exit(EXIT_FAILURE, "rte_eth_rx_queue_setup:err=%d, port=%u\n",
                   ret, portid);
      /* >8 End of RX queue setup. */
      
      • rte_eth_rx_queue_setup声明在DPDK<rte_ethdev.h>头文件下,用于分配和设置一个收队列,”0“ 是队列编号,l2fwd 只用 1 个队列所以是 0。
      • rte_eth_dev_socket_id声明在DPDK<rte_ethdev.h>头文件下,用于得到网卡所在NUMA节点,指的是网卡这个 PCIe 设备在物理上连接到哪个 CPU,写的是哪个CPU 的内存。
      • l2fwd_pktmbuf_pool,网卡需要知道收包时从哪个内存池取 mbuf 来装数据。
    4. 设置网口的发队列

      /* Init one TX queue on each port. 8< */
      fflush(stdout);
      txq_conf = dev_info.default_txconf;
      txq_conf.offloads = local_port_conf.txmode.offloads;
      ret = rte_eth_tx_queue_setup(portid, 0, nb_txd,
                                   rte_eth_dev_socket_id(portid),
                                   &txq_conf);
      if (ret < 0)
          rte_exit(EXIT_FAILURE, "rte_eth_tx_queue_setup:err=%d, port=%u\n",
                   ret, portid);
      /* >8 End of init one TX queue on each port. */
      
      • rte_eth_tx_queue_setup,声明在DPDK<rte_ethdev.h>头文件下,用于分配和设置一个发队列。
      • 与设置网口的收队列类似,但是它不需要内存池,发包时程序会主动吧要发的mbuf交给网卡,网卡只管发不管从哪来。
    5. 初始化发送缓冲区

      /* Initialize TX buffers */
      tx_buffer[portid] = rte_zmalloc_socket("tx_buffer",
                                      RTE_ETH_TX_BUFFER_SIZE(MAX_PKT_BURST), 0,
                                      rte_eth_dev_socket_id(portid));
      if (tx_buffer[portid] == NULL)
          rte_exit(EXIT_FAILURE, "Cannot allocate buffer for tx on port %u\n",
                   portid);
      
      rte_eth_tx_buffer_init(tx_buffer[portid], MAX_PKT_BURST);
      //错误回调
      ret = rte_eth_tx_buffer_set_err_callback(tx_buffer[portid],
                                               rte_eth_tx_buffer_count_callback,
                                               &port_statistics[portid].dropped);
      if (ret < 0)
          rte_exit(EXIT_FAILURE,
                   "Cannot set error callback for tx buffer on port %u\n",
                   portid);
      //关闭 包类型解析 的功能
      ret = rte_eth_dev_set_ptypes(portid, RTE_PTYPE_UNKNOWN, NULL,
                                                   0);
      if (ret < 0)
          printf("Port %u, Failed to disable Ptype parsing\n",
                 portid);
      
      
      • rte_zmalloc_socket声明在DPDK<rte_malloc.h>头文件下,用于在指定 NUMA 节点上分配清零的内存。”0“ 对齐要求,表示默认。
      • RTE_ETH_TX_BUFFER_SIZE(MAX_PKT_BURST) DPDK<rte_ethdev.h> 自带的宏,用于计算发送缓冲区需要的内存大小。
      • rte_eth_tx_buffer_init声明在DPDK<rte_ethdev.h>头文件下,用于初始化发送缓冲区。
      • rte_eth_tx_buffer_set_err_callback 声明在DPDK<rte_ethdev.h>头文件下,用于设置发送缓冲区的错误回调。
        • rte_eth_tx_buffer_count_callback 声明在DPDK<rte_ethdev.h>头文件下,标准回调函数,作用是把发送失败的包数量累加到 userdata 指向的计数器;
        • port_ststistics文件开头定义的全局数组。
      • rte_eth_dev_set_ptypes声明在DPDK<rte_ethdev.h>头文件下,用于设置网卡要识别哪些包类型。
        • RTE_PTYPE_UNKNOWN,只允许"未知类型",等于全部禁止即关闭所有包类型识别;
        • NULL不需要返回已设置的类型;
        • 0表示数组长度为0。
      • l2fwd 只做二层转发,只看 MAC 地址,不需要知道三层四层是什么协议。关闭这个功能可以:
        • 减少网卡硬件开销;
        • 减少 mbuf 字段的写入。
    6. 启动设备

      /* Start device */
      ret = rte_eth_dev_start(portid);
      if (ret < 0)
          rte_exit(EXIT_FAILURE, "rte_eth_dev_start:err=%d, port=%u\n",
                   ret, portid);
      
      printf("done: \n");
      
      • rte_eth_dev_start声明在DPDK<rte_ethdev.h>头文件下,用于真正启动网卡,开始收发数据。
      • 在这之前,所有的配置(队列、描述符、offload等)只是写入了软件结构体,调用这个函数后,网卡硬件才真正开始工作,启动后很多配置不能再修改。
    7. 开启混杂模式

      if (promiscuous_on) {
          ret = rte_eth_promiscuous_enable(portid);
          if (ret != 0)
              rte_exit(EXIT_FAILURE,
                       "rte_eth_promiscuous_enable:err=%s, port=%u\n",
                       rte_strerror(-ret), portid);
      }
      printf("Port %u, MAC address: " RTE_ETHER_ADDR_PRT_FMT "\n\n",
             portid,
             RTE_ETHER_ADDR_BYTES(&l2fwd_ports_eth_addr[portid]));
      
      • promiscuous_on文件开头定义的全局变量,默认开启
      • rte_eth_promiscuous_enable声明在DPDK<rte_ethdev.h>头文件下,用于开启网卡的混杂模式。
      • l2fwd 为什么要开混杂模式?
        • l2fwd 是转发器,需要处理所有经过的包;
        • 这些包的目的MAC可能不是本机,如果不开启,网卡会直接丢弃。
      • rte_strerror声明在DPDK<rte_string_fns.h>头文件下,用于把错误码转换成可读的错误字符串。
      • RTE_ETHER_ADDR_PRT_FMT 声明在DPDK<rte_ether.h>头文件下,用于格式化打印MAC地址。
      • RTE_ETHER_ADDR_BYTES 声明在DPDK<rte_ether.h>头文件下,用于提取MAC地址的6个字节,供打印使用。
    8. 初始化统计信息

      /* initialize port stats */
      memset(&port_statistics, 0, sizeof(port_statistics));
      
      • port_statistics文件开头定义的全局数组,确保每个端口的统计计数器都从 0 开始。
  • 第八阶段:检查链路

    //检查是否有可用端口
    if (!nb_ports_available) {
        rte_exit(EXIT_FAILURE,
                 "All available ports are disabled. Please set portmask.\n");
    }
    //检查所有端口的链路状态
    check_all_ports_link_status(l2fwd_enabled_port_mask);
    //启动多核转发
    ret = 0;
    /* launch per-lcore init on every lcore */
    rte_eal_mp_remote_launch(l2fwd_launch_one_lcore, NULL, CALL_MAIN);
    //等待所有工作核结束
    RTE_LCORE_FOREACH_WORKER(lcore_id) {
        if (rte_eal_wait_lcore(lcore_id) < 0) {
            ret = -1;
            break;
        }
    }
    
    • nb_ports_available在main函数开头定义的局部变量,在第七阶段的循环中累加。

    • check_all_ports_link_status,l2fwd自己定义的函数,等待所有启用的网口物理链路协商完成。

    • rte_eal_mp_remote_launch声明在DPDK<rte_launch.h>的头文件下,用于在所有 lcore 上并行启动指定的函数。

      参数传入的值含义
      l2fwd_launch_one_lcore函数指针在每个 lcore 上执行的函数,l2fwd自定义
      NULLNULL传给函数的参数(这里不需要)
      CALL_MAIN枚举值主核也参与执行(不仅仅是工作核)
      • DPDK 把 CPU 核分为两类:

        • MAIN lcore(主核):运行 main 函数的那个核
        • WORKER lcore(工作核):其他被 -l 启用的核

        CALL_MAIN 表示主核也要运行 l2fwd_launch_one_lcore,参与转发工作。

        如果传 SKIP_MAIN,主核就只负责管理,不参与转发。

    • RTE_LCORE_FOREACH_WORKER声明在DPDK的<rte_lcore.h>头文件下的宏,用于遍历所有工作核(不包含主核)。

    • rte_eal_wait_lcore声明在DPDK的头文件<rte_launch.h>头文件下,用于等待指定的 lcore 执行完毕。<0表示出错。

      • 为什么只等待 WORKER?

        因为主核本身就在这个 main 函数里,它也在运行 l2fwd_main_loop(死循环)。

        当用户按 Ctrl+C 触发 force_quit 时:

        • 所有核(包括主核)都从死循环里跳出来
        • 主核继续执行 main 函数后面的代码
        • 工作核执行完 l2fwd_launch_one_lcore 返回

        所以主核需要在这里等待所有工作核结束,确保大家都安全退出了,再进行清理

  • 第九阶段:清理退出

    //停止并关闭所有端口
    RTE_ETH_FOREACH_DEV(portid) {
        if ((l2fwd_enabled_port_mask & (1 << portid)) == 0)
            continue;
        printf("Closing port %d...", portid);
        ret = rte_eth_dev_stop(portid);
        if (ret != 0)
            printf("rte_eth_dev_stop: err=%d, port=%d\n",
                   ret, portid);
        rte_eth_dev_close(portid);
        printf(" Done\n");
    }
    
    • rte_eth_dev_stop声明在DPDK<rte_ethdev.h>的头文件下,用于停止以太网设备,暂停手法,可以调用 rte_eth_dev_start 重新启动。
    • rte_eth_dev_close 声明在DPDK<rte_ethdev.h>的头文件下,用于关闭以太网设备,安全释放资源,不能再用除非重新配置。
      • 释放网卡占用的资源
      • 如果之前用 -a 绑定了网卡,这里会解除绑定
    /* clean up the EAL */
    rte_eal_cleanup();
    printf("Bye...\n");
    
    return ret;
    
    • rte_eal_cleanup声明在DPDK<rte_eal.h>的头文件下,用于清理EAL环境。
      • 释放大页内存
      • 关闭各种 DPDK 内部资源
      • 让系统恢复到程序启动前的状态

2. 头文件部分

  • 提供基础功能的标准C库和posix系统调用的头文件
  • DPDK的核心库头文件
  • 全局变量与宏定义:

3. l2fwd自定义函数

  • check_all_ports_link_status

    1. 变量定义和初始化

    2. 主循环结构

      for (count = 0; count <= MAX_CHECK_TIME; count++) 
      
      //检查是否被用户强制退出
      if (force_quit)
          return;
      //初始化本轮检查状态
      all_ports_up = 1;
      //遍历所有DPDK接管的端口
      RTE_ETH_FOREACH_DEV(portid) {
          //只检查用户在-p中启用的端口
          if (force_quit)
              return;
          if ((port_mask & (1 << portid)) == 0)
              continue;
          //获取链路状态
          memset(&link, 0, sizeof(link));
          ret = rte_eth_link_get_nowait(portid, &link);
          if (ret < 0) {
              all_ports_up = 0;
              if (print_flag == 1)
                  printf("Port %u link get failed: %s\n",
                         portid, rte_strerror(-ret));
              continue;
          }
          /* print link status if flag set */
          if (print_flag == 1) {
              rte_eth_link_to_str(link_status_text,
                                  sizeof(link_status_text), &link);
              printf("Port %d %s\n", portid,
                     link_status_text);
              continue;
          }
          /* clear all_ports_up flag if any link down */
          if (link.link_status == RTE_ETH_LINK_DOWN) {
              all_ports_up = 0;
              break;
          }
      }
      /* after finally printing all link status, get out */
      if (print_flag == 1)
          break;
      
      if (all_ports_up == 0) {
          printf(".");
          fflush(stdout);
          rte_delay_ms(CHECK_INTERVAL);
      }
      
      /* set the print_flag if all ports up or timeout */
      if (all_ports_up == 1 || count == (MAX_CHECK_TIME - 1)) {
          print_flag = 1;
          printf("done\n");
      }
      
      • rte_eth_link_get_nowait声明在DPDK的头文件<rte_ethdev.h>下,用于非阻塞地获取链路状态。
      • 当 print_flag == 1 时(所有端口 Up 或者超时了),不再检查状态,而是打印每个端口的最终链路信息。
      • rte_eth_link_to_str,声明在DPDK的头文件<rte_ethdev.h>下,用于把链路状态结构体转换成可读的字符串。
      • RTE_ETH_LINK_DOWN声明在DPDK的头文件<rte_ethdev.h>下的宏定义,表示网卡链路处于断开状态。如果有端口Down,需要等待一段时间再检查。
      • rte_delay_ms声明在DPDK 的头文件<rte_cycles.h>下,用于毫秒级延时,延时CHECK_INTERVAL秒后再检查。
  • l2fwd_launch_one_lcore

    包装函数,调用l2fwd_main_loop()

  • l2fwd_main_loop

    1. 变量定义和初始化

      • drain_tsc即100微妙对应的CPU周期数,使用向上取整的除法技巧。
    2. 获取当前lcore的配置(rx收队列)

      • RTE_LOG声明在DPDK的头文件<rte_log.h>下的宏定义
    3. 主循环结构

      while (!force_quit) {}
      
      • 发送缓冲区刷新 tx

        /* Drains TX queue in its main loop. 8< */fa
        cur_tsc = rte_rdtsc();
        /*
         * TX burst queue drain
         */
         diff_tsc = cur_tsc - prev_tsc;
         if (unlikely(diff_tsc > drain_tsc)) {
             for (i = 0; i < qconf->n_rx_port; i++) {
                 portid = l2fwd_dst_ports[qconf->rx_port_list[i]];
                 buffer = tx_buffer[portid];
                 sent = rte_eth_tx_buffer_flush(portid, 0, buffer);
                 if (sent)
                     port_statistics[portid].tx += sent;
             }
        
             /* if timer is enabled */
             if (timer_period > 0) {
                  timer_tsc += diff_tsc;
                  if (unlikely(timer_tsc >= timer_period)) {
                      if (lcore_id == rte_get_main_lcore()) {
                          print_stats();
                          timer_tsc = 0;
                      }
                  }
             }
             prev_tsc = cur_tsc;
         }
         /* >8 End of draining TX queue. */
        
        1. unlikely 是Linux 内核宏,用于分支预测优化。告诉编译器"这个条件很少为真",让 CPU 流水线优先执行 if 外面的代码。

        这里 unlikely(diff_tsc > drain_tsc) 表示:大多数时候时间还没到 100 微秒,不需要刷新。

        1. 注意这里刷新的是发包端口的缓冲区。因为包是收进来后,往另一个端口发的。

        2. timer_period文件开头定义的全局变量默认为10s,在 l2fwd_parse_args 函数中,解析 -T 参数时赋值。

      • 收包和转发RX Burst

        /* Read packet from RX queues. 8< */
         for (i = 0; i < qconf->n_rx_port; i++) {
            portid = qconf->rx_port_list[i];
        nb_rx = rte_eth_rx_burst(portid, 0,
                                     pkts_burst, MAX_PKT_BURST);
          
            if (unlikely(nb_rx == 0))
                continue;
            
            port_statistics[portid].rx += nb_rx;
          
            for (j = 0; j < nb_rx; j++) {
                m = pkts_burst[j];
             rte_prefetch0(rte_pktmbuf_mtod(m, void *));
                l2fwd_simple_forward(m, portid);
            }
        }
        /* >8 End of read packet from RX queues. */
        
        1. rte_eth_rx_burst声明在DPDK的头文件<rte_ethdev.h>下,用于从收队列批量取包。
        2. rte_prefetch0声明在DPDK的头文件<rte_prefetch.h>下的宏定义,用于预取数据到 CPU 的 L1 缓存,处理当前包时 CPU 就不会等内存了。
        3. rte_pktmbuf_mtod声明在DPDK的头文件<rte_mbuf.h>下的宏定义,用于获取 mbuf 中数据区的指针。
        4. l2fwd_simple_forwardl2fwd 自己定义的函数,执行真正的转发逻辑(修改 MAC、放入发送缓冲区)。
  • l2fwd_simple_forward处理每一个收到的包

    unsigned dst_port;
    int sent;
    struct rte_eth_dev_tx_buffer *buffer;
    //查找目的端口
    dst_port = l2fwd_dst_ports[portid];
    //如果启用更新MAC地址
    if (mac_updating)
        l2fwd_mac_updating(m, dst_port);
    
    buffer = tx_buffer[dst_port];
    sent = rte_eth_tx_buffer(dst_port, 0, buffer, m);
    //统计发送成功的包
    if (sent)
        port_statistics[dst_port].tx += sent;
    
    • l2fwd_mac_upadtingl2fwd自定义函数
    • rte_eth_tx_buffer声明在DPDK的头文件<rte_ethdev.h>下,用于把一个包放入发送缓冲区。
      • 0:包被成功放入缓冲区,等待后续批量发送。
      • > 0:缓冲区满了,触发了一次刷新,返回值是成功发送的包数量。
  • l2fwd_mac_upadting

    struct rte_ether_hdr *eth;
    void *tmp;
    //获取以太网指针
    eth = rte_pktmbuf_mtod(m, struct rte_ether_hdr *);
    //把目的MAC地址改成02:00:00:00:00:xx,其中xx是发出端口号
    /* 02:00:00:00:00:xx */
    tmp = &eth->dst_addr.addr_bytes[0];
    *((uint64_t *)tmp) = 0x000000000002 + ((uint64_t)dest_portid << 40);/
    
    /* src addr */
    rte_ether_addr_copy(&l2fwd_ports_eth_addr[dest_portid], &eth->src_addr);
    
    • rte_pktmbuf_mtod声明在DPDK的头文件<rte_mbuf.h>下的宏定义,用于从 mbuf 获取数据区的指针。
    • 02:00:00:00:00:xx写入内存,小端序,低字节在低地址(02)。
    • ((uint64_t)dest_portid << 40)左移五个字节,即放在第六个字节的位置。
    • rte_ether_addr_copy声明在DPDK的头文件<rte_ether.h>下,用于复制 MAC 地址。发出端口的真实 MAC 地址复制到包的源 MAC 字段。

扩展

尝试l2fwd-event示例(用 Eventdev 框架调度转发):

该示例在 DPDK 25.11 版本中对软件 Eventdev 的配置确实存在兼容性问题,后续将进一步调控尝试。

后测试发现l2fwd-cat、l2fwd-crypto、l2fwd-macsec等均需要特定的软硬件支持。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐