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

这篇具有很好参考价值的文章主要介绍了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 实现了相关函数与数据结构,部分相关源码文件如下所示:
lwip pbuf作用,LwIP,lwip,pbuf,pool,memp,mem

配置

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

  • PBUF_LINK_HLEN: 链路层协议头的长度。默认值为以太网 802.3 标准中定义的 Ethernet II 头长度 14(不带 VLAN)。
    lwip pbuf作用,LwIP,lwip,pbuf,pool,memp,mem
  • PBUF_LINK_ENCAPSULATION_HLEN:链路层 Ethernet II 头之前的附加头长度,例如 802.11 协议就存在一个附加头,如下所示:
    lwip pbuf作用,LwIP,lwip,pbuf,pool,memp,mem
  • PBUF_POOL_BUFSIZE: 定义 MEMP_PBUF_POOL 这个内存池(用于 PBUF_POOL 这种类型的 PBUF)中每个元素的大小。默认设计大小是可以在一个 pbuf 中容纳单个完整的 TCP 帧(包括 TCP MSS、IP 头和链路层头)。
    lwip pbuf作用,LwIP,lwip,pbuf,pool,memp,mem
  • PBUF_POOL_SIZE: 定义 MEMP_PBUF_POOL 这个内存池(用于 PBUF_POOL 这种类型的 PBUF)中元素的个数
    lwip pbuf作用,LwIP,lwip,pbuf,pool,memp,mem
  • 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 pbuf作用,LwIP,lwip,pbuf,pool,memp,mem
  • LWIP_SUPPORT_CUSTOM_PBUF:如果自定义了该宏值,就可以启用 LwIP 中的自定义 PBUF 支持。目前,我所接触到的使用自定义 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)内存空间连续分配一整块内存)。
lwip pbuf作用,LwIP,lwip,pbuf,pool,memp,mem
  这种类型的 pbuf 通常用于发送数据,这是因为发送数据的长度是已知的,这样就可以直接从内存堆中分配指定长度的内空间,从而避免内存浪费。

PBUF_POOL

  PBUF_POOL 类型的 pbuf 表示从 内存池 MEMP_PUBF_POOL(其中元素个数由配置项 PBUF_POOL_SIZE 定义)中分配需要的内存(pbuf 结构体本身内存控件与数据缓冲区(payload)内存空间连续分配一整块内存)。由于内存池的元素大小都是预定义好的,因此,一个元素大小可能无法存放我们需要的数据长度,所以,从内存池分配时有可能需要多个 pbuf 结构连起来表示一包完整数据。
lwip pbuf作用,LwIP,lwip,pbuf,pool,memp,mem
  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
lwip pbuf作用,LwIP,lwip,pbuf,pool,memp,mem
  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 内部直接定死的。
lwip pbuf作用,LwIP,lwip,pbuf,pool,memp,mem
  协议栈从上到下是一个逐级加协议头的过程,为了避免内存复制,pbuf 结构通过为不同的协议栈层保留一块内存区域,用来存放对应层的协议头。这样,对于 PBUF 在经过每一层时,就可以简单的通过移动 payload 指针来填充或者忽略各层头。

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

PBUF_TRANSPORT

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

PBUF_IP

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

PBUF_LINK

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

PBUF_RAW_TX

  PBUF_RAW_TX 类型的 PBUF 仅保留了 PBUF_LINK_ENCAPSULATION_HLEN 头空间,通常只用在链路层发送数据时。LwIP 中,需要传递给 netif->linkoutput() 的 PBUF 通常就是 PBUF_RAW_TX 类型的。
lwip pbuf作用,LwIP,lwip,pbuf,pool,memp,mem
  在 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 pbuf作用,LwIP,lwip,pbuf,pool,memp,mem
  在 LwIP 的移植层文件 ethernetif.c 中,当使用 low_level_input() 接收到数据时,最终通过 netif->input(p, netif) 将接收的数据传递到内核中,这里的 p 就是一个 PBUF_RAW 类型的 PBUF。
