LwIP 第一周

Lwip学习笔记

本文档是学习《嵌入式网络那些事》的学习笔记。

本书是以STM32为平台,以LwIP源代码为基础,讲解TCP/IP协议。

目录

  1. 实验平台
  2. LwIP源码简介和结构
  3. 初步移植
  4. 内存管理
  5. 网络数据包
  6. 网络接口管理
  7. ARP协议
  8. 网际协议(IP)
  9. ICMP协议
  10. Raw API >> UDP
  11. Raw API >> TCP
  12. 操作系统模拟层
  13. Sequential API
  14. Socket 编程
  15. LwIP实战
  16. 高级篇
  17. 配置与调试

2022/3/29

第一章

嵌入式常用的TCP/IP协议栈。

1、uIP

8位/16位的非常小的协议栈。去掉了不常用的功能,完全用C编写,只需要几kB ROM和几百字节RAM中运行。但是在一些较高要求的场合,如可靠性和大数据量传输的场合则不适用。

2、uC/IP

基于uC/OS操作系统开放源码的TCP/IP协议栈。完全免费可供研究的TCP/IP协议栈。协议栈需要的代码容量空间在30kB到60kB之间。

没有LwIP那样提供多种上层应用,文档支持和软件升级管理上有很多不足。

3、uC/TCP-IP

功能较齐全,但代码量较大,主要用在32/64位处理器上。是一款收费软件

4、Linux

linux系统中有完整的TCP/IP协议的实现。但是可执行代码往往有数兆之大。需要高效的处理器熟读和大量的外存、内存开销。

5、LwIP

轻型IP协议。最大优势可以移植到操作系统上,也可以在无操作系统的情况下独立运行,且代码量小。是目前在嵌入式网络领域被讨论和使用很广泛的一个协议栈。开源特性和快速的版本更新效率。新版本还支持DNS、SNMP、DHCP、IGMP等高级应用功能。在资源有限的情况下实现TCP协议的主要功能。有自己的数据包和内存管理机制。

第二章

下载地址:http://download.savannah.gnu.org/releases/lwip/

Lwip的主要协议有:ARP、IP、ICMP、IGMP、UDP、TCP、DNS、SNMP、DHCP、AUTOIP、PPP。2.0之后的版本还有MQTT、HTTPD、TFTP、SNTP、SNMP。

但是这个书是根据1.4.1来讲解的、内核没有什么区别,主要是多了一些应用层协议

源代码结构

大致结构如下:

  1. doc 协议栈使用相关的文本文档。有以下两个重要文件
    1. rawapi.txt怎么是怎么使用Raw/Callback API编程
    2. sys_arch.txt移植使用,包含了一直说明,需要实现的函数和宏定义等
  2. src
    1. api: Sequential API 和Socket API两类接口和实现相关的源代码
    2. core:是LwIP内核源代码,可单独运行,不需要操作系统的支持
    3. include:整个协议栈使用的头文件
    4. netif:与底层接口相关的文件
  3. test 内核测试程序

