LwIP 之七 详解 PBUF 结构、通信数据流、性能优化

  数据包的复制在协议栈中是非常耗时的一个操作。LwIP 协议栈内部使用 pbuf 这种数据结构来对数据进行传递,灵活的 pbuf 结构体使得数据在不同网络层之间传递时可以减少内存的开销,避免频繁的内存复制,增加数据在不同层之间传递的速度。

简介

  Packet buffers 简称 pbuf,是一种用于在协议栈内部传递数据的数据结构,起源于 FreeBSD 协议栈。FreeBSD 协议栈使用 mbuf(Massage Buffer)来在协议栈内部传递数据,pbuf 是 mbuf 的简化版本。

Linux 协议栈使用的是 sk_buff(socket buffer)

  我们直接使用 PBUF 最多的情况就是移植层(例如 ethernetif.c)的数据收发接口(ethernetif_inputlow_level_output)中。此外,如果直接使用裸机 LwIP 进行编程(只能使用 Raw API),也必须直接使用 PBUF。

  pbuf 是对 LwIP 中内存堆(详见 LwIP 之五 详解内存堆(mem.c/h)动态内存管理策略)和内存池(详见 LwIP 之六 详解内存池(memp.c/h)动态内存管理策略)的一个基本应用,具体是在 pbuf.cpubf.h 实现了相关函数与数据结构,部分相关源码文件如下所示:
在这里插入图片描述

配置

  通常,PBUF 的相关配置与网卡的基本配置以及 TCP/IP 协议栈是相匹配的。对于以太网的移植(ethernetif.c),大部分参数采用默认值即可。与 PBUF 相关配置项说明如下:

  • PBUF_LINK_HLEN: 链路层协议头的长度。默认值为以太网 802.3 标准中定义的 Ethernet II 头长度 14(不带 VLAN)。
    在这里插入图片描述

  • PBUF_LINK_ENCAPSULATION_HLEN:链路层 Ethernet II 头之前的附加头长度,例如 802.11 协议就存在一个附加头,如下所示:
    在这里插入图片描述

  • PBUF_POOL_BUFSIZE: 定义 MEMP_PBUF_POOL 这个内存池(用于 PBUF_POOL 这种类型的 PBUF)中每个元素的大小。默认设计大小是可以在一个 pbuf 中容纳单个完整的 TCP 帧(包括 TCP MSS、IP 头和链路层头)。
    在这里插入图片描述

  • PBUF_POOL_SIZE: 定义 MEMP_PBUF_POOL 这个内存池(用于 PBUF_POOL 这种类型的 PBUF)中元素的个数
    在这里插入图片描述

  • LWIP_PBUF_REF_T:pbuf 中 ref 字段的数据类型,默认是 u8_t 类型。如果改为其他类型,需要注意一下结构体对齐。详见下文的 struct pbuf

  • LWIP_PBUF_CUSTOM_DATA: 用户自定义数据。在 PBUF 成员与数据之间增加一块自定义区域,可以用来填充对齐

  • MEMP_NUM_PBUF:定义 MEMP_PBUF 这个内存池(用于 PBUF_REFPBUF_ROM 这两种类型的 PBUF)中元素的个数。其每个元素的大小未固定值 sizeof(struct pbuf)
    在这里插入图片描述

  • LWIP_SUPPORT_CUSTOM_PBUF:自定义 PBUF。如果定义了该宏值,则需要提供一系列自定义实现(重点注意处理各种对齐操作)。

    #if LWIP_SUPPORT_CUSTOM_PBUF
    /** Prototype for a function to free a custom pbuf */
    typedef void (*pbuf_free_custom_fn)(struct pbuf *p);
    
    /** A custom pbuf: like a pbuf, but following a function pointer to free it. */
    struct pbuf_custom {
          
          
      /** The actual pbuf */
      struct pbuf pbuf;
      /** This function is called when pbuf_free deallocates this pbuf(_custom) */
      pbuf_free_custom_fn custom_free_function;
    };
    #endif /* LWIP_SUPPORT_CUSTOM_PBUF */
    

      默认情况下,除非外部驱动程序或应用程序代码需要,否则自定义 PBUF 仅用于 IP_FRAG 的一个特定配置。STM32Cube 给出的 DEMO 也使用了自定义 PBUF,流程具体如下:

    1. 首先需要使用 LWIP_MEMPOOL_DECLARE 来定义自己的 PBUF 内存空间,如下所示:
      typedef struct
      {
              
              
          struct pbuf_custom pbuf_custom;
          uint8_t buff[(ETH_RX_BUF_SIZE + 31) & ~31] __ALIGNED(32);
      } RxBuff_t;
      
      /* Memory Pool Declaration */
      #define ETH_RX_BUFFER_CNT             12U
      LWIP_MEMPOOL_DECLARE(RX_POOL, ETH_RX_BUFFER_CNT, sizeof(RxBuff_t), "Zero-copy RX PBUF pool");
      
    2. 在初始化中使用 PBUF 提供的 pbuf_alloced_custom() 接口来进行初始化以及定义释放 PBUF 的接口 void pbuf_free_custom(struct pbuf *p),然后赋值给 p->custom_free_function,如下所示:
      struct pbuf_custom* p = LWIP_MEMPOOL_ALLOC(RX_POOL);
      
      if (p)
      {
              
              
          /* Get the buff from the struct pbuf address. */
          *buff                   = (uint8_t*)p + offsetof(RxBuff_t, buff);
          p->custom_free_function = pbuf_free_custom;
          /* Initialize the struct pbuf.
           * This must be performed whenever a buffer's allocated because it may
           * be changed by lwIP or the app, e.g., pbuf_free decrements ref. */
          pbuf_alloced_custom(PBUF_RAW, 0, PBUF_REF, p, *buff, ETH_RX_BUF_SIZE);
      }
      else
      {
              
              
          RxAllocStatus = RX_ALLOC_ERROR;
          *buff         = NULL;
      }
      

    目前,我所接触到的使用自定义 PBUF 的情况是在 ethernetif.c 中,使用这种方式可以避免数据复制。

