网络数据包管理
标准TCP/IP协议栈中,每层都有独立的模块处理方法,负责完成一个独立的通信问题,他们都可以单独被实现,但是按这种分层模式实现的TCP/IP协议,会使数据包在各层间的数据递交变得非常慢,因为涉及到了一系列的内存拷贝问题,会影响总体效率。
LWIP为了解决上述问题,假设各层间的部分数据结构和实现原理在其它层可见,在数据包递交过程中,各层协议可以直接对数据包中属于其他层协议的字段进行操作,从而避免数据拷贝的开销。
数据包在各层间的拆包和分包过程:
1.1 数据包的描述
lwip使用数据结构pbuf来描述协议栈中使用的数据包。pbuf.h和pbuf.c实现了协议栈数据包管理相关的所有数据结构及函数。结构体pbuf定义如下:
struct pbuf { struct pbuf *next; //单链表的下一个pbuf节点 void *payload; //实际数据 u16_t tot_len; //buffer的总长度,即单链表中的所有实际数据长度之和 u16_t len; //此buffer的长度 u8_t /*pbuf_type*/ type; //8位长度表示类型 u8_t flags; //misc标志位 u16_t ref; //引用计数始终等于引用此pbuf的指针数。这可以是来自应用程序、堆栈本身的指针,也可以是来自链的pbuf->next指针 }; typedef enum { PBUF_RAM, /* pbuf data is stored in RAM */ //pbuf数据存储在RAM中 PBUF_ROM, /* pbuf data is stored in ROM */ //pbuf数据存储在ROM中 PBUF_REF, /* pbuf comes from the pbuf pool */ //pbuf来自pbuf池 PBUF_POOL /* pbuf payload refers to RAM */ //pbuf实际数据参考RAM } pbuf_type; |
pbuf有四种类型:PBUF_RAM、PBUF_POOL、PBUF_ROM与PBUF_REF:
(1)PBUF_RAM类型:通过内存堆分配得到,该类型pbuf在协议栈中最常用,协议栈的待发送数据和应用程序的待发送数据一般都采用这种形式的pbuf;
(2)PBUF_POOL类型:通过内存池分配得到,该类型pbuf可以在极短时间内得到分配(内存池的优点),在网卡接收数据包时常使用这种方式包装数据;
(3)PBUF_ROM类型:在内存池中分配一个pbuf结构但不申请数据区空间,payload指向ROM空间内的某段数据,在发送某些静态数据时常采用该类型pbuf;
(4)PBUF_REF类型:在内存池中分配一个pbuf结构但不申请数据区空间,payload指向RAM空间内的某段数据,在发送某些静态数据时常采用该类型pbuf。
1.PBUF_RAM
pbuf结构图如下:
payload指针指向该pbuf所记录的数据区域,但从上图可以看出payload并不是直接指向整个数据区的起始处,而是间隔了一定区域,这段间隔的数据区域offset用来存储数据包的各种首部字段,比如TCP报文首部、IP首部、以太网帧首部等。源码中申请PBUF_RAM类型的pbuf代码如下:
p = (struct pbuf*)mem_malloc(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF + offset) + LWIP_MEM_ALIGN_SIZE(length)); //调用内存堆分配函数,SIZEOF_STRUCT_PBUF为pbuf结构大小,offset为各首部字段大小,length为数据存储空间大小 |
2.PBUF_POOL
pbuf结构图如下:
pbuf通过next构成单向链表,只有第一个pbuf的payload有一个offset用于保存各首部字段。前面说PBUF_POOL得益于内存池分配的优点,分配速度很快,主要是由于内存池每个存储单位都是固定长度且使用前已经过初始化(这里我自己的理解是类似于操作系统中的消息队列),常用的内存池类型有两种:MEMP_PBUF与MEMP_PBUF_POOL,前者专门用来存放pbuf结构体,后者MEMP_PBUF_POOL的空间不仅包含了pbuf结构,还包含了LwIP认为协议栈中可能使用的最大TCP数据包空间(所有各层首部字段和 + 最大TCP数据段),默认长度为590字节(14 + 20 + 20 + 536),这个长度小于某些大的以太网数据包(比如大的ping包,长度可以达到MTU也即1500字节),此时可能需要多个MEMP_PBUF_POOL空间才能放得下这么大的数据包。源码中申请PBUF_POOL类型的pbuf代码如下:
p = memp_malloc(MEMP_PBUF_POOL); //调用内存池分配函数,分配MEMP_PBUF_POOL大小的空间 |
3.PBUF_ROM与PBUF_REF
pbuf结构图如下:
PBUF_ROM与PBUF_REF类型的pbuf基本相同,它们申请的都是内存池中一个MEMP_PBUF类型的POOL,而不申请数据区空间,二者的区别在于前者payload指向ROM空间内的某段数据,而后者指向RAM空间内的某段数据。源码中申请PBUF_ROM或PBUF_REF类型的pbuf代码如下:
p = memp_malloc(MEMP_PBUF); //调用内存池分配函数,分配MEMP_PBUF大小的空间 |
1.2 数据包的操作
1.数据包申请与释放函数
struct pbuf *pbuf_alloc(pbuf_layer l, u16_t length, pbuf_type type); //pbuf分配函数 u8_t pbuf_free(struct pbuf *p); //pbuf释放函数 |
分配函数的三个参数:(1)pbuf_layer指定该pbuf数据所处的协议层级,分配函数根据该值在pbuf数据区预留出首部空间offset;(2)length表示需要申请的数据区长度;(3)pbuf_type指出需要申请的pbuf类型。
pbuf_layer是一个枚举类型,在pbuf.h中定义如下:
#define ETH_PAD_SIZE 0 #define PBUF_LINK_HLEN (14 + ETH_PAD_SIZE) #define PBUF_TRANSPORT_HLEN 20 //TCP报文首部长度 #define PBUF_IP_HLEN 20 //IP数据报首部长度 typedef enum { PBUF_TRANSPORT, //传输层,预留PBUF_LINK_HLEN + PBUF_IP_HLEN + PBUF_TRANSPORT_HLEN PBUF_IP, //网络层,预留PBUF_LINK_HLEN + PBUF_IP_HLEN PBUF_LINK, //链路层,预留PBUF_LINK_HLEN PBUF_RAW //原始层,不预留任何空间 } pbuf_layer; |
数据包释放函数需要注意pbuf结构有一个成员变量ref,当pbuf被创建时ref初始值为1,代表被引用一次,每增加一次对该pbuf的引用则ref相应增加1;相应的每释放一次pbuf,则其ref值减1,只有当pbuf的ref值减为0时,该pbuf才被删除。
2.其他比较重要的数据包操作函数
如下表:
操作函数 | 功能描述 |
void pbuf_realloc(struct pbuf *p, u16_t size) | 在相应pbuf链表尾部释放一定的空间,将数据包pbuf中的数据长度减少为size; |
u8_t pbuf_header(struct pbuf *p, s16_t header_size) | 调整pbuf的payload指针(向前或向后移动header_size字节),使payload指针指向数据区前特定协议层的首部字段,为各层对数据包首部字段的操作提供方便; |
void pbuf_ref(struct pbuf *p) | 增加该pbuf的引用计数 |
u8_t pbuf_clen(struct pbuf *p) | 获取该数据包链表中pbuf的个数 |
void pbuf_cat(struct pbuf *head, struct pbuf *tail) | 将tai指向的pbuf连接到head指向的pbuf后面,但不增加tail所指向pbuf的引用计数; |
void pbuf_chain(struct pbuf *head, struct pbuf *tail) | 将tai指向的pbuf链接到head指向的pbuf后面,并增加tail所指向pbuf的引用计数; |
struct pbuf *pbuf_dechain(struct pbuf *p) | 从pbuf链表中解除链表首节点的链接,返回剩余链表首节点或NULL; |
err_t pbuf_copy(struct pbuf *p_to, struct pbuf *p_from) | 将一个任何类型的pbuf_from中的数据拷贝到一个PBUF_RAM类型的pbuf_to中; |
err_t pbuf_take(struct pbuf *buf, const void *dataptr, u16_t len) | 将dataptr开始长度为len的数据拷贝到pbuf的数据区域 |
更多内容详见下一节:LWIP协议栈解析(三)——内存管理