具体结构:

  1. netif

    1. ppp 包含了PPP协议实现的源代码。即点对点协议。LwIP提供了对PPPoE的支持
    2. etharp.c 包含了arp协议实现的相关函数。主要用来实现主机以太网物理地址到IP地址的映射。以太网底层的数据是基于网卡物理地址的,而不是主机的IP地址
    3. ethernetif.c 包含了与以太网网卡密切相关的初始化、发送、接收等函数的实现。其中的函数不能使用,是一个框架性的结构,需要移植具体的网卡
    4. slipif.c SLIP串行链路IP。串行链路上传送IP数据包。需要根据自己使用的串行线路特性来实现函数
  2. core

    1. 包含了核心协议与上层应用协议。
      1. ipv4 包括了IPv4标准中的相关代码。
      2. ipv6包括了IPv6标准中的相关代码。文件夹文件结构和ipv4相似
      3. snmp文件夹包含了SNMP协议实现相关的所有代码,SNMP为简单网络管理协议。LwIP中使用udp进行实现。
      4. def.c IP层使用到的一些功能函数的定义。
      5. dhcp.c 实现了DHCP客户端的所有代码
      6. dns.c dns客户端的代码。基于UDP传输数据
      7. init.c 初始化密切相关的函数,以及一些协议栈配置信息的检查与输出。
      8. mem.c 协议栈内存堆管理函数的实现。memp.c 协议栈内存池管理函数的实现。
      9. netif.c 包含了协议栈网络接口管理的相关函数,通过次文件中的函数进行统一管理。
      10. pbuf.c 包含了协议栈内核使用的数据包管理函数。
      11. raw.c 为应用层提供了移植直接和IP数据包交互的方式。类似于原始套接字,与TCP/UDP处于同一等级,可以直接读取IP层接受到的数据包。也可以自行构造ping包等。
      12. stats.c 内部数据统计与显示的相关函数,如内存使用状况、邮箱、信号量。
      13. tcp.c 包含了对TCP控制块操作的函数,也包括了TCP定时处理函数,tcp_in.c 包含了TCP协议中的数据接收、处理相关的函数还有TCP状态机的函数。tcp_out.c 包含了TCP发送相关的函数,如数据包发送、超时重传。
      14. udp.c UDP协议相关函数,包括UDP控制块管理、数据包发送与接受
      15. sys.c 简单实现了睡眠一定时间的功能,主要在PPP中使用。移植需要提供一个sys_arch.c 的文件才有效,sys_arch.c 需要封装一些操作系统中的函数。无操作系统不会编译
      16. timers.c 无操作系统时,需要移植sys_now 函数获取当前时间。判断事件是否超时。有操作系统时,实现了等待函数的再次封装、此封装可以在等待消息的同时,可以实现协议栈中各个定时事件的正确处理。
  3. ipv4文件夹

    1. autoip.c IP地址自动配置的相关函数
    2. icmp.c ICMP协议实现的相关函数。该协议为IP数据包传递过程中的差错报告、纠正以及目的地址可达性的支持。
    3. igmp.c 网络组管理IGMP协议的实现。为网络中的多播数据传输提供支持。
    4. inet.c 空。inet_chksum.c 实现了同IP数据包校验相关的函数。ip.c 包含了IPv4协议实现的相关函数。如数据包的接受、递交、发送等。ip_addr.c 实现了比较简单的IP地址处理函数。比如是否为广播地址以及点分十进制的转换。ip_frag.c 实现了数据包分片和重组相关的函数。

api 文件夹

api_lib.c 和api_msg.c包含了Sequential API函数的实现。前者主要包含预留给用户的接口。后者主要包含API信息的封装与处理。netbuf.c 包含了上层数据包管理函数的实现.netdb.c 与主机名字转换相关的函数。主要在socket中使用。netifapi.c 包含了上传网络接口管理函数的实现。socket.c 包含了Socket API函数的实现。tcpip.c 提供了上层API与协议栈内核交互的函数。整个上层API功能的接口

Include 文件夹

上述所有c文件对应的头文件声明。opt.h 包含了所有LwIP内核参数的默认配置值。init.h 与当前LwIP源代码相关的宏定义。

2022/3/20

分层思想

TCP/IP协议完整地包含了一些列构成互联网基础的网络协议。在OSI标准之前,并没有按照OSI模型中描述的各种结构来实现,有着自己的协议层次划分特点。TCP/IP协议模型可以分为四个层次。

  1. 网络接口层
  2. 网络层
  3. 传输层
  4. 应用层

低一层为高一层提供服务。最终完成数据在两台主机间的传递。

网络接口层

网络接口层是TCP/IP协议模型的最底层,主要负责网络上数据帧的发送和接受。数据帧是底层网络传输的基本单元。网络接口有不同的实现方式。如有线、无线的方式发送数据帧,不同的实现方式意味着不同的帧结构、发送速率等。网络接口层一方面将上层(网络层)的数据组装成自己特定的数据帧进行发送,另一方面接收网络中发给自己的数据帧,并解析。