struct pbuf

  pbuf 结构体 struct pbuf 定义于 /src/include/lwip/pbuf.h 文件中,结构非常简单。在 LwIP 中,一个数据包可以由多个 pbuf 组成,这多个 pbuf 通过成员 struct pbuf *next 串联为一个单向链表,称为 pbuf 链。

/** Main packet buffer struct */
struct pbuf {
    
    
  /** 指向 PBUF 链中的下一个成员,最后一个为 NULL */
  struct pbuf *next;

  /** 指向数据缓冲区的指针 */
  void *payload;

  /**
   * total length of this buffer and all next buffers in chain
   * belonging to the same packet.
   *
   * For non-queue packet chains this is the invariant:
   * p->tot_len == p->len + (p->next? p->next->tot_len: 0)
   */
  u16_t tot_len;

  /** length of this buffer */
  u16_t len;

  /** a bit field indicating pbuf type and allocation sources
      (see PBUF_TYPE_FLAG_*, PBUF_ALLOC_FLAG_* and PBUF_TYPE_ALLOC_SRC_MASK)
    */
  u8_t type_internal;

  /** misc flags */
  u8_t flags;

  /**
   * the reference count always equals the number of pointers
   * that refer to this pbuf. This can be pointers from an application,
   * the stack itself, or pbuf->next pointers from a chain.
   */
  LWIP_PBUF_REF_T ref;

  /** For incoming packets, this contains the input netif's index */
  u8_t if_idx;

  /** In case the user needs to store data custom data on a pbuf */
  LWIP_PBUF_CUSTOM_DATA
};

  pbuf 结构本身在内存中申请时也是用了大量的内存对齐,这些对齐在我们使用 PBUF 中将提供极大的便利。我们先暂时不关注对齐,等 API 章节再从源码上详细介绍都有哪些地方用到了内存对齐。

