6、网络数据包pbuf
TCP/IP 是一种数据通信机制,因此,协议栈的实现本质上就是对数据包进行处理,为了实现高效的效率, LwIP 数据包管理要提供一种高效处理的机制。 协议栈各层能对数据包进行灵活的处理,同时减少数据在各层间传递时的时间与空间开销,这是提高协议栈工作效率的关键点。在 BSD 的实现中,一个描述数据包的结构体叫做 mbuf,同样的 在 LwIP中,也有个类似的结构,称之为 pbuf,本章所有知识点将围绕 pbuf 而展开。
什么是数据包?数据包的种类可谓是五花八门,无奇不有,就比如网卡上的接收到的数据,它可以是一个一千多字节的数据包,也可以是几十个字节的 ARP 数据包,还有用户的数据,这些数据可能存在 RAM、 ROM 上,重点是这些数据大小不一,可以是几个字节,也看是上千个字节,并且 LwIP 各层在数据处理的时候极力避免进行数据的拷贝,所以就需要一个数据包对这些数据进行统一的管理,使得 LwIP 处理效率更加高效。
1、TCP/IP 协议的分层思想
在标准的 TCP/IP 协议栈中,各层之间都是一个独立的模块,它有着很清晰的层次结构,每一层只负责完成该层的处理,不会越界到其他层次去读写数据。而 LwIP 只是一个轻量级 TCP/IP 协议栈,它只是一个较完整的 TCP/IP 协议,多应用在嵌入式领域中,由于处理器的性能有限, LwIP 并没有采用很明确的分层结构,它假设各层之间的部分数据和结构体和实现原理在其他层是可见的,简单来说就传输层知道 IP 层是如何封装数据、传递数据的,IP 层知道链路层是怎么封装数据的等等。
为什么要模糊分层的处理?简单来说就是为了提高效率,例如链路层完成数据包在物理线路上传输的封装; IP 层完成数据包的选择和路由,负责将数据包发送到目标主机;传输层负责根据 IP 地址将数据包传输到指定主机,端口号识别一台主机的线程,向不同的应用层递交数据;但是,如果按照标准的 TCP/IP 协议栈这种严格的分层思想,在数据传输的时候就需要层层拷贝,因为各层之间的内存都不是共用的,在链路层递交到 IP 层需要拷贝,在 IP 层递交到传输层需要拷贝,反之亦然,这样子每当收到或者发送一个数据的时候都要CPU 去拷贝数据,这个效率就太慢了,所以 LwIP 假设各层之间的资源都是共用的,各层之间的实现方式也是已知的,那么在 IP 层往传输层递交数据的时候,链路层往 IP 层递交数据的时候就无需再次拷贝, 直接操作协议栈中属于其他层次的字段,得到相应的信息,然后直接读取传递的数据即可,这样子处理的方式就无需拷贝,各个层次之间存在交叉存取数据的现象,既节省系统的空间也节省处理的时间,而且更加灵活。 例如在传输层,在计算 TCP 报文段的校验以及 TCP 在重装无需报文时, TCP 层必须知道该报文的源 IP 地址和目的 IP 地址,为了得到这些信息,传输层并不是调用 IP 层的接口,而是直接访问数据包中的 IP 数据报首部,因为传输层已经知道 IP 层的数据报的格式及作用,直接访问读取这些信息即可。
在小型嵌入式设备中, LwIP 与用户程序之间通常没有太严格的分层结构,这种方式允许用户处理数据与内核之间变得更加宽松。 LwIP 假设用户完全了解协议栈内部的数据处理机制,用户程序可以直接访问协议栈内部各层的数据包,可以让协议栈与用户使用同样的内存区域,允许用户直接对这片区域进行读写操作,这样子就很好地避免了拷贝的现象,当然这样子的做法也有缺陷,取决于用户对协议栈处理过程的了解程度,因为数据是公共的,如果处理不正确那就让协议栈也没法正常工作。
当然,除了标准的 TCP/IP 协议,还存在很多其他的 TCP/IP 协议,即使这些协议栈内部存在着模糊分层、交叉存取现象,但是对协议栈外部的应用层则保持着明显的分层结构,在操作系统中, TCP/IP 协议栈往往被设计为内核代码的一部分,用户可以的函数仅仅是协议栈为用户提供的那些,或者直接完全封装起来,用户的操作类似于读写文件的方式进行(如 BSD Socket),这样子用户就无法避免数据的拷贝,在数据发送的时候,用户数据必须从用户区域拷贝到协议栈内部,在数据接收的时候,协议栈内部数据也将被拷贝到用户区域。
严格按照 TCP 协议的分层思想会导致数据包在各层递交产生内存拷贝问题,影响性能。为了节省时间和空间的开销, LwIP 没有遵循严格的分层机制,各层次之间存在交叉存取的现象,提高效率。
由于 LwIP 的内存共享机制,使得应用程序能直接对协议栈内核的内存区域直接操作,减少时间和空间的损耗。
2、LwIP 的线程模型
线程模型可以理解为协议栈的实现被划分在多个线程之中,如让协议栈的各个层次都独立成为一个线程,在这种模式下,各个层次都有严格分层结构, 各个层次的提供的 API接口也是分层清晰的,这样子能使得编程的时候更加简便,代码的组织也更加灵活,当然,按照前面所说的,在嵌入式设备中,这种严格的分层结构并不是最好的, 在数据包向其他层递交的时候,都需要进行拷贝以及切换线程,这是一个很大的开销,当数据量太大的时候,这种开销就足以导致系统没法处理大量的数据,如一个数据包在各个层次间的递交至少需要进行 3 次切换线程:底层网卡接收到一个数据包,此时是链路层的线程在工作,当它往上层递交该数据包的时候,就需要切换线程,该数据包被拷贝到 IP 层,此时是 IP 层的线程在工作,当 IP 层处理完毕之后,要向传输层递交数据包,那么此时又需要切换线程,到传输层的线程中处理该数据包,这样子使得协议栈的效率非常低下。
还有一种方式就是协议栈与操作系统融合, 成为操作系统的一部分,这样子用户线程与协议栈内核之间都是通过操作系统提供的函数来实现的,这种情况让协议栈各层之间与用户线程就没有很严格的分层结构,各层之间能交叉存取,从而提高效率。
但是 LwIP 采用了另一种方式,让协议栈内核与操作系统相互隔离,协议栈仅仅作为操作系统的一个独立线程存在,用户程序能驻留在协议栈内部,协议栈通过回调函数实现用户与协议栈之间的数据交互;也可以让用户程序单独实现一个线程,与协议栈使用系统的信号量和邮箱等 IPC 通信机制联系起来,进行数据的交互。 当使用第一种通过回调函数进行交互情况的时候,也就是我们所说的 RAW API 编程,而使用第二种通过操作系统 IPC通信机制的时候,就是另外两种 API 编程,即 NETCONN API 和 Socket API。当然这样子既有优点也有缺点,优点就是能在任何的操作系统中移植,缺点就是受到操作系统的影响,因为即使 LwIP 作为一个独立的线程,也是需要借助操作系统进行调度的,因此,协议栈的响应的实时性会有一定影响,并且建议设置 LwIP 线程的优先级为最高优先级。
3、pbuf 结构体说明
pbuf 就是一个描述协议栈中数据包的数据结构, LwIP 中在 pbuf.c 和 pubf.h 实现了协议栈数据包管理的所有函数与数据结构, pbuf 数据结构的定义具体见下面代码:
/** Main packet buffer struct */ struct pbuf { /** next pbuf in singly linked pbuf chain */ struct pbuf *next; (1) /** pointer to the actual data in the buffer */ void *payload; (2) u16_t tot_len; (3) /** length of this buffer */ u16_t len; (4) u8_t type_internal; (5) /** misc flags */ u8_t flags; (6) LWIP_PBUF_REF_T ref; (7) /** For incoming packets, this contains the input netif's index */ u8_t if_idx; (8) }
代码清单 (1): next 是一个 pbuf 类型的指针,指向下一个 pbuf,因为网络中的数据包可能很大,而 pbuf 能管理的数据包大小有限,就会采用链表的形式将所有的 pbuf 包连接起来,这样子才能完整描述一个数据包,这些连接起来的 pbuf 包会组成一个链表,我称之为 pbuf 链表。
代码清单 (2): payload 是一个指向数据区域的指针, 指向该 pbuf 管理的数据区域起始地址, 这里的数据区域可以是紧跟在 pbuf 结构体地址后面的 RAM 空间,也可以是ROM 中的某个地址上,取决于 pbuf 的类型。
代码清单 (3): tot_len 中记录的是当前 pbuf 及其后续 pbuf 所有数据的长度, 例如如果当前 pbuf 是 pbuf 链表上第一个数据结构, 那么 tot_len 就记录着整个 pbuf 链表中所有pbuf 中数据的长度;如果当前 pbuf 是链表上最后一个数据结构,那就记录着当前 pbuf 的长度。
代码清单 (4): len 表示当前 pbuf 中有效的数据长度。
代码清单 (5): type_internal 表示 pbuf 的类型, LwIP 中有 4 种 pbuf 的类型,并且使用了一个枚举类型的数据结构定义他们,具体见代码清单 6-2。
代码清单 (6): flags 字段在初始化的时候一般被初始化为 0,此处就不对 flags 字段进行过多讲解。
代码清单 (7): ref 表示该 pbuf 被引用的次数,引用表示有其他指针指向当前 pbuf,这里的指针可以是 pbuf 的 next 指针,也可以是其他任意形式的指针,初始化一个 pbuf 的时候, ref 会被设置为 1,因为该 pbuf 的地址一点会被返回一个指针变量,当有其他指针指向 pbuf 的时候,就必须调用相关函数将 ref 字段加 1。
代码清单 (8): if_idx 用于记录传入的数据包中输入 netif 的索引,也就是 netif 中num 字段。
4、pbuf 的类型
typedef enum { PBUF_RAM = (PBUF_ALLOC_FLAG_DATA_CONTIGUOUS | PBUF_TYPE_FLAG_STRUCT_DATA_CONTIGUOUS | PBUF_TYPE_ALLOC_SRC_MASK_STD_HEAP), PBUF_ROM = PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF, PBUF_REF = (PBUF_TYPE_FLAG_DATA_VOLATILE | PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF), PBUF_POOL = (PBUF_ALLOC_FLAG_RX | PBUF_TYPE_FLAG_STRUCT_DATA_CONTIGUOUS | PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF_POOL) } pbuf_type;
pbuf 的类型有 4 种,分别为 PBUF_RAM、 PBUF_POOL 、 PBUF_ROM、 PBUF_REF。
4.1 PBUF_RAM 类型的 pbuf
PBUF_RAM 类型的 pbuf 空间是通过内存堆分配而来的, 这种类型的 pbuf 在协议栈中使用得最多, 一般协议栈中要发送的数据都是采用这种形式, 在申请这种 pbuf 内存块的时候,协议栈会在管理的内存堆中根据需要的大小进行分配对应的内存空间,这种 pbuf 内存块包含数据空间以及 pbuf 数据结构区域,在连续的 RAM 内存空间中。很多人又会有疑问了,不是说各个协议层都有首部吗,这些内存空间在哪呢?能想到这一层的读者是非常聪明的,我很欣慰,你们有认真看前面的章节,内核申请这类型的 pbuf 时,也算上了协议首部的空间,当然是根据协议栈不同层次需要的首部进行申请, LwIP 也使用一个枚举类型对不同的协议栈分层需要的首部大小进行定义,关于各层间的首部区域我们在后续讲解,此处只需知道即可。那么申请这种 pbuf 是怎么样申请的呢?具体见下面代码清单:
/* 函数原型 */ struct pbuf * pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type); //例子 struct pbuf *p; p = pbuf_alloc(PBUF_RAW, (u16_t)(req_len + 1), PBUF_RAM); p = pbuf_alloc(PBUF_TRANSPORT, 1472, PBUF_RAM);
PBUF_RAM 类型的 pbuf 示意图具体见图 6-1, 图中可以看出整个 pbuf 就是一个连续的内存区域, layer(offset) 就是各层协议的首部, 如 TCP 报文首部、 IP 首部、以太网帧首部等, 预留出来的这些空间是为了在各个协议层中灵活地处理这些数据, 当然 layer 的大小也可以是 0,具体是多少就与数据包的申请方式有关,具体在后面的章节中讲解。

