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

news2024/11/25 2:21:58

  数据包的复制在协议栈中是非常耗时的一个操作。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 协议栈是相匹配的。对于以太网的移植,大部分参数采用默认值即可。与 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 字段的数据类型

  • LWIP_PBUF_CUSTOM_DATA: 用户自定义数据

  • 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 的一个特定配置。使用流程具体如下:

    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 中,使用这种方式可以避免数据复制。

结构

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

/** Main packet buffer struct */
struct pbuf {
  /** next pbuf in singly linked pbuf chain */
  struct pbuf *next;

  /** pointer to the actual data in the buffer */
  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 不是这样)。

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_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 仅从 MEMP_PBUF 这个内存池中申请的 pbuf 本身的空间且其 payload 字段为 NULL,需要由使用者来赋值。

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

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

  3. ref 字段值为 1

  4. 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 接收任务。这个是用户应用,我们在这里计算接收性能。在极限性能下,该任务优先级必须最高
  2. ethernetif_input 任务。该任务负责将网卡数据传递到 LwIP 内部。该任务优先级次之
  3. ethernetif_link_check 网络连接检测任务。该任务优先级最低!
  4. tcpip_thread 任务。这个任务是 LwIP 内部用来处理各种消息传递。此外它还负责处理各种超时定时器。通常我们会定义 LWIP_TCPIP_CORE_LOCKING_INPUT 为 1 来直接传递数据,避免消息传递,因此,这个任务优先级也可以低一些。

参考

  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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/587969.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OJ练习第122题——交错字符串

交错字符串 力扣链接:97. 交错字符串 题目描述 给定三个字符串 s1、s2、s3,请你帮忙验证 s3 是否是由 s1 和 s2 交错 组成的。 两个字符串 s 和 t 交错 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串: s …

Idea+maven+springboot项目搭建系列--1 整合Rocketmq

前言:本文以mavenspringboot 整合Rocketmq 完成消息的发送和接收。 1 Rocketmq 介绍: 1.1 Rocketmq 特性: Apache RocketMQ是一款快速、可靠的分布式消息传递和流处理平台,具有可扩展性和高性能。它是一个分布式的、去中心化的消…

docker 快速搭建elk

Docker搭建ELK步骤详解 文章目录 一.安装前须知二.安装 Docker三.Docker 安装 ElasticSearch四.Docker 安装 ElasticSearch-head(可选)五.Docker 安装 Kibana六.Docker 安装 LogStash七.创建springboot应用七.后记 一.安装前须知 以下步骤在 VMware 中…

清晰、明了的@Transcation事务嵌套使用