pbuf 链

  因为网络中的数据包可能很大,而 pbuf 能管理的数据包大小有限,就会采用链表的形式将所有的 pbuf 包连接起来,这样才能完整描述一个数据包,这些连接起来的 pbuf 包会组成一个链表,称之为 pbuf 链。

  struct pbuf 中的 next 字段用于指向下一个 pbuf,最后一个 pbuf 的 next 为 NULL,从而形成 pbuf 链。但是,在实际使用时,我们通常会将 pbuf 数据缓冲区直接定义为以太网帧的大小,从而尽量不使用 pbuf 链。

数据 buffer

  struct pbuf 中的 payload 字段指向该 pbuf 管理的数据缓冲区的起始地址,这里的数据缓冲区可以是紧跟在 pbuf 结构体地址后面的 RAM 空间,也可以是 ROM 或其他 RAM 中的某个地址,取决于pbuf 的类型。

数据长度

  struct pbuf 中的 tot_len 字段记录的是当前 pbuf 及其后续 pbuf 的所有数据的长度。例如,如果当前 pbuf 是 pbuf 链表上第一个数据结构,那么 tot_len 就记录着整个 pbuf 链中所有 pbuf 中数据的长度。而 len 字段则表示当前 pbuf 中的数据长度

类型标志位

  为了表示各种 PBUF 的不同用途和来源,LwIP 定义了很多标志位,这些标志位被记录在 pbuf 的相应字段中。有些标志位会在实际代码中解析使用,有些则不会。struct pbuf 中的 type_internal 表示 pbuf 类型和分配来源,按位使用,取值为以下宏的组合(以下省略部分没有实际使用标志):

  • PBUF_TYPE_FLAG_STRUCT_DATA_CONTIGUOUS: 取值 0x80,指示数据 buffer 直接跟在 pbuf 结构体后面
  • PBUF_TYPE_FLAG_DATA_VOLATILE:取值 0x40,指示存储在此 PBUF 中的数据可能会更改。如果需要排队,则必须复制。
  • PBUF_ALLOC_FLAG_RX:取值 0x0100,表示该 pbuf 用于 RX(如果未设置,则表示用于 TX)
  • PBUF_ALLOC_FLAG_DATA_CONTIGUOUS:指示应用程序需要将 pbuf 的 payload 作为一个整体
  • PBUF_TYPE_ALLOC_SRC_MASK_STD_HEAP:取值 0x00,表示此 pbuf 是从内存堆分配的
  • PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF:取值 0x01,表示此 pbuf 是从 MEMP_PBUF 分配的
  • PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF_POOL:取值 0x02,表示 pbuf 是从 MEMP_PBUF_POOL 分配的

flags

  struct pbuf 中的 flags 用来指示一些 pbuf 配置信息,这些标志位被用在 LwIP 协议栈内部来标识 payload 中数据的一些信息,从而在不同协议层进行判断和处理。flags 取值为以下按位使用的宏值的组合

  • PBUF_FLAG_PUSH: 取值 0x01,指示此数据包的数据应立即传递给应用程序
  • PBUF_FLAG_IS_CUSTOM:取值 0x02,表示这是一个用户自定义的 pbuf。注意,不同于其他宏值,这个宏值是 PBUF 内部自己使用的!
  • PBUF_FLAG_MCASTLOOP:取值 0x04,表示该 pbuf 是要回环的 UDP 组播
  • PBUF_FLAG_LLBCAST:取值 0x08,表示该 pbuf 中收到的是链路广播帧
  • PBUF_FLAG_LLMCAST:取值 0x10,表示该 pbuf 中收到的是链路组播帧
  • PBUF_FLAG_TCP_FIN:取值 0x20,表示该 pbuf 包含 TCP FIN 标志

ref 机制

  struct pbuf 中的 ref 字段表示该 pbuf 被引用的次数,申请 pbuf 的时候,ref 会被设置为 1,在释放时,会先将 ref 减 1,如果减 1 后的 ref 为 0 才会释放该 pbuf。

  该字段是 LwIP 提供的一个 pbuf 特性,其允许由用户来自行释放协议栈发送数据时传递到底层的 pbuf(不用阻塞等待网卡发送完成)。这个特性在发送数据的性能优化中就会被使用,后文性能优化章节再详细说明。