4.2、 PBUF_POOL 类型的 pbuf
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 链表中第一个 pbuf 是有 layer 字段的,用于存放协议头部,而在它后面的 pbuf 则是没有该字段,由于 PBUF_POOL 类型 pbuf 都是以固定长度分配的, 在最后一个 pbuf 中, 可能会被浪费大量的空间,并且,每个 pbuf 的 tot_len 字段记录的就是自身及其后面的 pbuf 总大小。
4.3、 PBUF_ROM 和 PBUF_REF 类型 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 一起描述,如图 6-4 所示,就是采用多种 pbuf 描述一个数据包,但是无论怎么样描述,数据包的处理都是不变的, payload 指向的始终是数据区域,采用链表的形式连接起来的数据包,其 tot_len 字段永远是记录当前及其后续 pbuf 的总大小。

5 、pbuf_alloc()
数据包申请函数 pbuf_alloc()在系统中的许多地方都会用到, 例如在网卡接收数据时,需要申请一个数据包, 然后将网卡中的数据填入数据包中;在发送数据的时候,协议栈会申请一个 pbuf 数据包,并将即将发送的数据装入到 pbuf 中的数据区域,同时相关的协议首部信息也会被填入到 pbuf 中的 layer 区域内,所以 pbuf 数据包的申请函数几乎无处不在,存在协议栈于各层之中,当然,在不同层的协议中, layer 字段的大小是不一样的,因为不一样的协议其首部大小是不同的,这个知识点会在后文讲解各协议的时候讲解,此处只需了解一下即可。协议栈中各层首部的大小都会被预留出来, LwIP 采用枚举类型的变量将各个层的首部大小记录下来,在申请的时候就把 layer 需要空间的大小根据协议进行分配,具体见下面代码清单:
#define PBUF_TRANSPORT_HLEN 20 #define PBUF_IP_HLEN 20 typedef enum { PBUF_TRANSPORT = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN + PBUF_IP_HLEN + PBUF_TRANSPORT_HLEN,(1) PBUF_IP = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN + PBUF_IP_HLEN, (2) PBUF_LINK = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN, (3) PBUF_RAW_TX = PBUF_LINK_ENCAPSULATION_HLEN, (4) PBUF_RAW = 0 (5) } pbuf_layer;
代码清单(1):传输层协议首部内存空间,如 UDP、 TCP 报文协议首部。
代码清单(2):网络层协议首部内存空间,如 IP 协议。
代码清单(3):链路层协议首部内存空间,如以太网。
代码清单(4)(5): 原始层,不预留空间, PBUF_LINK_ENCAPSULATION_HLEN宏定义默认为 0。
数据包申请函数有两个重要的参数:数据包 pbuf 的类型和数据包在哪一层被申请。数据包类型就是我们之前讲的那四种,数据包在哪一层申请这个参数主要是为了预留各层协议的内存大小,也就是前面所说的 layer 值, 当数据包申请时,所处的层次不同,就会导致预留空间的的 layer 值不同。
pbuf_alloc()函数的思路很清晰,根据传入的 pbuf 类型及协议层次 layer, 去申请对应的pbuf, 就能预留出对应的协议首部空间, 对于 PBUF_ROM 与 PBUF_REF 类型的 pbuf,内核不会申请数据区域,因此, pbuf 结构体中 payload 指针就需要用户自己去设置,我们通常在申请 PBUF_ROM 与 PBUF_REF 类型的 pbuf 成功后, 紧接着就将 payload 指针指向某个数据区域。举个例子,假设 TCP 协议需要申请一个 pbuf 数据包,那么就会调用下面代码进行申请:
p = pbuf_alloc(PBUF_TRANSPORT, 1472, PBUF_RAM);
内核就会根据这句代码进行分配一个 PBUF_RAM 类型的 pbuf, 其数据区域大小是1472 字节,并且会根据协议层次进行预留协议首部空间,由于是传输层,所以内核需要预留 54 个字节空间,即以太网帧首部长度 PBUF_LINK_HLEN(14 字节)、 IP 数据报首部长度 PBUF_IP_HLEN(20 字节) 、 TCP 首部长度 PBUF_TRANSPORT_HLEN(20 字节)。当数据报往下层递交的时候,其他层直接填充对应的协议首部即可,无需对数据进行拷贝等操作,这也是 LwIP 能快速处理的优势。
6、pbuf_free()
数据包 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都有一个值,具体见图 6-5,当调用 pbuf_free()删除第一个节点的时候,剩下的 pbuf 变化情况,具体见