文章目录 概述Transcation注解事务实现原理 Transcation使用1、事务生效的情况:1. 外层有事务,内层无事务结论:外层有事务,内层也会有事务 2. 外层事务(requierd),内层事务(not_supp…

HEVC环路后处理核心介绍

介绍 为什么需要环路后处理技术 hevc采用基于快的混合编码框架,方块效应、振铃效应、颜色偏差、图像模糊等失真效应依旧存在,为了降低此类失真影响,需要进行环路滤波技术; 采用的技术 去方块滤波DF,为了降低块效应…

ADC和DAC的工作原理及其区别

ADC和DAC的工作原理及其区别 ADC和DAC都是用于模拟信号与数字信号之间的转换器。 ADC,即模数转换器,是将连续的模拟信号转换为数字信号的电路。其输入为模拟信号,输出为数字信号。ADC的主要组成部分是模拟信号采样模块、模拟信号处理模块、模…

生态系统服务(InVEST模型)土壤保持、水源涵养、氮磷输出、生态保护、生物多样性、碳固

白老师(研究员):长期从事生态系统结构-格局-过程-功能-服务的变化与响应关系等研究工作;重点围绕生物多样性、生态系统服务与价值等,构建生物地球化学模型和评价指标体系,为城市、区域和自然保护区的可持续…

sqlserver中动态sql语句应用

前言 一、使用exec 1.用拼接方法 二、使用sp_executesql 1.用拼接方法 2.传参的方法 总结 前言 例如:列表查询条件不固定,根据前端传过来的参数,这时需要根据查询条件后台动态生成SQL语句 一、使用exec exec适用于字符串拼接的方式&#xf…

mac安装python

接上集,我们已经安装了Homebrew 那么在 macOS 上安装 Python 有多种方法,以下是其中两种常用方法: 1:使用 Homebrew 安装 Python Homebrew 是 macOS 上的包管理器,可以方便地安装和管理各种软件包。如果您已经安装了…

美国E8267C是德(KEYSIGHT) E8267D 20G/44G矢量信号发生器

Agilent E8267C、Keysight E8267D、 PSG 矢量信号发生器,高达 44 GHz ​Keysight E8267D (Agilent) PSG 矢量信号发生器是业界首款 I/Q 调制高达 44 GHz 的集成微波矢量信号发生器。它具有先进的宽带内部基带发生器,能够灵活地播放任意波形或生成复杂的…

在Windows11上模拟运行Linux命令的几种方式

在 Windows 上运行 Linux 命令的软件有很多,以下是其中几个比较常用的: Cygwin Cygwin 是一个为 Windows 提供类 Unix 环境的开源软件,它包含了大量的 Unix 工具和命令,可以在 Windows 上运行 Linux 命令。 安装命令 winget i…

【Java 继承】了解Java类的继承的特点,继承的关系,继承的使用,到底什么是继承?

博主:_LJaXi Or 東方幻想郷 专栏: Java | 从入门到入坟 Java 继承 继承的特点 \ 介绍 ❓特点 ♊ 继承的使用方式 🔞避免重复方法 子类访问父类的成员变量 🈲子类访问父类的成员变量(直接访问)访问父类与子类…

MapReduce实战案例(3)

案例三: MR实战之TOPN(自定义GroupingComparator) 项目准备 需求测试数据 有如下订单数据 订单id商品id成交金额Order_0000001Pdt_01222.8Order_0000001Pdt_0525.8Order_0000002Pdt_03522.8Order_0000002Pdt_04122.4Order_0000002Pdt_05722.4Order_0000003Pdt_01222.8 现在…

6 具有 OCR 功能的顶级 PDF 图像转 Word 转换器

如果您在 PDF 图像中找到一些有用的信息并想转换为 Word 格式以供进一步使用,您将需要一个具有OCR 功能的 PDF 图像转 Word 转换器,该转换器旨在识别 PDF 图像中的文本并将其制作出来可编辑。 将 PDF 图像转换为 Word 并不容易,因为我们需要…

高压放大器工作原理(高压放大器怎么用的)

高压放大器是一种能够将低电平信号放大到足够高的电平,以便用于驱动大功率负载或处理高电压信号的电子设备。它广泛应用于各种电子设备中,例如音频放大器、射频放大器、电力电子设备等。下面我们将详细介绍高压放大器的工作原理以及使用方法。 高压放大器…

一分钟:GTP鼓谱导出转换MIDI格式教程

const loadPromise self.osmd.load("/resource/test");loadPromise.then(function () {self.osmd.render();});作为一名鼓手,我深知鼓谱转换MIDI格式的重要性,但是找了好久,一直没有找到一个好用的工具。 直到我发现了GTP鼓谱转换…

下载YouTube视频的一种方法

文章目录 工具名称下载方法使用方法1.只下载音频2.下载音频转换成mp3(加上-x –audio-format参数)3.下载视频(带音频)ID:22 | EXT:mp4 | 1280*720 下载的数据集:YouCook2 工具名称 yt-dlp 下载…

doxygen使用: 跨平台方式让markdown文件包含另一个文件

文章目录 1. 目的和问题2. 解决思路2.1 FILTER_PATTERNS 选项2.2 基于 Python 的 FILTER_PATTERNS 选项2.3 sledcpp.py 脚本 3. 完整工程3.1 目录结构3.2 hello.h 文件内容3.3 CHANGELOG.md 文件内容3.4 generate_doxyfile.py 文件内容3.5 docs/root.md3.6 docs/changelog.md3.…

Redis 事务详细介绍

事务 注意:Redis单条命令是保证原子性的;但是事务不保证原子性! Redis事务没有隔离级别的概念,所有的命令在事务中,并没有直接被执行,只有发起执行命令时才执行 Redis事务本质:一组命令的集合&…

API接口对接的流程和注意的事项

API接口对接是将两个应用程序或系统连接并进行数据交换的过程。在进行API接口对接时,需要确保两个系统具有相同的协议和格式,并且数据传输过程中不会出现错误或数据丢失。下面是API接口对接的流程和注意事项: 流程: 1.确认数据格…