网络层

网络层负责主机之间的通讯中选择数据报的传输路径,即路由。当网络层接收到来自与上层(传输层)的数据分组后,会封装在IP数据报中,填入数据报的首部。使用路由算法来确定是直接交付还是传递给路由器,然后将数据交给适当的网络接口进行传输。

还有负责处理传入的数据报,并检验其数据有效性,然后判断该数据报是否是给本机,如果不是,则通过路由算法将数据报出去转发,如果是,网络层需要除去数据数据包中的首部得到数据分组,然后将数据分组递交给传输层

传输层

传输层主要提供应用程序之间的通讯服务,这种通讯又称为端对端通信。传输层协议把上层(应用层)要传输的数据流划分为分组,把每个分组连同目的地址交给网络层去发送。传输层要系统的管理两端数据的准确交互,要提供可靠的传输服务,以确保数据到达无差错、无乱序。为了达到这个目的,传输层协议可以采用协商、确认、重发等机制。

应用层

应用层是分层模型的最高层,他最简单的解释就是利用传输层提供了数据传输功能发送自己的数据到对方。传输层协议类型有多种,不同的类型意味着不同的传输速度和可靠性,而往往这二者是不可兼得的。所以每个应用程序选择最合适的传输服务类型。以达到最佳效果。

应用层(DHCP、DNS、HTTP、SNMP、API、BSD Socket)
传输层(TCP、UDP)
网络互联层(IPv4,IPv6、ICMP、IGMP、ARP)
网络接口层(PPP、SLIP、以太网、环回接口)

各个层都被描述为一个独立的模块形式,每一层负责完成一个独立的通讯问题。

IP地址只能唯一地标识出一台主机、端口号标识出一台主机上的不同进程。传输层通过识别端口号来向不同的用户应用程序递交数据。

每个层次的协议相互独立,他们都可以被单独实现,只要保证它们之间的接口不变就可以了。

如果按照这种严格的分层模式实现TCP/IP协议,会使数据包在各层间的递交变得非常慢,涉及到一系列的内存拷贝问题,因此,系统的总体性能也会受到影响。为了避免这种现象。LwIP内部并未采用完整的分层结构,它会假设各层间的部分数据结构和实现原理在其他层是可见的。在数据包的递交过程中、各层协议可以直接对数据包中属于其他层次协议的字段进行操作,更加灵活

LwIP实现时,参考了TCP/IP协议的分层思想,即每层都在一个单独的模块中实现,并为其他层提供一些输入、输出函数。每个模块都在一个单独的文件中实现。

LwIP将协议栈内核和操作系统内核互相隔离,而同时整个协议栈作为操作系统中的一个单独的进程存在。

协议栈编程接口

Raw/Callback API

该方式编程时,协议栈与用户程序之间通讯是通过回调函数实现的,此时用户程序和协议栈内核运行于同一进程中,应用程序通过函数注册的方式与内核产生联系,在内核相关事件发生时,用户函数通过回调的方式被执行。该方式可以方便的构造出一个服务器对多个客户端的TCP并发连接。

但是该方式出现了相互制约的关系,用户在执行程序时,内核一直处于等待 状态。

Sequential API

出发点是上层已经预知了协议栈内核的部分结构、API可以使用这种预知避免数据拷贝的出现,用户进程可以直接操作内核进行中的数据包数据。

Socket API

该接口简单、易理解。但是负载大,会将pbuf中的所有数据拷贝之后再递交给用户。

使用LwIP最佳的方式是运用Raw/Callback API和Sequential API进行灵活编程,让这两者优势互补。LwIP的设计者对Sequential API进行了简单的封装,这样就得到了套接字函数,但只是简单的模拟,有部分函数并未实现,并不完整。

第三章