lwip pbuf作用,LwIP,lwip,pbuf,pool,memp,mem

自定义 PBUF

  自定义 PBUF 实际上就是对于内存池的一种应用。 通过定义 LWIP_SUPPORT_CUSTOM_PBUF,就可以启用自定义 PBUF 功能,相关数据结构位于 pbuf.h 文件中,如下所示:

#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 的一个特定配置。自定义 PBUF 及使用步骤如下所示:

  1. lwipopts.h 中定义 LWIP_SUPPORT_CUSTOM_PBUF 为 1 启用自定义 PBUF 功能

  2. 在自己的 .c 文件中以类似于定义全局变量的型式使用 LWIP_MEMPOOL_DECLARE 来定义自己的 PBUF 内存池空间,如下所示:

    /*==================== 名字 ======= 个数 ========== 每个的大小 ============ 描述 ==========*/
    LWIP_MEMPOOL_DECLARE(RX_POOL, ETH_RX_BUFFER_CNT, sizeof(RxBuff_t), "Zero-copy RX PBUF pool");
    
  3. 在初始化函数中使用 LWIP_MEMPOOL_INIT(RX_POOL); 初始化定义的内存池空间

  4. 自己实现获取 PBUF 的接口。当调用 pbuf_free 时,将自动调用 pbuf_custom->custom_free_function() 来实际释放!

    /**
      * @brief  Custom Rx pbuf free callback
      * @param  pbuf: pbuf to be freed
      * @retval None
      */
    void pbuf_free_custom(struct pbuf *p)
    {
        struct pbuf_custom* custom_pbuf = (struct pbuf_custom*)p;
        LWIP_MEMPOOL_FREE(RX_POOL, custom_pbuf);
    }
    
    /**
      * @brief  Rx Allocate callback.
      * @param  buff: pointer to allocated buffer
      * @retval None
      */
    void ETH_RxAllocateCallback(uint8_t** buff)
    {
        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; /* 这里必须为每个 PBUF 指定释放内存的接口 */
            /* 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_FLAG_IS_CUSTOM 标志,在释放的时候会判断这个标志 */
            pbuf_alloced_custom(PBUF_RAW, 0, PBUF_REF, p, *buff, ETH_RX_BUF_SIZE);
        }
        else
        {
            RxAllocStatus = RX_ALLOC_ERROR;
            *buff         = NULL;
        }
    }
    
  5. 使用 pbuf_free 释放之前申请的 PBUF。其内部会判断 PBUF 是否为自定义,从而调用自定义 PBUF 的释放接口,如上所示:
    lwip pbuf作用,LwIP,lwip,pbuf,pool,memp,mem

主要接口

  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 各成员变量。
    lwip pbuf作用,LwIP,lwip,pbuf,pool,memp,mem
    这两种类型的 payload 字段指向 pbuf 后固定偏移处。注意,这里的 payload 部分也是用 MEM_ALIGNMENT 对齐的。各种对齐如下所示:
    lwip pbuf作用,LwIP,lwip,pbuf,pool,memp,mem

  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。如下是四种释放情况示例:
lwip pbuf作用,LwIP,lwip,pbuf,pool,memp,mem

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) 来接收或者发送数据(发送描述符和接收描述符内容是不同的) 。
lwip pbuf作用,LwIP,lwip,pbuf,pool,memp,mem
  每次启动发送或者接收,MAC 就会采用 Round-robin 的方式遍历整个描述符 RING,然后解析描述符内容,最后通过内部的 DMA 直接将其中挂载的数据缓冲区中的数据发送出去,或者将接收的数据存放到对应描述符下挂载的缓冲区中。

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

数据流

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

避免复制

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

发送数据

  发送的数据是由 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 性能有关的主要配置项(实际取值需要根据自己的测试情况来),如下:文章来源地址https://www.toymoban.com/news/detail-606933.html

  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