其他

  • if_idx:用于记录传入的数据包中的 netif 的编号,也就是 struct netifnum 字段
  • LWIP_PBUF_CUSTOM_DATA:用于预留一部分用户自定义空间

类型

  pbuf 类型决定了 pbuf 内存空间是从哪里分配而来的,由定义于 /src/include/lwip/pbuf.h 文件中的 pbuf_type 枚举来表示。之所以定义使用不同的内存位置的类型主要就是为了适应不同的应用场景。

/**
 * @ingroup pbuf
 * Enumeration of pbuf types
 */
typedef enum {
    
    
  /** pbuf data is stored in RAM, used for TX mostly, struct pbuf and its payload
      are allocated in one piece of contiguous memory (so the first payload byte
      can be calculated from struct pbuf).
      pbuf_alloc() allocates PBUF_RAM pbufs as unchained pbufs (although that might
      change in future versions).
      This should be used for all OUTGOING packets (TX).*/
  PBUF_RAM = (PBUF_ALLOC_FLAG_DATA_CONTIGUOUS | PBUF_TYPE_FLAG_STRUCT_DATA_CONTIGUOUS | PBUF_TYPE_ALLOC_SRC_MASK_STD_HEAP),
  /** pbuf data is stored in ROM, i.e. struct pbuf and its payload are located in
      totally different memory areas. Since it points to ROM, payload does not
      have to be copied when queued for transmission. */
  PBUF_ROM = PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF,
  /** pbuf comes from the pbuf pool. Much like PBUF_ROM but payload might change
      so it has to be duplicated when queued before transmitting, depending on
      who has a 'ref' to it. */
  PBUF_REF = (PBUF_TYPE_FLAG_DATA_VOLATILE | PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF),
  /** pbuf payload refers to RAM. This one comes from a pool and should be used
      for RX. Payload can be chained (scatter-gather RX) but like PBUF_RAM, struct
      pbuf and its payload are allocated in one piece of contiguous memory (so
      the first payload byte can be calculated from struct pbuf).
      Don't use this for TX, if the pool becomes empty e.g. because of TCP queuing,
      you are unable to receive TCP acks! */
  PBUF_POOL = (PBUF_ALLOC_FLAG_RX | PBUF_TYPE_FLAG_STRUCT_DATA_CONTIGUOUS | PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF_POOL)
} pbuf_type;

  注意,每种类型的 PBUF 是按照其用途进行了特定赋值的(各值的解释见上文),类型会被记录在 type_internal 字段中(这个字段只有低四位是用作类型(PBUF_TYPE_ALLOC_SRC_MASK)),在分配或释放时会解析。

PBUF_RAM

  PBUF_RAM 类型的 pbuf 表示从 内存堆 ram_heap(内存大小由配置项 MEM_SIZE 定义) 中分配需要的内存,通常是一块连续的内存空间(pbuf 结构体本身内存控件与数据缓冲区(payload)内存空间连续分配一整块内存)。
在这里插入图片描述
  这种类型的 pbuf 通常用于发送数据,这是因为发送数据的长度是已知的,这样就可以直接从内存堆中分配指定长度的内空间,从而避免内存浪费。

PBUF_POOL

  PBUF_POOL 类型的 pbuf 表示从 内存池 MEMP_PUBF_POOL(其中元素个数由配置项 PBUF_POOL_SIZE 定义)中分配需要的内存(pbuf 结构体本身内存控件与数据缓冲区(payload)内存空间连续分配一整块内存)。由于内存池的元素大小都是预定义好的,因此,一个元素大小可能无法存放我们需要的数据长度,所以,从内存池分配时有可能需要多个 pbuf 结构连起来表示一包完整数据。
在这里插入图片描述
  PBUF_POOL 通常用在接收数据中,因为接收数据不是定长的,因此,通常将 MEMP_PBUF_POOL 的元素大小定义为协议的最大大小(以太网 1500),然后将 PBUF_POOL 类型的 pbuf 分配给 NIC,当 NIC 收到数据后,将数据放到此 pbuf 中,进而传递到协议栈内部。

