目录
1. TCP/IP 协议分层
2. lwIP的线程模型
3. pbuf 结构体说明
4. pbuf 的类型
4.1 PBUF_RAM类型
4.2 PBUF_POOL 类型
4.3 PBUF_ROM和PBUF_REF 类型
5. pbuf_alloc()
6. pbuf_free()
7. 其它pbuf 操作函数
7.1 pbuf_realloc()
7.2 pbuf_header()
7.3 pbuf_take()
8. 网卡中使用的pbuf
TCP/IP 是一种数据通信机制,因此,协议栈的实现本质上就是对数据包进行处理,为了实现高效的效率,LwIP 数据包管理要提供一种高效处理的机制。协议栈各层能对数据包进行灵活的处理,同时减少数据在各层间传递时的时间与空间开销,这是提高协议栈工作效率的关键点。在BSD 的实现中,一个描述数据包的结构体叫做mbuf,同样的 在 LwIP中,也有个类似的结构,称之为 pbuf,本章所有知识点将围绕 pbuf 而展开。
在标准的TCP/IP 协议栈中,各层之间都是一个独立的模块,它有着很清晰的层次结构,每一层只负责完成该层的处理,不会越界到其他层次去读写数据。
LwIP 只是一个轻量级TCP/IP 协议栈,它只是一个较完整的TCP/IP 协议,多应用在嵌入式领域中,由于处理器的性能有限,LwIP 并没有采用很明确的分层结构,它假设各层之间的部分数据和结构体和实现原理在其他层是可见的,简单来说就传输层知道IP 层是如何封装数据、传递数据的,IP 层知道链路层是怎么封装数据的等等。
为什么要模糊分层的处理?简单来说就是为了提高效率,例如链路层完成数据包在物理线路上传输的封装;IP 层完成数据包的选择和路由,负责将数据包发送到目标主机;传输层负责根据IP 地址将数据包传输到指定主机,端口号识别一台主机的线程,向不同的应用层递交数据;但是,如果按照标准的TCP/IP 协议栈这种严格的分层思想,在数据传输的时候就需要层层拷贝,因为各层之间的内存都不是共用的,在链路层递交到IP 层需要拷贝,在IP 层递交到传输层需要拷贝,反之亦然,这样子每当收到或者发送一个数据的时候都要CPU 去拷贝数据,这个效率就太慢了。
所以LwIP 假设各层之间的资源都是共用的,各层之间的实现方式也是已知的,那么在IP 层往传输层递交数据的时候,链路层往IP 层递交数据的时候就无需再次拷贝,直接操作协议栈中属于其他层次的字段,得到相应的信息,然后直接读取传递的数据即可,这样处理的方式就无需拷贝,各个层次之间存在交叉存取数据的现象,既节省系统的空间也节省处理的时间,而且更加灵活。
在小型嵌入式设备中,LwIP 与用户程序之间通常没有太严格的分层结构,这种方式允许用户处理数据与内核之间变得更加宽松。LwIP 假设用户完全了解协议栈内部的数据处理机制,用户程序可以直接访问协议栈内部各层的数据包,可以让协议栈与用户使用同样的内存区域,允许用户直接对这片区域进行读写操作,这样子就很好地避免了拷贝的现象,当然这样子的做法也有缺陷,取决于用户对协议栈处理过程的了解程度,因为数据是公共的,如果处理不正确那就让协议栈也没法正常工作。
当然,除了标准的TCP/IP 协议,还存在很多其他的TCP/IP 协议,即使这些协议栈内部存在着模糊分层、交叉存取现象,但是对协议栈外部的应用层则保持着明显的分层结构,在操作系统中,TCP/IP 协议栈往往被设计为内核代码的一部分,用户可以的函数仅仅是协议栈为用户提供的那些,或者直接完全封装起来,用户的操作类似于读写文件的方式进行(如BSD Socket),这样子用户就无法避免数据的拷贝,在数据发送的时候,用户数据必须从用户区域拷贝到协议栈内部,在数据接收的时候,协议栈内部数据也将被拷贝到用户区域。
简而言之,对于资源有限的嵌入式开发,平衡软件设计层次之间的耦合度和资源消耗。
线程模型可以理解为协议栈的实现被划分在多个线程之中,如让协议栈的各个层次都独立成为一个线程,在这种模式下,各个层次都有严格分层结构,各个层次的提供的API接口也是分层清晰的,这样固然使得编程更加简便、高效、灵活。但对于嵌入式设备,严格的分层,势必引起更多数据的传输、拷贝及线程切换,这是很大的开销。如从链路层到应用层,数据要经历数次的拷贝和线程切换,这样使得协议栈的效率低下。
其次,协议栈与操作系统融合,成为操作系统的一部分,这样子用户线程与协议栈内核之间都是通过操作系统提供的函数来实现的,这种情况让协议栈各层之间与用户线程就没有很严格的分层结构,各层之间能交叉存取,从而提高效率。
LwIP 采用了另一种方式,让协议栈内核与操作系统相互隔离,协议栈仅仅作为操作系统的一个独立线程存在,用户程序能驻留在协议栈内部,协议栈通过回调函数实现用户与协议栈之间的数据交互;也可让用户程序单独实现一个线程,与协议栈使用系统的信号量和邮箱等 IPC 通信机制联系起来,进行数据的交互。
当使用第一种通过回调函数进行交互情况的时候,也就是我们所说的RAW API 编程。当通过操作系统IPC通信机制的时候,就是另外两种API 编程,即NETCONN API 和Socket API。当然这样子既有优点也有缺点,优点就是能在任何的操作系统中移植,缺点就是受到操作系统的影响。
因为即使LwIP 作为一个独立的线程,也是需要借助操作系统进行调度的,因此,协议栈的响应的实时性会有一定影响,并且建议设置LwIP 线程的优先级为最高优先级。
pbuf 就是一个描述协议栈中数据包的数据结构,LwIP 中在pbuf.c 和pubf.h 实现了协议栈数据包管理的所有函数与数据结构。
/** 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;
};
next 是一个pbuf 类型的指针,指向下一个pbuf,因为网络中的数据包可能很大,单个数据包被分割成几个pbuf来记录数据,并以单链表的形式连接起来,被称为pbuf 链表。
payload 是一个指向数据区域的指针,指向该pbuf 管理的数据区域起始地址,这里的数据区域可以是紧跟在pbuf 结构体地址后面的RAM空间,也可以是ROM中的某个地址上,取决于pbuf 的类型。
tot_len 中记录的是当前pbuf 及其后续pbuf 所有数据的长度,例如如果当前pbuf 是pbuf 链表上第一个数据结构,那么tot_len 就记录着整个pbuf 链表中所有pbuf 中数据的长度;如果当前pbuf 是链表上最后一个数据结构,那就记录着当前pbuf 的
长度。
len 表示当前pbuf 中有效的数据长度
type_internal 表示pbuf 的类型,LwIP 中有4 种pbuf 的类型,并且使用了一个枚举类型的数据结构定义,见4.
flags 字段在初始化的时候一般被初始化为0
ref 表示该pbuf 被引用的次数, 引用计数总是等于引用此pbuf的指针数。 这可以是来自应用程序,堆栈本身或pbuf->链中的下一个指针的指针。初始化一个pbuf 的时候,ref 会被设置为1,因为该pbuf 的地址一点会被返回一个指针变量,当有其他指针指向pbuf 的时候,就必须调用相关函数将ref 字段加1。
if_idx 用于记录传入的数据包中输入netif 的索引,也就是netif 中num 字段。
pbuf 的类型有4 种,分别为PBUF_RAM、PBUF_POOL 、PBUF_ROM、PBUF_REF
/**
* @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_RAM类型的pbuf 空间是通过内存堆分配而来的,一般协议栈中要发送的数据都是采用这种形式,这种类型的pbuf 在协议栈中使用得最多,在申请这种pbuf 内存块的时候,协议栈会在管理的内存堆中根据需要的大小进行分配对应的内存空间,这种pbuf 内存块包含数据空间以及pbuf 数据结构区域,在连续的RAM内存空间中。
内核申请这类型的pbuf 时,也算上了协议首部的空间,当然是根据协议栈不同层次需要的首部进行申请,LwIP 也使用一个枚举类型对不同的协议栈分层需要的首部大小进行定义。
PBUF_RAM类型的pbuf 示意图具体见下图 ,图中可以看出整个pbuf 就是一个连续的内存区域,layer(offset)就是各层协议的首部,如TCP 报文首部、IP 首部、以太网帧首部等,预留出来的这些空间是为了在各个协议层中灵活地处理这些数据,当然layer 的大小也可以是0,具体是多少就与数据包的申请方式有关。
PBUF_POOL 类型的pbuf 与PBUF_RAM类型的pbuf 都是差不多的,其pbuf 结构体与数据缓冲区也是存在于连续的内存块中,但它的空间是通过内存池分配的,这种类型的pbuf 可以在极短的时间内分配得到,因为这是内存池分配策略的优势,在网卡接收数据的时候,LwIP 一般就使用这种类型的pbuf 来存储接收到的数据,申请PBUF_POOL 类型时,协议栈会在内存池中分配适当的内存池个数以满足需要的数据区域大小。
除此之外,在系统进行内存池初始化的时候,还需初始化两个与pbuf 相关的内存池,分别为MEMP_PBUF、MEMP_ PBUF_POOL。
LWIP_MEMPOOL(PBUF, MEMP_NUM_PBUF, sizeof(struct pbuf),"PBUF_REF/ROM")
LWIP_PBUF_MEMPOOL(PBUF_POOL,PBUF_POOL_SIZE,PBUF_POOL_BUFSIZE,"PBUF_POOL")
MEMP_PBUF 内存池是专门用于存放pbuf 数据结构的内存池,主要用于PBUF_ROM、PBUF_REF 类型的pbuf,其大小为sizeof(struct pbuf),内存块的数量为MEMP_NUM_PBUF;
MEMP_PBUF_POOL 则包含pbuf 结构与数据区域,也就是PBUF_POOL 类型的pbuf,内存块的大小为PBUF_POOL_BUFSIZE,其值由用户自己定义,默认为590(536+40+0+14)字节,当然也可以由我们定义TCP_MSS 的大小改变该宏定义,我们将宏定义TCP_MSS 的值定义为1460,这样子我们PBUF_POOL 类型的pbuf 的内存池大小为1514(1460+40+0+14),内存块的个数为PBUF_POOL_SIZE。
如果按照默认的内存大小,对于有些很大的以太网数据包,可能就需要多个pbuf 才能将这些数据存放下来,这就需要申请多个pbuf,因为是PBUF_POOL 类型的pbuf,所以申请内存空间只需要调用memp_malloc()函数进行申请即可。然后再将这些pbuf 通过链表的形式连接起组成pbuf 链表上,以保证用户的空间需求,分配与连接成功的pbuf 示意图。
PBUF_ROM和PBUF_REF 类型的pbuf 基本是一样的,它们在内存池申请的pbuf 不包含数据区域,只包含pbuf 结构体,即MEMP_PBUF 类型的POOL,这也是PBUF_ROM和PBUF_REF 与前面两种类型的pbuf 最大的差别。
PBUF_ROM类型的pbuf 的数据区域存储在ROM中,是一段静态数据,而PBUF_REF 类型的pbuf 的数据区域存储在RAM空间中。申请这两种类型的pbuf 时候也是只需要调用memp_malloc()函数从内存池中申请即可,申请内存的大小就是MEMP_PBUF,它只是一个pbuf 结构体大小,正确分配到的pbuf 内存块示意图。
注意:对于一个数据包,它可能会使用任意类型的pbuf 进行描述,也可能使用多种不同的pbuf 一起描述,如下图 所示,就是采用多种pbuf 描述一个数据包,但是无论怎么样描述,数据包的处理都是不变的,payload 指向的始终是数据区域,采用链表的形式连接起来的数据包,其tot_len 字段永远是记录当前及其后续pbuf 的总大小。
数据包申请函数pbuf_alloc()在系统中的许多地方都会用到,例如在网卡接收数据时,需要申请一个数据包,然后将网卡中的数据填入数据包中;在发送数据的时候,协议栈会申请一个pbuf 数据包,并将即将发送的数据装入到pbuf 中的数据区域,同时相关的协议首部信息也会被填入到pbuf 中的layer 区域内,所以pbuf 数据包的申请函数几乎无处不在,存在协议栈于各层之中,当然,在不同层的协议中,layer 字段的大小是不一样的,因为不一样的协议其首部大小是不同的。协议栈中各层首部的大小都会被预留出来,LwIP 采用枚举类型的变量将各个层的首部大小记录下来,在申请的时候就把layer 需要空间的大小根据协议进行分配。
#define PBUF_TRANSPORT_HLEN 20
#if LWIP_IPV6
#define PBUF_IP_HLEN 40
#else
#define PBUF_IP_HLEN 20
#endif
/**
* @ingroup pbuf
* Enumeration of pbuf layers
*/
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().
*/
/* 传输层协议首部内存空间,如UDP、TCP 报文协议首部 */
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 的类型和数据包在哪一层被申请。layer 值,当数据包申请时,所处的层次不同,就会导致预留空间的的layer 值不同。
struct pbuf *
pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type)
{
struct pbuf *p;
u16_t offset = (u16_t)layer;
LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_alloc(length=%"U16_F")\n", length));
switch (type) {
case PBUF_REF: /* fall through */
case PBUF_ROM:
/* 根据具体的pbuf 类型进行分配,对于PBUF_ROM与PBUF_REF 类
型的pbuf,只分配pbuf 结构体空间大小 */
p = pbuf_alloc_reference(NULL, length, type);
break;
case PBUF_POOL: {
struct pbuf *q, *last;
u16_t rem_len; /* remaining length */
p = NULL;
last = NULL;
rem_len = length;
do {
u16_t qlen;
/* 分配内存块,内存块类型为MEMP_PBUF_POOL。 */
q = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL);
if (q == NULL) {
PBUF_POOL_IS_EMPTY();
/* free chain so far allocated */
if (p) {
pbuf_free(p);
}
/* bail out unsuccessfully */
return NULL;
}
/* 分配成功,得到实际数据区域长度。 */
qlen = LWIP_MIN(rem_len, (u16_t)(PBUF_POOL_BUFSIZE_ALIGNED - LWIP_MEM_ALIGN_SIZE(offset)));
/* 初始化pbuf 结构体的成员变量 */
pbuf_init_alloced_pbuf(q, LWIP_MEM_ALIGN((void *)((u8_t *)q + SIZEOF_STRUCT_PBUF + offset)),
rem_len, qlen, type, 0);
LWIP_ASSERT("pbuf_alloc: pbuf q->payload properly aligned",
((mem_ptr_t)q->payload % MEM_ALIGNMENT) == 0);
LWIP_ASSERT("PBUF_POOL_BUFSIZE must be bigger than MEM_ALIGNMENT",
(PBUF_POOL_BUFSIZE_ALIGNED - LWIP_MEM_ALIGN_SIZE(offset)) > 0 );
if (p == NULL) {
/* allocated head of pbuf chain (into p) */
p = q;
} else {
/* make previous pbuf point to this pbuf */
/* 将这些pbuf 连接成pbuf 链表。 */
last->next = q;
}
last = q;
rem_len = (u16_t)(rem_len - qlen); // 计算存下所有数据需要的长度
offset = 0;
} while (rem_len > 0); // 继续分配内存块,直到将所有的数据装下为止
break;
}
case PBUF_RAM: {
u16_t payload_len = (u16_t)(LWIP_MEM_ALIGN_SIZE(offset) + LWIP_MEM_ALIGN_SIZE(length));
mem_size_t alloc_len = (mem_size_t)(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF) + payload_len);
/* bug #50040: Check for integer overflow when calculating alloc_len */
if ((payload_len < LWIP_MEM_ALIGN_SIZE(length)) ||
(alloc_len < LWIP_MEM_ALIGN_SIZE(length))) {
return NULL;
}
/* If pbuf is to be allocated in RAM, allocate memory for it. */
p = (struct pbuf *)mem_malloc(alloc_len);
if (p == NULL) {
return NULL;
}
pbuf_init_alloced_pbuf(p, LWIP_MEM_ALIGN((void *)((u8_t *)p + SIZEOF_STRUCT_PBUF + offset)),
length, length, type, 0);
LWIP_ASSERT("pbuf_alloc: pbuf->payload properly aligned",
((mem_ptr_t)p->payload % MEM_ALIGNMENT) == 0);
break;
}
default:
LWIP_ASSERT("pbuf_alloc: erroneous type", 0);
return NULL;
}
LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_alloc(length=%"U16_F") == %p\n", length, (void *)p));
return p;
}
pbuf_alloc()函数的思路很清晰,根据传入的pbuf 类型及协议层次layer,去申请对应的pbuf,就能预留出对应的协议首部空间,对于PBUF_ROM与PBUF_REF 类型的pbuf,内核不会申请数据区域,因此,pbuf 结构体中payload 指针就需要用户自己去设置,我们通常在申请PBUF_ROM与PBUF_REF 类型的pbuf 成功后,紧接着就将payload 指针指向某个数据区域。
/* 例子 */
p = pbuf_alloc(PBUF_TRANSPORT, 1472, PBUF_RAM);
数据包pbuf 的释放是必须的,因为当内核处理完数据就要将这些资源进行回收,否则就会造成内存泄漏,在后续的数据处理中无法再次申请内存。当底层将数据发送出去后或者当应用层将数据处理完毕的时候,数据包就要被释放掉。
释放数据包有条件,pbuf 中ref 字段就是记录pbuf 数据包被引用的次数,在申请pbuf 的时候,ref 字段就被初始化为1,当释放pbuf 的时候,先将ref减1,如果ref 减1 后为0,则表示能释放pbuf 数据包,此外,能被内核释放的pbuf 数据包只能是首节点或者其他地方未被引用过的节点,如果用户错误地调用pbuf 释放函数,将pbuf 链表中的某个中间节点删除了,那么必然会导致错误。
一个数据包可能会使用链表的形式将多个pbuf 连接起来,那么假如删除一个首节点,怎么保证删除完属于一个数据包的数据呢?LwIP 的数据包释放函数会自动删除属于一个数据包中连同首节点在内所有pbuf。
举个例子,假设一个数据包需要3 个pbuf 连接起来,那么在删除第一个pbuf 的时候,内核会检测一下它下一个pbuf释放与首节点是否存储同一个数据包的数据,如果是那就将第二个节点也删除掉,同理第三个也会被删除。但如果删除某个pbuf 链表的首节点时,链表中第二个节点的pbuf 中ref字段不为0,则表示该节点还在其他地方被引用,那么第二个节点不与第一个节点存储同一个数据包,那么就不会删除第二个节点。
下面用示意图来解释一下删除的过程,假设有4 个pbuf 链表,链表中每个pbuf 的ref都有一个值,当调用pbuf_free()删除第一个节点的时候,剩下的pbuf 变化情况,具体见下图:
pbuf_realloc()函数在相应pbuf(链表)尾部释放一定的空间,将数据包pbuf 中的数据长度减少为某个长度值。对于PBUF_RAM类型的pbuf,函数将调用内存堆管理中介绍到的mem_realloc()函数,释放这些多余的空间。对于其他三种类型的pbuf,该函数只是修改pbuf 中的长度字段值,并不释放对应的内存池空间。
pbuf_header()函数用于调整pbuf 的payload 指针(向前或向后移动一定字节数) 。
函数使payload 指针指向数据区前的首部字段,这就为各层对数据包首部的操作提供了方便。当然,进行这个操作的时候,len 和tot_len 字段值也会随之更新
pbuf_take()函数用于向pbuf 的数据区域拷贝数据。
pbuf_copy()函数用于将一个任何类型的pbuf 中的数据拷贝到一个PBUF_RAM类型的pbuf 中。
pbuf_chain()函数用于连接两个pbuf(链表)为一个pbuf 链表。
pbuf_ref() 函数用于将pbuf 中的值加1。
low_level_output()
low_level_input()
ethernetif_input()
https://blog.csdn.net/XieWinter/article/details/99544178#4.4%20low_level_output()