整个移植过程中,重点和难点都在网卡驱动的移植上。

可分为两大类:

  1. 只移植内核核心,只能基于Raw/Callback API进行。
  2. 移植内核核心和上层API函数模块,可使用三种API进行编程。

第一类:

只需要完成几个头文件的定义,同时根据使用的具体网卡情况完全跟ethernetif.c 中函数的编写

第二类:

除了实现第一种移植中的所有文件和函数外,还必须使用操作系统提供的邮箱和信号量机制,完成系统模拟层文件sys_arch.c 和sys_arch.h 编写。必须要在操作系统上进行。

本章主要是第一类

主要包括网卡初始化函数、网卡数据发送函数、网卡数据接收 函数。只需要将这个三个函数进行封装就可以了。

文件移植与头文件

文件

文件core文件夹中的所有文件和ipv4文件夹的所有文件

头文件

  1. 新建cc.h头文件对协议栈内部使用的数据类型进行定义。还有协议栈调试信息输出相关宏、大小端定义等。以及获取系统时间的sys_now函数,单位为ms
  2. 新建perf.h 是与系统统计和测量相关的头文件,和处理器密切相关、暂时为空。
  3. 新建lwipopts.h 围殴文件,包含对协议栈内核的参数配置。opt.h为默认配置,可通过此头文件进行修改默认配置

网卡驱动

操作相关函数:

void Init(unsigned char *macaddr);
void Send(unsigned int len,unsigned char *packet);
unsigned int Receive(unsigned int maxlen,unsigned char * packet);
//packet 需要用户事先开辟好空间

ethernetif.c 的五个函数框架

//网卡初始化,主要完成网卡复位及参数初始化,还需要设置netif中与网卡属性相关的函数,如MAC地址长度
static void low_level_init(struct netif *netif);
//网卡发送函数,该函数将pbuf中描述的数据包发送出去
static err_t lowe_level_output(struct netif *netif, struct pbuf *p);
//网卡接收函数,必须将数据封装为pbuf的形式
static struct pbuf *low_level_input(struct netif *netif);
//调用low_level_input读取一个数据包,并进行解析数据包类型(ARP或者IP)然后将数据包递交给上层,已经是可以直接使用的函数。调用一次便可以完成一个数据包的接收和递交
static void ethernetif_input(struct netif *netif);
//上层在管理网络接口结构netif会调用的函数,主要完成netif结构中的字段初始化,并最终调用low_level_init进行网卡初始化,可以不进行改写
err_t ethernetif_init(struct netif *netif);

2022/3/31

low_level_init()

将设备的mac地址进行赋值,并初始化网口即可。还有网络接口的属性字段。

网络接口netif是协议栈内核对系统网络接口设备进行管理的重要数据结构,内核会为每个网络接口分配一个netif结构,以描述对应接口的属性

low_level_output()

整个移植过程中真正的难点,要求移植者对网卡操作及协议栈数据包结构pbuf有详细的了解。内核上层需要发送的数据包都组织在pbuf中,数据包可能很大,需要多个pbuf才能完全封装。这些pbuf组成一个链表。使用网卡发送数据时,需要发送链表上所有的pbuf数据,但需要注意的是同一个数据包中的所有数据必须放到同一个以太网帧中发送。

low_level_input()

从网卡中读数据,并将数据组装在pbuf中结构中供内核使用

ethernetif_input()

该函数完成数据包接收的总调度,包括调用前面的网卡接收函数接收数据包,同时将数据包递交给内核处理。

ethernetif_init()

这是一个可以直接使用的函数。

安装系统时钟

1.4.1版本引入了函数sys_check_timeouts来处理内核的各种定时事件。需要实现sys_now()函数

协议栈初始化与运行

使用协议栈前,协议栈内核必须先初始化完毕,然后注册网络接口,再进行使能。

查询

主函数中进行循环调用数据包接收和处理函数。还需要周期性的调用协议栈内核的定时处理函数,来满足内核需求。