PBUF_ROM 和 PBUF_REF

  源码中,PBUF_ROMPBUF_REF 的实现是一样的,这俩共同特点都是用户缓冲区(payload)均独立于 pbuf,放到了其他位置,而 pbuf 本身的内存空间在内存池 MEMP_PBUF 中(其中元素个数由配置项 MEMP_NUM_PBUF 定义)。PBUF_ROM 实际就是用户缓冲区(payload)放到 ROM 的一种特殊情况的 PBUF_REF
在这里插入图片描述
  PBUF_ROM 实际用的比较少,因为其数据不可变,因此通常需要进行复制,而 PBUF_REF 则用的相对多一些,其最常用的就是自定义数据内存位置的情况。在某些情况下,我们的数据是有专门的存放位置的,此时就可以使用 PBUF_REF,然后将 payload 指向我们的数据地址即可。

层类型

  pbuf 层类型指定了要将 pbuf 用在协议栈的哪一层,由定义于 /src/include/lwip/pbuf.h 文件中的 pbuf_layer 枚举来表示。不同的层类型会在 PBUF 的 payload 中预留不同的空间,类型值就是预留的空间字节数(老版本 LwIP 是在函数中 switch 的)

typedef enum {
    
    
  /** Includes spare room for transport layer header, e.g. UDP header.
   * Use this if you intend to pass the pbuf to functions like udp_send().
   */
  PBUF_TRANSPORT = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN + PBUF_IP_HLEN + PBUF_TRANSPORT_HLEN,
  /** Includes spare room for IP header.
   * Use this if you intend to pass the pbuf to functions like raw_send().
   */
  PBUF_IP = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN + PBUF_IP_HLEN,
  /** Includes spare room for link layer header (ethernet header).
   * Use this if you intend to pass the pbuf to functions like ethernet_output().
   * @see PBUF_LINK_HLEN
   */
  PBUF_LINK = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN,
  /** Includes spare room for additional encapsulation header before ethernet
   * headers (e.g. 802.11).
   * Use this if you intend to pass the pbuf to functions like netif->linkoutput().
   * @see PBUF_LINK_ENCAPSULATION_HLEN
   */
  PBUF_RAW_TX = PBUF_LINK_ENCAPSULATION_HLEN,
  /** Use this for input packets in a netif driver when calling netif->input()
   * in the most common case - ethernet-layer netif driver. */
  PBUF_RAW = 0
} pbuf_layer;

  其中,PBUF_LINK_ENCAPSULATION_HLENPBUF_LINK_HLEN 这俩是配置项,上文介绍过了;PBUF_IP_HLEN 就是 IP 的头长度,PBUF_TRANSPORT_HLEN 表示传输层头长度,这俩是在 PBUF 内部直接定死的。
在这里插入图片描述
  协议栈从上到下是一个逐级加协议头的过程,为了避免内存复制,pbuf 结构通过为不同的协议栈层保留一块内存区域,用来存放对应层的协议头。这样,对于 PBUF 在经过每一层时,就可以简单的通过移动 payload 指针来填充或者忽略各层头。

  注意,在实际实现中,TCP/IP 协议栈中各层的头并不是定长的,因此,LwIP 预留的仅仅是最常用的协议的头长度,一般不包含协议头的扩展内容。此外,由于 TCP/IP 协议栈中协议众多,协议头差别也很大,因此,预留的长度是按照常用的协议预留的。

PBUF_TRANSPORT

  PBUF_TRANSPORT 表示传输层专用的 PBUF,该类型的 PBUF 预留了从传输层到链路层的所有协议头的空位,通常只用在传输层发送数据时。
在这里插入图片描述
  一般情况下,我们用不到这种类型的 PBUF。LwIP 中,需要传递给 udp_send() 的 PBUF 通常就是 PBUF_TRANSPORT 类型的。因此,在 LwIP 内部的传输层相关接口中,存在大量使用 PBUF_TRANSPORT 类型的 PBUF 的地方。
在这里插入图片描述

PBUF_IP

  PBUF_IP 表示网络层专用的 PBUF,该类型的 PBUF 预留了从网络层到链路层的所有协议头的空位,通常只用在网络层发送数据时。