到了这里,关于LwIP 之七 详解 PBUF 结构、通信数据流、性能优化的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【FPGA】Verilog描述电路的三种方式(结构化、数据流和行为化)

    众所周知,Verilog是作为一种HDL(Hardware Description Language,硬件描述语言)出现的,它的主要功能是在不同的抽象层级上描述电路,从而实现电路设计。那么到底该如何描述电路?Verilog提供了3种不同的方式: 结构化描述方式(结构模型,Structural Modeling) 数据流描述方式(数

    2024年01月18日
    浏览(55)
  • OPC通信从入门到精通_1_OPC基础知识及简单C#程序编写(OPCDA,OPCUA简介;OPC通信数据流框架图;C#程序编写)

    OPC的诞生及历史 :软件进行开发时需要与各种不同的协议进行对接,例如Modbus协议等,当设备很多,协议很多的情况下,上位机与硬件的沟通就会变得很麻烦,所以就有了将这些协议抽象出一个标准接口,对于软件人员就无需和协议对接,只需要对接接口即可,因此OPC就诞生

    2024年02月15日
    浏览(50)
  • 什么是Vue的数据流(单向数据流)?如何进行数据流管理

    在Vue中,数据流是指数据的传递和管理方式。Vue采用的是单向数据流,也就是说,数据是从父组件流向子组件,子组件不能直接修改父组件的数据。本文将介绍Vue的数据流机制,以及如何进行数据流管理。 Vue的数据流机制可以分为两类:props和events。 Props 在Vue中,父组件可以

    2024年02月08日
    浏览(62)
  • 银行储蓄系统的顶层数据流图及细化数据流图

    绘制出银行储蓄系统的顶层数据流图及细化数据流图; 银行储蓄系统存、取款流程如下: 1)业务员事先录入利率信息; 2)如果是存款,储户填写存款单,业务员将存款单键入系统,系统更新储户存款信息(存款人姓名、存款人账号、电话号码、身份证号码、存款金额、存

    2024年01月17日
    浏览(48)
  • Elasticsearch:将 ILM 管理的数据流迁移到数据流生命周期

    警告 :此功能处于技术预览阶段,可能会在未来版本中更改或删除。 Elastic 将努力解决任何问题,但技术预览版中的功能不受官方 GA 功能的支持 SLA 的约束。目前的最新版本为 8.12。 在本教程中,我们将了解如何将现有数据流(data stream)从索引生命周期管理 (ILM) 迁移到数据

    2024年04月29日
    浏览(46)
  • 数据流图(DFD)

    数据流图是用于表示系统逻辑模型的一种工具。从数据 传递和加工 的角度,以图形的方式描述数据在系统中流动和处理的过程 数据字典是指对数据的数据项、数据结构、数据流、数据存储、处理逻辑等进行定义和描述,其目的是 对数据流图中的各个元素做出详细的说明 ,

    2024年02月04日
    浏览(51)
  • postman 数据流请求

    备注: Postman version : Version 9.21.3 Windows 版本 1.修改headers 2.Body 部分 选择raw 格式数据 3.最后执行请求

    2024年02月11日
    浏览(63)
  • Flink数据流

    官网介绍 Apache Flink 是一个框架和分布式处理引擎,用于对无界和有界数据流进行有状态计算。Flink 被设计为在所有常见的集群环境中运行,以内存中的速度和任何规模执行计算。 1.无限流有一个开始,但没有定义的结束。它们不会在生成数据时终止并提供数据。必须连续处

    2024年02月17日
    浏览(48)
  • Java文件读写数据流

    以下这几个类都是抽象类.并且都有对于文件操作的具体实现类.File+类名就是具体的实现类 1.1.1.InputStream 以二进制方式读.有两个主要方法. 1.read(); 该方法有三个版本 无参: read() 读取一个字节的数据,返回 -1 表示读取结束 一个参数: read(byte[] b) 最多读取 b.length 字节的数据到 b

    2024年02月16日
    浏览(44)
  • C# 数据流 FileStream

     

    2024年03月24日
    浏览(44)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包