还需要设置一下头文件路径。

这样就可以正常运行了,可通过ping命令进行测试。

中断

注册中断回调函数,在函数中进行调用接收和处理函数。

Ping问题

ping是协议栈移植之后最基本最重要的测试。可测试保证能否正常工作,是上层协议及应用程序能够正常运行的基础。

提供一些解决思路:

  1. 首先需要排除硬件是否有问题。
  2. 可通过指示灯判断有没有发生跑飞和内存践踏现象。
  3. 可先采用查询的方式,来避免配置中断出现问题。
  4. 保证协议栈ARP正常。输出一些与主机相关的IP和MAC信息
  5. ping通。IP层最常见的一个问题是包校验和失败。可以打开IP层的串口输出信息进行观察。
  6. 观察ICMP协议的日志输出。如果输出正常,则可能是发送通路的问题。

第四章

LwIP为用户提供了两种最基本的内存管理机制:动态内存池管理和动态内存堆管理。

常见内存分配策略

  1. 系统在规定用户申请内存时,申请大小必须指定为某几个固定值,否则不予分配。

由于为固定长度,所以不可避免的产生内存浪费的现象。该方式为动态内存池分欸,这种方式可以用来对某种固定的数据结构进行空间的分配(TCP首部、IP首部),该方式的最大特点是快,实现简单,不用整理内存碎片。

  1. 与第一种较为相似。可能会存在多种类型。把用户的内存空间划分为几个范围,用以满足系统某个固定大小,分配速度很高,但是易产生空间浪费。但有一种存储紧缩操作可以进行优化。LwIP中这种内存方式是可选的
  2. 各个空闲块的大小是随着系统运行而改变的。可当成一个大数组,直接寻找能存下的位置
    1. 首次拟合,最常用的方法。LwIP实现为内存堆方式
    2. 最佳拟合,与长度最接近的空闲块进行分配,需要遍历链表,还需要进行整理
    3. 最差拟合,从最大的空闲块中划分出n个字节分配给用户。需要遍历链表,但是长度又大到小,只需要使用链表的第一个元素

三种拟合各有优缺点,最佳拟合适用于用户请求大小范围较广的系统,该方式可以找到最接近请求大小的空闲块,但是易产生一些小的无法使用的内存篇。最差拟合每次选择最大的空闲块,会使空闲链表中的各空闲块大小趋近均匀。首次拟合是随机的。

在时间上,首次拟合分配O(n),释放O(1)。最差拟合分配O(1),释放O(n)。最佳拟合,均为O(n)

动态内存池

实现简单,分配、释放效率高。可以有效防止内存碎片产生,但只能申请固定大小,主要用于固定结构体的分配,内核在初始化的时候已经为每个数据结构类型都初始化了一定数量的POOL。在对应功能使能时,对应的内存池会被建立。

把协议栈中所有的POOL放在一起,组成一片连续的区域,呈现给用户的就是一个大的缓冲池。

