• 【lwip】04-网络数据包流向



    前言

    了解了lwip的内存管理后,接下来就是网络数据包的了解。

    注意与内部lwip消息的区别:网络数据包用于网络数据的流转,而内部lwip消息用于内部协作。

    本篇开始,结合源码独立分析lwip,逐步拆解lwip开源库。

    参考:

    4.1 TCPIP分层与lwip数据共享

    TCPIP分层思想

    • 在标准的TCP/IP协议栈中,各层之间都是一个独立的模块,每层只需要负责本层的工作即可,不会越界到其他层次去读写数据,数据传输需要层层拷贝。

    lwip数据共享

    • lwip的主要目标是嵌入式设备,作为轻量级的TCP/IP协议栈,模糊掉标准的TCP/IP分层思想,可以提高数据处理效率和内存空间利用率。
    • 即是数据在lwip的tcpip协议栈各层中是公用的,各层只需要处理各层负责的字段即可。

    4.2 协议栈线程模型

    多线程模型:

    • 协议栈各个层次都独立成为一个线程。
    • 这种线程模型严格分层,代码容易维护,功能组件容易增删,但是层次数据需要通过线程通信进行交互,可能存在层层拷贝,不适用于嵌入式设备。

    协议栈与操作系统融合:

    • 协议栈成为操作系统的一部分。
    • 线程与协议栈内核之间都是通过操作系统提供的函数来实现,协议栈各层之间与线程就没有很严格的分层结构,各层之间能交叉存取,从而提高效率。

    协议栈内核与操作系统相互隔离:(lwip在用)

    • 协议栈只是操作系统的一条独立的线程。
    • 用户程序能驻留在协议栈内部(回调方式),协议栈通过回调函数实现用户与协议栈之间的数据交互。(RAW API接口编程)
    • 也可以让用户程序单独实现一个线程,通过信号量、消息等IPC通信机制与协议栈线程进行数据交互。(NETCONN API和Socket API 编程)

    4.3 pbuf 结构体

    注意:

    • pbuf链表中第一个pbuf是有layer字段的,用于存放协议头部,而在它后面的pbuf则是没有该字段。
    • pbuf链表中,其中的节点pbuf可以由不同pbuf类型来组成的。

    源代码:

    4.3.1 pbuf的标志位flags

    有以下属性:

    4.4 pbuf的类型

    pbuf的类型,主要是以pbuf的空间结构和空间来源来区别的。

    这些标志位可以在lwip内核内部其它地方判断当前pbuf的内存属性。

    4.4.1 PBUF_RAM类型

    PBUF_RAM类型的pbuf:

    • PBUF_RAM类型的pbuf空间是由内存堆分配;
    • pbuf的数据管理区和数据区地址空间是连续的;
    • 多用于发送数据。

    4.4.2 PBUF_ROM类型

    PBUF_ROM类型的pbuf:

    • PBUF_ROM类型的pbuf结构体空间是由内存池分配,即是MEMP_PBUF类型的POOL;(不包含数据区)
    • pbuf的数据管理区和数据区地址空间是不连续的,PBUF_ROM的数据区存在ROM中,一般是静态数据。

    4.4.3 PBUF_REF类型

    PBUF_REF类型的pbuf和PBUF_ROM类型的pbuf结构一样,只是数据区的存储地址一个在RAM区一个在ROM区。

    PBUF_REF类型的pbuf:

    • PBUF_REF类型的pbuf结构体空间是由内存池分配,即是MEMP_PBUF类型的POOL;(不包含数据区)
    • pbuf的数据管理区和数据区地址空间是不连续的,PBUF_REF的数据区存在RAM中。

    4.4.4 PBUF_POOL类型

    PBUF_POOL类型的pbuf:

    • PBUF_POOL类型的pbuf空间是由内存池分配;
    • pbuf的数据管理区和数据区地址空间是连续的;
    • 该pbuf的实际空间大小是固定的;
    • 多用于接收数据,因为空间申请快。
    • 不要用于TX,因为如果当内存池为空了,TCP在排队等待,就会接收不了TCP ACK。

    系统会初始化两个与pbuf相关的内存池:

    • MEMP_PBUF:用于存放pbuf数据结构的内存池。主要用于pbuf数据结构和数据区地址不连续的PBUF_ROM、PBUF_REF类型的pbuf。
    • MEMP_ PBUF_POOL:用于存放pbuf数据结构和数据区地址连续的内存池。主要供给PBUF_POOL类型的pbuf。

    PBUF_POOL类型的pbuf链表如下图所示:

    • 由于PBUF_POOL类型的pbuf内存是由内存池分配的,所以pbuf链表中最后一个pbuf存在空间浪费的可能。

    MEMP_PBUF_POOL类型的pbuf长度:PBUF_POOL_BUFSIZE_ALIGNED

    PBUF_POOL_BUFSIZE_ALIGNED长度是整个TCPIP协议栈从链路层到传输层的最大报文长度的size,是包含TCP_MSS, TRANSPORT header, IP header, and link header,还有一个原始层首部(默认为0),且需要字节。

    TCP_MSS:除去头部之后,一个网络包所能容纳的 TCP 数据的最大长度。参考下图。

    PBUF_IP_HLEN:IP层首部长度。

    PBUF_TRANSPORT_HLEN:传输层首部长度。

    PBUF_LINK_ENCAPSULATION_HLEN:原始层首部长度。默认为0。

    PBUF_LINK_HLEN:链路层首部长度。

    4.5 pbuf_alloc()

    pbuf_alloc()是数据包申请函数:(详细直接分析源码)

    • pbuf_layer layer:协议层枚举,直接就是该层首部大小了。不同的协议层,layer大小不一样。
    • u16_t length:pbuf有效载荷的大小。和layer参数共同决定pbuf空间大小。
    • pbuf_type type:pbuf的类型,决定了pbuf空间怎样分配和空间来源。

    4.5.1 各层首部大小

    层级越高,首部预留的空间要越大,因为往下层传的时候需要下层需要填充该层的首部。

    相关宏:

    • PBUF_LINK_ENCAPSULATION_HLEN:原始层首部长度。默认为0,在TCPIP协议栈中不预留空间。
    • PBUF_LINK_HLEN:链路层首部长度。默认是以太网的标准值,14。可以按系统bit长度来偏移,让IP层从系统对齐地址起。
    • PBUF_IP_HLEN:IP层首部长度。默认ipv4是20,如果使能了ipv6,就是40。
    • PBUF_TRANSPORT_HLEN:传输层首部长度。默认20。

    4.5.2 各个pbuf类型的空间分配实现(简要)

    PBUF_REFPBUF_ROM类型,只分配pbuf数据结构空间,从MEMP_PBUF内存池中获得。

    PBUF_POOL类型,空间从MEMP_PBUF_POOL内存池中获得,如果一个pbuf节点不够,就会以链表的方式获取。

    PBUF_RAM类型,空间从内存堆中获得,一次性获取。

    4.5.3 PBUF_POOL类型malloc实现

    由于MEMP_PBUF_POOL内存池中每个节点的空间大小都是固定的,所以可能会出现一个节点不够用的情况,这样就需要pbuf链表的形式管理申请到的空间。

    调用q = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL);申请一个MEMP_PBUF_POOL类型的pbuf节点空间。

    如果内存池为空,可以通过调用PBUF_POOL_IS_EMPTY();来释放ooseq链表中释放无序报文的MEMP_PBUF_POOL内存池空间,但是本次申请也是需要退出的。

    退出本次空间申请前,需要释放本次循环申请MEMP_PBUF_POOL类型的pbuf节点空间。如调用pbuf_free(p);

    当前pbuf节点空间申请成功后:

    获取当前pbuf节点实际需要的、有效的数据空间长度。

    如果是首个pbuf节点,还需要根据layer参数预留首部空间。

    初始化当前pbuf节点。

    更新变量值。

    4.5.4 PBUF_RAM类型malloc实现

    先计算申请堆空间长度:pbuf数据结构空间+首部空间+用户实际申请的空间。注意字节对齐。

    通过p = (struct pbuf *)mem_malloc(alloc_len);从内存堆中申请。

    void *mem_malloc(mem_size_t size_in);这个函数有三种实现方式:

    1. 基于自定义。
    2. 基于内存池。
    3. 基于内存堆。

    申请成功后初始化当前pbuf节点。

    4.5.5 pbuf_alloc()源码

    4.6 pbuf_free()

    4.6.1 相关参数

    函数原型:u8_t pbuf_free(struct pbuf *p)

    • struct pbuf *p:需要解除应用的pbuf或pbuf链表头。
    • 返回:返回被释放空间的pbuf个数。

    4.6.2 释放pbuf空间的条件

    pbuf_free()用于释放pbuf空间。

    pbuf中ref字段就是记录pbuf数据包被引用的次数;

    该字段在pbuf申请初始化时,被置为1,表示还没有被引用。

    后续被引用一次,该字段+1,被pbuf_free()一次,该字段-1。

    当ref字段值为0时,该pbuf空间才可以备释放。

    4.6.3 释放pbuf链表的逻辑

    pbuf_free()释放pbuf链表的逻辑:

    • 从pbuf链表首节点开始ref减1,为0则直接释放当前节点;
    • 直到pbuf链表全部释放完毕或遇到ref减1后不为0的pbuf节点为止。后续的pbuf节点的ref字段也不会减1。

    因为lwip检索pbuf链表,ref减一后不为0,则认为当前pbuf节点为下一个数据包的首个pbuf节点。

    所以,pbuf_free()遇到释放pbuf链表时,只会处理第一个数据包占用的pbuf节点。

    pbuf链表中的同一个数据包的分界线实现,是通过在每个数据包的首个pbuf节点多加一个ref引用标志。

    举个几个栗子:

    • [一个pbuf链表的ref] --> [经过pbuf_free()释放后的pbuf链表的ref]
    • [1--2--3--3] --> [..--1--3--3]
    • [2--1--2] --> [1--1--2]
    • [1--1--2] --> [..--..--1]

    4.6.4 pbuf_free()使用说明

    1. 不能调用pbuf_free()释放包队列(packet queue)空间。

    2. 如果需要释放pbuf链表,则必须传入pbuf链表头指针,切不能传入中间pbuf节点的指针,避免内存管理异常。

    3. pbuf的引用计数器ref等于指向pbuf(或指向pbuf)的指针的数量。

      • 创建pbuf时,ref为1,就是只有一个指针指向当前pbuf。

    4.6.5 pbuf_free()实现说明

    每个pbuf的ref字段操作都需要实现线程安全、原子性操作。

    在多线程的系统下,可以在lwipports.h文件中实现这些宏定义:

    • 这些宏是在lwip内部必要时,实现线程安全、原子性操作。
    • 下面只是例子,进入临界操作。
    • 当然其它能实现线程安全和原子性操作的都可以,如锁。(这个纯属个人推测,并未思考过多问题

    4.6.7 pbuf_free()源码分析

    4.7 其它pbuf处理函数

    4.7.1 pbuf_realloc()

    用于裁剪pbuf(链表)尾部空间。

    只能裁剪,不能扩张。

    对于PBUF_RAM类型的pbuf,是可能会释放尾部空间,而其它三种pbuf类型,不会释放空间,只会修改pbuf中的长度字段值。

    而对于pbuf链表,在截断分界线后的pbuf,都会调用pbuf_free()进行释放。当ref减1后为0,也会存在真正释放空间的可能。

    对于pbuf链表,即是截断分界线后的pbuf没有被真正释放空间,这个pbuf链表也会截断,拆分链表。

    下面用到的void *mem_trim(void *rmem, mem_size_t newsize)函数属于内存管理的内存堆范畴,这里不对其进行源码剖析,可简单说明:

    • 用于裁剪内存堆尾部空间,不支持内存扩充。

    • 函数简要实现内容:

      • 计算被裁剪的空间能否构成下一个内存堆节点。
      • 符合则初始化新的内存堆节点,插入内存堆链表。
      • 返回NULL或者传入的rmem地址。

    4.7.2 pbuf_header()

    调整pbuf中的payload指针以隐藏或显示数据区前的首部字段。

    payload指针偏移后,pbuf中的len和tot_len字段也会刷新。

    不支持PBUF_ROM和PBUF_REF类型的pbuf修改payload指针偏移。

    隐藏部分头部字段,如下层转交pbuf到上层时的处理。调用pbuf_remove_header()实现。

    暴露部分头部字段,如上层转交pbuf到下层时的处理。调用pbuf_add_header_impl()实现。

    4.7.3 pbuf_take()

    pbuf_take()函数用于向pbuf的数据区域拷贝数据。

    虽然该函数内部实现只是限制了传入的数据长度不能大于pbuf的tot_len,

    但是该函数在使用来说,建议只用于复制buf->tot_len的等价数据,即是数据传入的数据刚好填满pbuf。

    4.7.4 pbuf_copy()

    pbuf_copy()函数用于将一个任何类型的pbuf中的数据拷贝到一个PBUF_RAM类型的pbuf中。

    用于代表lwIP堆栈对包进行排队,如ARP队列。

    4.7.5 pbuf_cat()

    pbuf_cat() 用于拼接两个pbuf链表,且后面接入的pbuf链表不与前面的pbuf链表有分割标志,拼接后,后面的pbuf链表不能被其它地方引用了。

    如果还想保留后面的pbuf链表能被其它地方引用的权限,就使用pbuf_chain()函数来拼接。

    需要注意的是,该函数的实现没有对tot_len字段溢出监测处理,所以使用时需要预判,两个链表的tot_len拼接后不要有溢出的可能。

    4.7.6 pbuf_ref()

    pbuf_ref()函数用于将pbuf中的值加1。

    4.7.7 pbuf_chain()

    pbuf_chain()函数用于连接两个pbuf(链表)为一个pbuf链表。

    调用该函数后,后接入的链表不能使用pbuf_free()对其进行释放了。

    而且后面接入的pbuf链表的首个pbuf节点的ref引用字段+1,作为两个数据包的分割点。

    4.7.8 更多

    pbuf_dechain()用于把pbuf链表中的首个pbuf节点进行拖链,且,返回新的pbuf链表。

    参考pbuf.cpbuf.h

    4.8 网卡中使用的pbuf

    网卡中的回调函数需要根据网卡设备类型来实现。

    low_level_output()low_level_input()函数是网卡的南向直接操作函数,是对网卡设备的写、读处理。

    相当于网卡设备的驱动范畴的函数。

    4.8.1 low_level_output()

    low_level_output()函数只是单纯的往网卡设备发送数据。

    一般把这个函数插入到netif->linkoutput()中,供其网卡调用。给是ARP用来往链路层发送数据。

    该函数类型如下:

    • 传入的数据是pbuf形式,该函数的实现需要从pbuf从获取数据体出来,发送出去。

    4.8.2 low_level_input()

    low_level_input()从网卡设备中接收数据。

    在lwip中,该函数需要实现从网卡设备中获取数据,并把数据组装成为pbuf形式,是MEMP_PBUF_POOL类型的pbuf。

    该函数不会直接插入到netif的数据结构中,因为网卡没有直接调用该函数的主动性,是靠外部收到数据后触发执行low_level_input()函数获取数据;

    然后再调用netif->input()把数据按要求上交给TCPIP协议栈。


    __EOF__

  • 本文作者: 李柱明
  • 本文链接: https://www.cnblogs.com/lizhuming/p/16630602.html
  • 关于博主: 嵌入式从业者。RTOS、Linux lwip mbedtls...
  • 版权声明: 版权归博主所有
  • 声援博主: 学习笔记分享
  • 相关阅读:
    Linux系统之编译安装python3
    嵌入式经验分享:面试薪资直接翻番,我才明白TA的重要性!
    百度面试题:为什么使用接口而不是直接使用具体类?
    一文搞定CMakeLists编写与库配置
    React+Vue相关插件使用的缺陷小合集
    题目0061-第K个最小码值的字母
    poi 设置允许西文在单词中间换行
    Linux下安装配置各种软件和服务
    [BJDCTF2020]Cookie is so stable 1
    Explain信息中Extra字段解释
  • 原文地址:https://www.cnblogs.com/lizhuming/p/16630602.html