从这两张图中我们也看到了,当删除第一个节点后,如果后续的 pbuf 的 ref 为 1(即与第一个节点存储同一个数据包),那么该节点也会被删除。 第一个 pbuf 链表在删除首节点之后就不存在节点;第二个 pbuf 链表在删除首节点后只存在 pbuf3;第三个 pbuf 链表在删除首节点后还存在 pbuf2 与 pbuf3;第四个链表还不能删除首节点,因为该数据包还在其他地方被引用了。
pbuf 的释放要小心,如果 pbuf 是串成链表的话, pbuf 在释放的时候,就会把 pbuf 的ref 值减 1,然后函数会判断 ref 减完之后是不是变成 0,如果是 0 就会根据 pbuf 的类型调用内存池或者内存堆回收函数进行回收。 然后这里就有个很危险的事, 对于这个 pbuf_free()函数, 用户传递的参数必须是链表头指针,假如不是链表头而是指向链表中间的某个 pbuf的指针,那就很容易出现问题, 因为这个 pbuf_free()函数可不会帮我们检查是不是链表头,这样子势必会导致一部分 pbuf 没被回收,意味着一部分内存池就这样被泄漏了,以后没办法用了。 同时,还可能将一些尚未处理的数据回收了,这样子整个系统就乱套了。
7、其它 pbuf 操作函数
(1)buf_realloc()
pbuf_realloc()函数在相应 pbuf(链表)尾部释放一定的空间,将数据包 pbuf 中的数据长度减少为某个长度值。对于 PBUF_RAM 类型的 pbuf,函数将调用内存堆管理中介绍到的mem_realloc()函数,释放这些多余的空间。对于其他三种类型的 pbuf,该函数只是修改pbuf 中的长度字段值,并不释放对应的内存池空间。
(2)pbuf_header()
pbuf_header()函数用于调整 pbuf 的 payload 指针(向前或向后移动一定字节数),在前面也说到过,在 pbuf 的数据区前可能会预留一些协议首部空间,而 pbuf 被创建时,payload 指针是指向数据区的,为了实现对这些预留空间的操作,可以调用 pbuf_header()函数使 payload 指针指向数据区前的首部字段,这就为各层对数据包首部的操作提供了方便。当然,进行这个操作的时候, len 和 tot_len 字段值也会随之更新。
(3)pbuf_take()
pbuf_take()函数用于向 pbuf 的数据区域拷贝数据。 pbuf_copy()函数用于将一个任何类型的 pbuf 中的数据拷贝到一个 PBUF_RAM 类型的 pbuf 中。 pbuf_chain()函数用于连接两个pbuf(链表)为一个 pbuf 链表。 pbuf_ref 函数用于将 pbuf 中的值加 1。
8、网卡中使用的 pbuf
前面我们仅仅讲解了网卡初始化相关的内容, 但是对于网卡的接收数据与发送数据并未做过多的讲解, 现在我们学习了 pbuf 数据包, 那么就能编写网卡底层的接收与发送数据相关的代码了。
(1) low_level_output()
网卡发送数据是通过 low_level_output()函数实现的,该函数是一个底层驱动函数,这要求用户熟悉网卡底层特性,还要熟悉 pbuf 数据包。首先说说发送数据的过程,用户在应用层想要通过一个网卡发送数据,那么就要将数据传入 LwIP 内核中,经过内核的传输层封装、 IP 层封装等等,简单来说就是上层将要发送的数据层层封装,存储在 pbuf 数据包中,可能数据很大,想要多个 pbuf 才能存放得下,这时候 pbuf 就以链表的形式存在,当数据发送的时候,就要将属于一个数据包的数据全部发送出去,此处需要注意的是,属于同一个数据包中的所有数据都必须放在同一个以太网帧中发送。 low_level_output()函数的实现具体见下面代码清单:
(2)low_level_input()
与 low_level_output()函数相反的是 low_level_input()函数,该函数用于从网卡中接收一个数据包,并将数据包封装在 pbuf 中递交给上层。 low_level_input()函数的编写也需要用户熟悉 pbuf 与网卡底层驱动,该函数的实现具体见下面代码清单:
(3)ethernetif_input()
low_level_output()函数只是完成了网卡驱动接收,但是还没将 pbuf 数据包递交给上层,那么又是谁将 pbuf 数据包递交给上层的呢?我们在前面讲解 4.3 小节的时候, 就知道ethernetif_input()函数会被周期性调用(如果是采用操作系统方式,往往会将该函数变成线程形式运行),这样子就能接收网卡的数据,在接收完毕,就能将数据通过网卡 netif 的input 接口将 pbuf 递交给上层,该函数的实现具体见下面代码清单:

浙公网安备 33010602011771号