内存池各种类型的定义之前已经学习过,这里不在展开记笔记。(主要是##的使用与include头文件的使用)

最后一个memp_memory的定义倒未了解过。

static u8_t memp_memory[MEM_ALIGNMENT - 1 
#define LWIP_MEMPOOL(name,num,size,desc) + ( (num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size) ) )
#include "lwip/memp_std.h"
];

这里申请的数组空间大小为所有的小内存池组成的大空间,用MEM_ALIGNMENT进行内存对齐,用宏定义将小的内存块进行相乘再相加,保证申请足够的内存又不存在浪费。

struct memp {
  struct memp *next;
#if MEMP_OVERFLOW_CHECK
  const char *file;
  int line;
#endif /* MEMP_OVERFLOW_CHECK */
};

static struct memp *memp_tab[MEMP_MAX];

这个memp_tab是一个数组,数组中存放的是首地址,在LwIP初始化时,会进行memp赋值,指向另一个内存池的首地址。在每次malloc之后,对应位置的指针就会向下一个偏移。

动态内存堆

动态内存堆的分配策略的本质就是对一个事先定义好的内存块进行合理有效的组织和管理。

书里着重讲的是首次拟合。只要找到一个比用户请求空间大的空闲块、就从中切割出合适的块,并把剩余的部分返回到动态内存堆中。

首次拟合数据结构

这种策略下用户申请的内存块大小具有最小限制,即请求的大小不能小于MIN_SIZE,否则 系统自动将请求大小设置为MIN_SIZE。通常被定义为12字节,改变该值在分配时可以节省空间,但是会导致大的内存块被不断细分小的内存块。内存释放的过程是相反的过程,内存回收函数会查看该节点前后相邻的内存块是否空闲,如果空闲则被合并成一个大的内存空闲块。内存浪费小,比较简单,但是会频繁的动态分配和释放。可能会造成严重的内存碎片,如果碎片严重的情况下,可能导致内存分配不成功。

u8_t ram_heap[MEM_SIZE_ALIGNED + (2*SIZEOF_STRUCT_MEM) + MEM_ALIGNMENT];
/** pointer to the heap (ram_heap): for alignment, ram is now a pointer instead of an array */
//首地址
static u8_t *ram;
/** the last entry, always unused! */
//指向内存堆最后一个内存块
static struct mem *ram_end;
/** pointer to the lowest free block, this is used for faster search */
//指向地址最低的可用内存块
static struct mem *lfree;
/** concurrent access protection */
static sys_mutex_t mem_mutex;

//内存块前的数据,用于记录当前内存块
struct mem {
  /** index (-> ram[next]) of the next struct */
  mem_size_t next;
  /** index (-> ram[prev]) of the previous struct */
  mem_size_t prev;
  /** 1: this area is used; 0: this area is unused */
  u8_t used;
};

简单来说,就是先开辟一个ram_heap这么大一个数组。第一次申请时,比如4个字节,会将mem这个结构体贴在头部,然后标记4字节已使用。会将原本的一个大内存块分为了一个4字节的内存块和剩下那么多大小的内存块,多次malloc和free之后,就会如下图。

img

内存块的位置和大小都不是固定的,在malloc时会进行分割在free时会进行整理,把相邻未使用的装成一个大内存块。

  • mem_init

进行初始化,必须最开始调用

  • mem_malloc

LwIP内存堆申请函数

  • mem_free

LwIP内存堆释放函数

在free函数之后,会调用plug_holes进行未使用的内存块合并。

内存泄漏

在PC中常会听到内存泄漏这个概念。应用程序在调用函数堆分配函数malloc之后,在使用完成,没有及时的将内存释放或者进行了错误的释放。一两次很难觉察,且不会带来很大的影响。但是由于malloc一直在使用,就会一步步用完系统的所有内存资源,最终出现无法申请内存,出现异常。

其他内存分配策略

  • MEM_LIBC_MALLOC

    如果此值为1,则会使用stdlib库中的函数,mem_malloc等函数会进行重定向为库的函数。

  • MEMP_MEM_MALLOC

    如果此值为1,则使用内存堆的方式来实现内存池分配。

  • MEM_USE_POOLS

    如果该值为1,则使用内存池的方式来实现内存堆分配。此种需要自己定义内存池的一些宏与头文件,大小与数量等。

网络数据包

TCP/IP本质时一种数据通讯机制,因此,协议栈的实现本质上就是堆数据包进行处理。本章书中讨论围绕pbuf展开。

LwIP的分层特点

在标准TCP/IP协议结构中,各个层都被描述为一个独立的模块形式,每一层负责完成一个独立的通讯问题。

LwIP在实现时,参考了TCP/IP协议的分层思想,每层在一个单独的模块中实现,并为其他层次模块提供一些输入/输出接口函数。虽然将所有层级用不同的文件进行分开,但并没有完全的分层,为了节省事件和空间上的开销,各个层次可能存在交叉存取现象。不与Linux等操作系统相同,不同层都需要进行内存拷贝和读取。如TCP不是调用IP层的相关接口函数,而是直接访问数据包中的IP数据报首部。

虽然内核各层存在一定的交错现象,但在用于程序与协议栈内核有着明显的分层结构。LwIP假设应用程序了解协议栈内部的数据处理机制。用户可以直接访问协议栈内部的数据包。

协议进程模型

可以理解为协议实现时被划分在几个进程中,即实现协议需要几个进程。

  • 单进程模型

协议中的每个模块都独立的成为一个进程,这种模式下,各个模块之间有着明显的界限区分,各个接口定义也非常清晰。可使协议的代码更加灵活。但是会设计到频繁的进程切换。底层网卡接收到的数据至少经过3次进程切换。协议栈效率较低

  • 将协议栈驻留在操作系统

没有明显的分层切换,可以采用一定的交叉编程技术提高协议栈的运行效率

  • LwIP的模型

协议栈内核同操作系统内核相互隔离,而同时整个协议栈作为操作系统的一个单独的进程而存在。用户应用程序可以驻留在协议栈内核的进程中,也可以时限为一个单独的进程。第一种时通过Ram/Callback API 方式实现。第二种方式时,需要使用其他两种API进行编程。但这种方式需要将LwIP进程设置为最高优先级,否则协议栈响应 的实时性会受操作系统调度的影响。

数据包管理原理

数据包的种类和大小可以说是五花八门:首先是王凯上接收的原始数据包,它可以包含TCP报文的长达数百字节的数据包,也可以时仅有几十字节的ARP数据包。LwIP极力避免数据的拷贝工作,需要有一个高效的数据包管理核心。

pbuf结构

数据包管理机构采用数据结构pbuf描述协议栈中的数据包,文件pbuf.h 和pbuf.c 实现了协议栈数据包管理相关的所有数据结构和函数。定义如下

struct pbuf {
  /** next pbuf in singly linked pbuf chain */
  //下一个pbuf结构
  struct pbuf *next;

  /** pointer to the actual data in the buffer */
  //数据指针,该pbuf节点所记录的数据区域
  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)
   */
  //当前pbuf及其后所有pbuf有效数据的总长度
  u16_t tot_len;

  /** length of this buffer */
  //当前节点的数据长度
  u16_t len;

  /** pbuf_type as u8_t instead of enum to save space */
  // pbuf 的类型
  u8_t /*pbuf_type*/ type;

  /** 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.
   */
  //该节点被引用的次数
  u16_t ref;
};
  • next

    下一个pbuf结构,因为实际发送和接收的数据包可能很大,但每个pbuf能够管理的数据可能有限。因此可能存在多个pbuf才能完全描述一个数据包。用链表进行连接

  • payload是数据指针,指向pbuf管理的数据起始地址,该pbuf的可以在RAM也可以在ROM,依据type进行决定。

  • len 当前pbuf的有效长度,tot_len是当前pbuf及其后所有pbuf有效数据的总长度。因此链表第一个节点是数据包的总长度,最后一个节点tot_len与len字段相等。

  • pbuf的类型,具体共有四种类型。最难理解的部分,下一小节展开说

  • flags字段设计之初的目的在于指出当前pbuf的一些特殊属性。如状态、创建者等。通常设置为0.

  • ref表示pbuf被引用的次数,表示有其他指针指向当前buf,可能是其他pbuf的next指针,也可以是其他任何形式的指针,初始化时,被设置为1,当有其他指针需要指向该pbuf时,必须调用相关函数将其增加

pbuf的类型

有四类

typedef enum {
  PBUF_RAM, /* pbuf data is stored in RAM */
  PBUF_ROM, /* pbuf data is stored in ROM */
  PBUF_REF, /* pbuf comes from the pbuf pool */
  PBUF_POOL /* pbuf payload refers to RAM */
} pbuf_type;

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!