在这里插入图片描述
  一般情况下,我们用不到这种类型的 PBUF。LwIP 中,需要传递给 raw_send() 的 PBUF 通常就是 PBUF_IP 类型的。

PBUF_LINK

  PBUF_LINK 类型的 PBUF 仅保留了数据链路层协议头空间(PBUF_LINK_ENCAPSULATION_HLENPBUF_LINK_HLEN),通常只用在链路层发送数据时。
在这里插入图片描述
  一般情况下,我们用不到这种类型的 PBUF。LwIP 中,需要传递给 ethernet_output() 的 PBUF 通常就是 PBUF_LINK 类型的。例如,netif_loop_output 回环接口、arp 发送数据时等等
在这里插入图片描述

PBUF_RAW_TX

  PBUF_RAW_TX 类型的 PBUF 仅保留了 PBUF_LINK_ENCAPSULATION_HLEN 头空间,通常只用在链路层发送数据时。LwIP 中,需要传递给 netif->linkoutput() 的 PBUF 通常就是 PBUF_RAW_TX 类型的。
在这里插入图片描述
  在 LwIP 的移植层文件 ethernetif.c 中,在 low_level_output() 中申请的 PBUF 通常也是 PBUF_RAW_TX 类型的,使用这种类型可以最大程度上节省内存。

由于 PBUF_LINK_ENCAPSULATION_HLEN 通常是 0,因此,PBUF_RAW_TXPBUF_RAW 是一样的!

PBUF_RAW

  PBUF_RAW 不预留任何协议头空间(offset = PBUF_RAW = 0) ,通常只用在接收数据时。LwIP 中,需要传递给 netif->input() 的 PBUF 通常就是 PBUF_RAW 类型的。
在这里插入图片描述
  在 LwIP 的移植层文件 ethernetif.c 中,当使用 low_level_input() 接收到数据时,最终通过 netif->input(p, netif) 将接收的数据传递到内核中,这里的 p 就是一个 PBUF_RAW 类型的 PBUF。
在这里插入图片描述

主要接口

  pbuf 的各个接口在实现上并不复杂,下面根据情况以图示或者源码注释的形式来介绍一下主要的几个接口。

pbuf_alloc

  pbuf_alloc 用来根据入参分配 pbuf。如果从内存堆申请,通常是一个 pbuf( next 字段必为 NULL),但是,如果从内存池申请,则可能是一个 pbuf 链(最后一个 pbuf 的 next 字段必为 NULL)。

  1. 对于 PBUF_ROMPBUF_REF 这两种类型,pbuf_alloc 调用 pbuf_alloc_reference 仅从 MEMP_PBUF 这个内存池中申请的 pbuf 本身的空间且其 payload 字段为 NULL,需要由使用者来赋值。

    payload 的对齐问题也由用户自己处理

  2. PBUF_POOL 是从 MEMP_PBUF_POOL 内存池中分配 pbuf 本身 + payload,PBUF_RAM 则是从内存堆中分配 pbuf 本身 + payload,然后通过 pbuf_init_alloced_pbuf 初始 PBUF 各成员变量。
    在这里插入图片描述
    这两种类型的 payload 字段指向 pbuf 后固定偏移处。注意,这里的 payload 部分也是用 MEM_ALIGNMENT 对齐的。各种对齐如下所示:
    在这里插入图片描述

  3. MEMP_PBUF_POOL 内存池中分配需要循环处理是因为,池中的单个元素大小可能小于我们实际需要的大小,因此,需要多个池元素串成一个 PBUF 链。

  4. pbuf_init_alloced_pbuf 中会将 ref 字段值为 1,pbuf 的类型被保存在 type_internal 字段中,在释放时会被使用。

pbuf_free

  pbuf_free 遍历 pbuf 链,依次将每个 pbuf 中 ref 字段的值减 1,如果 ref 为零,则释放 pbuf,最终返回释放的 pbuf 个数。注意,如果遇到不是 0 的 pbuf,则立即终止遍历后续 pbuf。如下是四种释放情况示例:
在这里插入图片描述

pbuf_ref

  pbuf_ref用来将 pbuf 中 ref 字段的值增 1。pbuf 中 ref 字段就是记录 pbuf 数据包被引用的次数, 在申请 pbuf 的时候,ref 字段就被初始化为 1,当释放 pbuf 的时候,先将 ref 减 1,如果 ref 减 1 后为 0,则表示能释放 pbuf。

pbuf_cat

  pbuf_cat 用来将两个 pbuf 链链接在一起(入参的后者被链接到前者上)。注意,链接之后就是一条 pbuf 链了,第二个入参的 pbuf 链不要在单独使用了。释放时也只需要释放一次即可。

性能优化

  实际上,PBUF 这种结构是一种非常适用于网卡中的 MAC 硬件设计的数据结构。大多数网卡中 MAC 的硬件设计都是使用描述符 RING(每个描述符下挂载一个数据 buffer) 来接收或者发送数据(发送描述符和接收描述符内容是不同的) 。
在这里插入图片描述
  每次启动发送或者接收,MAC 就会采用 Round-robin 的方式遍历整个描述符 RING,然后解析描述符内容,最后通过内部的 DMA 直接将其中挂载的数据缓冲区中的数据发送出去,或者将接收的数据存放到对应描述符下挂载的缓冲区中。

  因此,性能的第一个优化点就是要定义与网卡(通常是 MAC)以及 TCP/IP 协议栈相匹配的资源。例如,PBUF_POOL_SIZEMEMP_NUM_NETCONNMEM_SIZE 等配置项的数值必须合理。

数据流

  LwIP 内部数据流都是通过 PBUF 进行传递的。在其移植层文件 ethernetif.c 中,接收数据后需要将数据放到 PBUF 中传递到协议栈内部,同样,发送的数据在 LwIP 内部也是通过 PBUF 逐层传递到移植层文件 ethernetif.c 中。
在这里插入图片描述
  在使用了操作系统时,在这个数据传递流程中,通常都是通过各种消息来传递数据,因此,务必保证各种传递资源的足够。例如,TCPIP_MBOX_SIZEDEFAULT_UDP_RECVMBOX_SIZEDEFAULT_TCP_RECVMBOX_SIZE 等等这些宏值要定义足够资源。

避免复制

  数据包的复制在协议栈中是非常耗时的一个操作。因此,在实际对 LwIP 进行移植时,必须尽量避免对数据包复制。通常的做法是将 pbuf 的 payload 直接作为网卡描述符的数据 buffer。
在这里插入图片描述

发送数据

  发送的数据是由 LwIP 内部申请的一个 pbuf 传送到网卡,通常做法是直接将 pbuf 的 payload 当做描述符的数据 buffer(通常在初始化网卡时,每个发送描述符不会申请数据 buffer),从而避免复制数据包。

  但是,此时就面临一个严重问题:如果函数立刻返回,到了协议栈内部就会尝试释放 pbuf,而此时,有可能网卡正在发送数据,而如果一直等到发送完成,势必严重影响效率(网卡只能使用一个描述符)!

  通常做法是,使用 pbuf 提供的 ref 特性。将 pbuf 的 payload 当做描述符的数据 buffer 后,我们会自行保存好当前 pbuf,然后通过 pbuf 的 api 将 ref 增加,函数立刻返回,当 LwIP 内部想要释放 pbuf 时,就会由于 ref 的问题忽略释放,而当实际发送完成后,我们自行释放 pbuf,这样极限情况下,网卡就可以用满所有描述符。

接收数据

  通常在初始化网卡时,每个接收描述符对应申请一个 pbuf,直接将 PBUF 的 payload 当做描述符的数据 buffer,这样在接收数据之后,就可以再次申请一个 pbuf 替换当前描述符下已经存放数据的 pbuf,然后直接将存放数据的 pbuf 传递给协议栈,从而避免复制数据包。

netbuf

  LwIP 中的 struct netbuf 是一个在 Socket API 和 Netconn API 中使用的一种数据结构,其核心就是 PBUF。在 Socket API 和 Netconn API 内部会使用 struct netbuf 来传递数据。

  LwIP 中的 Socket API 是在 Netconn API 基础上实现的。在进行 Socket API 和 Netconn API 编程时(需要有操作系统支持),PBUF 被封装在 struct netbuf 中,在 Socket API 和 Netconn API 内部传递数据。

Raw API 编程:通常是裸机 LwIP 编程,此时只能使用 Raw API,我们需要直接使用 PBUF 进行数据传递。

  注意,定义 LWIP_NETIF_TX_SINGLE_PBUF 宏值时,数据会被复制到申请的 struct netbuf 中,否则,LwIP 内部直接引用用户数据,必须要等待网卡把数据发送完成才可以释放用户数据空间!

减少任务调度

  在配合操作系统使用 LwIP 时,频繁的任务调度将大大降低 LwIP 收发数据的性能。因此,在某些需求下,我们通常会采用临时关闭调度,待网卡接收一定数据量之后再开启调度的方式来保证性能。数据接收举例如下:

static void ethernetif_input(void *argument)
{
    
    
    void *new_msg = NULL;
    struct pbuf *p;
    struct netif *netif = (struct netif *)argument;
    int work = 0;

    for (;;)
    {
    
    
        if (sys_arch_mbox_fetch(&cur_netif.mbox_netif_rcv, &new_msg, 0) != SYS_ARCH_TIMEOUT)
        {
    
    
            netif = (struct netif *)new_msg;
            vTaskSuspendAll();
            do
            {
    
    
                p = low_level_input(netif);
                if (p != NULL)
                {
    
    
                    if (netif->input(p, netif) != ERR_OK)
                    {
    
    
                        LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_input: pack input error\n"));
                        pbuf_free(p);
                    }
                    work++;
                    if (work >= 8)
                    {
    
    
                        xTaskResumeAll();
                        vTaskSuspendAll();
                        work = 0;
                    }
                }
            } while (p != NULL);

            xTaskResumeAll();
            work = 0;

            /* we must enable interrupts again,because these are disabled in MAC_IRQHandler */
            GMAC_QueueITConfig(USED_GMAC, GMAC_Q_IT_RX_PKT_CMPL, ENABLE);
        }
    }
}

合理的任务优先级

  在配合操作系统使用 LwIP 时,合理安排每个任务的优先级可以提高性能。以 FreeRTOS 下 UDP 接收数据为例,正常运行后,通常会有以下四个任务在运行:

  1. UDP 接收任务。这个是用户应用,我们在这里计算接收性能。多数人可能会认为,这个任务的优先级要高,实则不然。这个任务与 ethernetif_input 任务通常是一个优先级,因为,UDP 接收任务是被阻塞的,只有 ethernetif_input 任务工作后,才可以触发 UDP 接收任务。
  2. ethernetif_input 任务。该任务负责将网卡数据传递到 LwIP 内部。该任务优先级次之
  3. ethernetif_link_check 网络连接检测任务。该任务优先级最低!
  4. tcpip_thread 任务。这个任务是 LwIP 内部用来处理各种消息传递。此外它还负责处理各种超时定时器(因此,这个任务必须有足够优先级,否则导致定时器不准确)。通常我们会定义 LWIP_TCPIP_CORE_LOCKING_INPUT 为 1 来直接传递数据,避免消息传递,因此,这个任务优先级也可以低一些。

TCP

  通常,UDP 的性能优化很容易达到线速,但是,TCP 则比价难。这里记录几个 TCP 性能有关的主要配置项(实际取值需要根据自己的测试情况来),如下:

  1. TCP_SND_BUF
  2. TCP_WND
  3. PBUF_POOL_SIZE

参考

  1. https://www.cnblogs.com/-Angel/p/5028096.html
  2. https://blog.csdn.net/u010261063/article/details/118254462
  3. https://blog.csdn.net/weixin_44821644/article/details/111090269
  4. https://blog.csdn.net/Sundy19890210/article/details/122307262

猜你喜欢

转载自blog.csdn.net/ZCShouCSDN/article/details/130943435