LwIP 分析

 

本文是对LwIP的分析记录

本文是对LwIP的分析记录

内存管理

内存分配策略

  • C标准库自带的内存分配策略
  • 动态内存池
    • 每次只能申请几种固定大小的内存
    • 为TCP、IP首申请对应大小的内存
    • 系统启动时,初始化几类固定大小的内存块链表
  • 动态内存堆
  • 宏开关
      #define MEM_LIBC_MALLOC  0
      #define MEMP_MEM_MALLOC  0
      #define MEM_USE_POOLS    0
    
  • 动态内存堆可以和C标准库内存分配共用
    • 可同时打开
  • 动态内存池不可和C标准库共用
  • 一般使用动态内存池和动态内存堆

动态内存池

pool

  • LwIP用这种策略给UDP控制块、TCP控制块(TCB)申请内存
  • LwIP内核在初始化时就为不同的数据结构初始化好的一定数量的POOL
  • 文件:
    • memp.c
    • memp.h
  • lwipopts.h的配置
    • 打开了开关,LWIP会建立它相关的多种内存池
    • UDP相关的内存池开关
      • #define LWIP_UDP 1
    • TCP相关内存池开关
      • #define LWIP_TCP 1
  • 内存池的组织
    • 放了A类型的a个A_POOL
    • 紧接着放B类型的b个B_POOL
    • 在放C类型的c个C_POOL

memp_init

首先在使用LwIP时,会先执行memp_init函数:

  • 初始化lwIP内置的内存池
  • 将memp_memory分割成每个池类型的链接列表
void memp_init(void) {
  u16_t i;

  /* for every pool: */
  for (i = 0; i < LWIP_ARRAYSIZE(memp_pools); i++) {
    memp_init_pool(memp_pools[i]);
  }
}

其中memp_pools是内存池描述符

const struct memp_desc* const memp_pools[MEMP_MAX] = {
#define LWIP_MEMPOOL(name,num,size,desc) &memp_ ## name,
#include "lwip/priv/memp_std.h"
};
  • ##是连字符,比如LWIP_MEMPOOL(例子, num1, size1, desc)会编译成MEMP_例子
  • #把后面的宏参数字符化
      #define ERR_IF(expr) {if(expr) printf("error:" #expr "\n");}
    

    对于ERR_IF(a==b)会展开成: ``` {if(a==b) printf(“error:” “a==b” “\n”);}

MEMP_MAX由下面的内容决定

typedef enum {
#define LWIP_MEMPOOL(name,num,size,desc)  MEMP_##name,
#include "lwip/priv/memp_std.h"
  MEMP_MAX
} memp_t;

memp_std.h的本质是多个LWIP_MEMPOOL宏组成,比如

#if LWIP_RAW
LWIP_MEMPOOL(RAW_PCB, MEMP_NUM_RAW_PCB,sizeof(struct raw_pcb),"RAW_PCB")
#endif /* LWIP_RAW */

#if LWIP_UDP
LWIP_MEMPOOL(UDP_PCB,MEMP_NUM_UDP_PCB,sizeof(struct udp_pcb),"UDP_PCB")
#endif /* LWIP_UDP */

因为我们通过lwipopts.hopt.h打开了LWIP_RAWLWIP_UDP的开关,所以这里就展开成

typedef enum {
	MEMP_RAW_PCB,
	MEMP_UDP_PCB,
	MEMP_MAX
} memp_t;

这样,memp_pools[MEMP_MAX]就等于:

const struct memp_desc* const memp_pools[MEMP_MAX] = {
	&memp_RAW_PCB,
	&memp_UDP_PCB
};

然后在通过memp_init()给每个类型分配内存池

这个memp_init()的代码如下:

void memp_init_pool(const struct memp_desc* desc) {
  int i;
  struct memp* memp;

  *desc->tab = NULL;
  memp = (struct memp*)LWIP_MEM_ALIGN(desc->base);
  /* create a linked list of memp elements */
  for (i = 0; i < desc->num; ++i) {
    memp->next = *desc->tab;
    *desc->tab = memp;
    memp = (struct memp*)(void*)((u8_t*)memp + MEMP_SIZE + desc->size);
  }
}

上面的函数就是将desc->numdesc->size大小的内存块串联到desc->tab上,通过初始化之后的结果如下:

pools

对于&memp_RAW_PCB的分配,是通过下面的宏实现的

#define LWIP_MEMPOOL(name,num,size,desc) LWIP_MEMPOOL_DECLARE(name,num,size,desc)
#include "lwip/priv/memp_std.h"

而LWIP_MEMPOOL_DECLARE的定义如下:

#define LWIP_MEMPOOL_DECLARE(name,num,size,desc) \
  LWIP_DECLARE_MEMORY_ALIGNED(memp_memory_ ## name ## _base, ((num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size)))); \
    \
  LWIP_MEMPOOL_DECLARE_STATS_INSTANCE(memp_stats_ ## name) \
    \
  static struct memp *memp_tab_ ## name; \
    \
  const struct memp_desc  = { \
    DECLARE_LWIP_MEMPOOL_DESC(desc) \
    LWIP_MEMPOOL_DECLARE_STATS_REFERENCE(memp_stats_ ## name) \
    LWIP_MEM_ALIGN_SIZE(size), \
    (num), \
    memp_memory_ ## name ## _base, \
    &memp_tab_ ## name \
  };

对于RAW_PCB

LWIP_MEMPOOL(RAW_PCB,MEMP_NUM_RAW_PCB,sizeof(struct raw_pcb),"RAW_PCB")

编程的结果:

LWIP_DECLARE_MEMORY_ALIGNED(memp_memory_RAW_PCB_base,
  ((MEMP_NUM_RAW_PCB) * (MEMP_SIZE + MEMP_ALIGN_SIZE(sizeof(struct raw_pcb)))));

LWIP_MEMPOOL_DECLARE_STATS_INSTANCE(memp_stats_RAW_PCB)

static struct memp *memp_tab_RAW_PCB;

const struct memp_desc memp_RAW_PCB =
{
    DECLARE_LWIP_MEMPOOL_DESC("RAW_PCB")
    LWIP_MEMPOOL_DECLARE_STATS_REFERENCE(memp_stats_RAW_PCB)
    LWIP_MEM_ALIGN_SIZE(sizeof(struct raw_pcb)),
    (MEMP_NUM_RAW_PCB),
    memp_memory_RAW_PCB_base,
    &memp_tab_RAW_PCB
};

最终的结果:

u8_t memp_memory_RAW_PCB_base[(((((MEMP_NUM_RAW_PCB) *
               (MEMP_SIZE + MEMP_ALIGN_SIZE(sizeof(struct raw_pcb)))))
                + MEM_ALIGNMENT - 1U))];

static struct memp *memp_tab_RAW_PCB;

const struct memp_desc memp_RAW_PCB ={
(((sizeof(struct raw_pcb)) + MEM_ALIGNMENT - 1U) & ~(MEM_ALIGNMENT-1U)),
    LWIP_MEM_ALIGN_SIZE(sizeof(struct raw_pcb)),
    (MEMP_NUM_RAW_PCB),
    memp_memory_RAW_PCB_base,
    &memp_tab_RAW_PCB
};

memp_malloc

分配指定类型的内存,因为不同类型内存的固定大小不同

void *memp_malloc(memp_t type) {
  void* memp;

  memp = do_memp_malloc_pool(memp_pools[type]);

  return memp;
}

分配内存的核心代码就是memp = *desc->tab,得到对应内存池中的第一块内存

static void * do_memp_malloc_pool(const struct memp_desc *desc) {
    struct memp *memp;
    SYS_ARCH_DECL_PROTECT(old_level);
	// 这就是分配的内存
	memp = *desc->tab;
    if (memp != NULL) {
		// 去除分配掉的内存
        *desc->tab = memp->next;
        SYS_ARCH_UNPROTECT(old_level);
        return ((u8_t *)memp + MEMP_SIZE);
    } else {
        SYS_ARCH_UNPROTECT(old_level);
    }
    return NULL;
}

MEMP_SIZE偏移的空间大小,因为内存块需要一些空间存储内存块相关的信息

memp_free

把内存放回到对应内存池链表的表首

void memp_free(memp_t type, void *mem) {
    if (mem == NULL) {
        return;
    }
    do_memp_free_pool(memp_pools[type], mem);
}

static void do_memp_free_pool(const struct memp_desc *desc, void *mem) {
	...
	memp->next = *desc->tab;
	*desc->tab = memp;
	...
}

动态内存堆

简介

  • 动态内存堆又可以分成
    • 标准C库提供的内存管理
    • lwip自己实现的管理
    • 通过 MEM_LIBC_MALLOC 选择
  • LwIP的pool可以由heap实现,反之亦然
    • 通过 MEM_USE_POOL 和 MEMP_MEM_MALLOC 选择

内存堆的数据结构

next和prev两个字段表示的是目的地址的偏移量,不是指向下一个内存块与上一个内存块的指针

struct mem {
  mem_size_t next;
  mem_size_t prev;
  /* 1: 内存块已经使用; 0: 未使用 */
  u8_t used;
};

/* 定义最小的内存块,它至少要能保存管理数据 */
#define MIN_SIZE     12

/* 定义堆内存的地址 */
/* 1. MEM_SIZE_ALIGNED: MEM_SIZE对齐后的堆内存大小 
   2. MEM_ALIGNEMENT: CPU 4字节对齐 
   3. ram_heap[]就是内核的内存堆空间*/
u8_t ram_heap[MEM_SIZE_ALIGNED + (2*SIZEOF_STRUCT_MEM) + MEM_ALIGNMENT];
#define LWIP_RAM_HEAP_POINTER ram_heap

/* 指向堆内存对齐后的起始地址 */
static u8_t *ram;

/* 堆内存的最后一块内存,总是未使用的 */
static struct mem *ram_end;

/* 指向最低地址的空闲块,用于快速查找 */
static struct mem *lfree;

5.3.3 堆内存的初始化

堆内存的初始化使用mem_init函数

void mem_init(void) {
  struct mem *mem;

  /* 1. 得到对齐后的堆内存首地址 */
  ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);
  /* 2. 在堆内存的首地址处放置一个mem管理块 */
  mem = (struct mem *)(void *)ram;
  /* 2.1 mem管理后面整个对齐了的堆内存 */
  mem->next = MEM_SIZE_ALIGNED;
  mem->prev = 0; // 前面没有内存
  mem->used = 0;
  /* 3. 在堆内存最后放置一个mem内存管理快 */
  ram_end = (struct mem *)(void *)&ram[MEM_SIZE_ALIGNED];
  /* 已使用,因为结束的地方是没有内存可分配的 */
  ram_end->used = 1;
  /* 指向自身,表示已经到了堆内存的结束处 */
  ram_end->next = MEM_SIZE_ALIGNED;
  ram_end->prev = MEM_SIZE_ALIGNED;

  /* 初始化最低地址指针指向 ram 堆内存的起始地址,因为当前只有一块整的内存 */
  lfree = (struct mem *)(void *)ram;

  MEM_STATS_AVAIL(avail, MEM_SIZE_ALIGNED);

  /* 初始化一个互斥锁 */
  if(sys_mutex_new(&mem_mutex) != ERR_OK) {
	LWIP_ASSERT("failed to create mem_mutex", 0);
  }
}
  • 初始化之后,内存被分配成两块
    • 整个内存块,大小为整个堆内存,可分配
    • 结束内存块,大小为0,不可分配
  • 系统运行中
    • lfree会不断改变,都指向最低空闲内存地址
    • ram_end不会改变

mem_init

mem_malloc 内存分配

  • mem_malloc()参数是用户指定大小的内存字节数,如果申请成功则返回内存块的地址,如果内存没有分配成功,则返回NULL
  • 该内存并未进行初始化,可能包含任意的随机数据
  • 内存堆是一个全局变量,在操作系统的环境中进行申请内存块是不安全的
    • 所以LwIP使用互斥量实现了对临界资源的保护
    • 在多个线程同时申请或者释放的时候,会因为互斥量的保护而产生延迟

分配后的内存如下:

heap

LwIP 的内存配置选项

  • MEM_LIBC_MALLOC
    • 是否使用C 标准库自带的内存分配策略
MEMP_MEM_MALLOC MEM_USE_POOLS 内存分配策略
0 0 LwIP中默认的宏定义,内存池与内存堆独立实现,互不相干。
0 1 内存堆的实现由内存池实现。
1 0 内存池的实现由内存堆实现。
1 1 不允许的方式

用内存池方式实现内存堆,需要将MEM_USE_POOLS与MEMP_USE_CUSTOM_POOLS定义为 1,并且宏定义MEMP_MEM_MALLOC必须为 0

还需要创建lwippools.h文件,增加内存池相关代码

LWIP_MALLOC_MEMPOOL_START
LWIP_MALLOC_MEMPOOL(20, 256)
LWIP_MALLOC_MEMPOOL(10, 512)
LWIP_MALLOC_MEMPOOL(5, 1512)
LWIP_MALLOC_MEMPOOL_END

设置好内存池的数量和容量,并且容量由小到大

系统将这些内存块放入内存池,在申请时根据最合适的大小选择内存块

6. 网络数据包

6.1 简介

TCP/IP是一个通讯的机制,它由多个协议组成,而每个协议其实都是对数据包的处理。LwIP要提高TCP/IP处理的效率,就是要加快每层数据处理的速度,减少数据在各层之间的传递。BSD采用mbuf来管理数据包,LwIP采用pbuf管理 数据包有如下特性:

  • 数据包的类型不定,可以是ARP数据,也可以是用户数据
  • 数据包所在的地方不定,可以是ROM也可以是RAM
  • 数据包大小不定

LwIP是如何管理数据包的呢?

6.2 分层

典型的TCP/IP是分层的,每一层有独立的模块进行处理;各模块之间通过数据结构传递数据。但是LwIP模糊了每层的接线,以提高数据包处理的效率。因为数据不需要在各层之间拷贝,所以节省了时间

6.3 线程模型

  • 线程模型是指LwIP的协议栈划分到多少个线程来实现
  • 模型1:典型的TCP/IP模型
    • 每个协议一个线程
    • 数据传递要拷贝
    • 协议切换需要线程切换
    • 嵌入式系统负担不起
  • 模型2:协议栈放在OS中
    • 用户线程通过OS提供的接口与协议栈交互
    • 提高了效率
  • 模型3:独立
    • LwIP内核与OS独立,自己作为一个独立的线程运行
    • 用户程序驻留在协议栈内部
    • 用户通过回调函数与LwIP交互(Raw API)
    • 用户通过OS提供的邮箱和信号量交互(NetConn API和Socket API)

6.4 pbuf结构体

pbuf就是协议栈中的数据包,LwIP在pbuf.c/h中实现了数据包的管理函数和数据结构

struct pbuf {
  /* 数据包通过链表管理起来,next指向链表的下一个pbuf
	 网络中一个数据包会很大,需要pbuf链表容纳 */
  struct pbuf *next;

  /* buf中指向真实数据的指针 */
  void *payload;

  /* For non-queue packet chains this is the invariant:
   * p->tot_len == p->len + (p->next? p->next->tot_len: 0)
   * 记录包含当前buf之后的数据长度 */
  u16_t tot_len;

  /* 当前buf的有效数据长度 */
  u16_t len;

  /** pbuf_type as u8_t instead of enum to save space */
  /***************************/
   pbuf的类型:
	  1. PBUF_RAM
	  2. PBUF_POOL
	  3. PBUF_REF
	  4. PBUF_ROM
   ***************************/
  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;
};

6.5 pbuf的类型

6.5.1 PBUF_RAM 类型的 buf

  • PBUF_RAM类型的buf空间是通过内存堆的方式分配的
    • 协议栈发送数据一般使用这种类型
    • LwIP根据需要的大小在堆中申请一块内存
    • 包含数据空间和pbuf数据结构区域
  • 内核申请这类型的pbuf时
    • 算上了协议首部的空间
    • 当然是根据协议栈不同层次需要的首部进行申请
    • 函数
      struct pbuf *pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type);
      

      它会根据length的长度,自动申请足够多的pbuf连成一个链表

  • 申请方式
      p = pbuf_alloc(PBUF_RAW, (u16_t)(req_len + 1), PBUF_RAM);
      p = pbuf_alloc(PBUF_TRANSPORT, 1472, PBUF_RAM);
    

pbuf

layer就是各个协议的头部 payload: 有效载荷

6.5.2 PBUF_POOL 类型的 buf

  • PBUF_POOL类型的pbuf,也包含pbuf管理块和数据块两个部分
  • 通过内存池的方式分配的
  • 分配速度块,网卡接收的数据都是通过PBUF_POOL类型的buf存储
  • 与PBUF_POOL相关的两个内存池
    • MEMP_PBUF内存池
      • 专门用于存放pbuf数据结构的内存池
        • 主要用于PBUF_ROM、PBUF_REF类型的pbuf
        • 大小为sizeof(struct pbuf)
        • 内存池中块的数量:MEMP_NUM_PBUF
    • MEMP_PBUF_POOL内存池
      • 包含pbuf结构与数据区域
      • 内存块的大小为PBUF_POOL_BUFSIZE
        • (TCP_MSS + 40 + 0 + 14)
        • 默认为590(536+40+0+14)字节
        • TCP_MSS=1460时
          • 块大小:1514(1460+40+0+14)
      • 块的数量:PBUF_POOL_SIZE
      • 当申请的内存很大时
        • pbuf_alloc会用memp_malloc申请多个pbuf
        • 再将这些pbuf通过链表的形式连接起组成pbuf链表上

memp

  • 只有第一个pbuf有layer字段
    • 协议头
  • tot_len是自己和后面的pbuf的中大小

6.5.3 PBUF_ROM和PBUF_REF类型的buf

  • 不包含数据区
    • PBUF_ROM的数据区在ROM中
    • PBUF_REF的数据区在RAM中
  • 申请
    • memp_malloc
    • 大小MEMP_PBUF
  • 分配到的pbuf内存示意图 rom_ref

buf:某个类型内存池的一个空闲内存块

数据包可以存储在任意类型的内存池中,也可以存储在混合类型的内存池中

pack

6.6 pbuf_alloc 数据包申请

数据包申请函数pbuf_alloc说明:

struct pbuf *pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type)
  • layer:协议层涉及到协议头
    • 传输层TCP:PBUF_TRANSPORT
    • 网络层IP:PBUF_IP
    • 链路层LINK: PBUF_LINK
    • 原始层RAW:PBUF_RAW
  • length:数据区大小
  • type:pbuf的类型
    • PBUF_REF
    • PBUF_ROM
    • PBUF_POOL
    • PBUF_RAM
  • pbuf_alloc函数
    • 分配一个给定类型的pbuf(可能是PUF_POOL类型的链表)
    • 为pbuf分配的实际内存由分配pbuf的层和请求的大小(来自于大小参数)决定。
  • 例子
      p = pbuf_alloc(PBUF_TRANSPORT, 1472, PBUF_RAM)
    
    • 根据协议层PBUF_TRANSPORT预留协议头空间
      • 传输层,内核需要预留54个字节空间
        • 以太网帧首部长度PBUF_LINK_HLEN(14字节)
        • IP数据报首部长度PBUF_IP_HLEN(20字节)
        • TCP首部长度PBUF_TRANSPORT_HLEN(20字节)
      • 当数据报往下层递交的时候
        • 其他层直接填充对应的协议首部即可
        • 无需对数据进行拷贝等操作
    • 数据区为1472字节
    • 分配一个PBUF_RAM类型的pbuf

具体实现:

struct pbuf * pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type) {
    struct pbuf *p;

	/* 根据layer的类型,预留出对应的协议首部空间offset */
  	switch (layer) {
  	case PBUF_TRANSPORT:
    	/* add room for transport (often TCP) layer header */
    	offset = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN + PBUF_IP_HLEN + PBUF_TRANSPORT_HLEN;
    	break;
  	case PBUF_IP:
  		/* add room for IP layer header */
  		offset = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN + PBUF_IP_HLEN;
  		break;
	case PBUF_LINK:
		...
	case PBUF_RAW_TX:
		...
	case PBUF_RAW:
		/* no offset (e.g. RX buffers or chain successors) */
		offset = 0;
		break;
	}

	/* 根据pbuf的类型, 初始化pbuf结构体的成员变量 */
  	switch (type) {
		/* pbuf引用现有的ROM有效载荷 */
		case PBUF_ROM:
		/* pbuf引用现有的(外部分配的)RAM有效载荷 */
		case PBUF_REF:
			alloc_rom_ref_pbuf();
			break;
		
        case PBUF_RAM:
            alloc_ram_pbuf();
            break;

        case PBUF_POOL:
            alloc_pool_pbuf();
            break;
	}

    return p;
}

6.6.1 从rom和ref分配数据包空间

void alloc_rom_ref_pbuf() {
    /* 只为pbuf结构分配内存 */
    p = (struct pbuf*)memp_malloc(MEMP_PBUF);
    /* caller must set this field properly, afterwards */
    /* 设置后面的字段 */
    p->payload = NULL;
    /* 外部的存储区,不用lwip自己分配空间 */
    p->len = p->tot_len = length;
    p->next = NULL;
    p->type = type;
}

6.6.2 从ram区域分配数据包空间

void alloc_ram_pbuf(void) {
    /* 根据字节对齐的规则给bpuf结构体和协议头分配空间,再给数据区分配空间 */
    mem_size_t alloc_len = LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF + offset) + LWIP_MEM_ALIGN_SIZE(length);

    /* pbuf被分配到RAM中,要为其分配内存。*/
    p = (struct pbuf*)mem_malloc(alloc_len);
    /* 设置pbuf的内部结构 */
    /* 真正放置用户数据的地址 */
    p->payload = LWIP_MEM_ALIGN((void*)((u8_t*)p + SIZEOF_STRUCT_PBUF + offset));
    p->len = p->tot_len = length;
    p->next = NULL;
    p->type = type;
}

6.6.3 从pool区域分配数据包空间

  • 通过memp_malloc从pbuf内存池中分配所需长度的pbuf块,并串联成一个链表
void alloc_pool_pbuf(void) {
  /* 从PBUF内存池中取出第一块内存,给p */
  p = (struct pbuf*)memp_malloc(MEMP_PBUF_POOL);
  p->type = type;
  p->next = NULL;

  /* 使有效载荷指针指向pbuf数据包空间去除struct pbuf和offset的后面 */
  p->payload = LWIP_MEM_ALIGN((void*)((u8_t*)p + (SIZEOF_STRUCT_PBUF + offset)));
  /* pbuf链的总长度为请求的大小 */
  p->tot_len = length;
    /* 设置链中第一个pbuf的长度 */
    /* PBUF_POOL_BUFSIZE_ALIGNED = PBUF_POOL_BUFSIZE = 1500 */
    /* 它是pbuf内存池中每个pbuf的大小 */
  p->len = min(length, PBUF_POOL_BUFSIZE_ALIGNED - LWIP_MEM_ALIGN_SIZE(offset));
   /* 设置引用计数 */
  p->ref = 1;

    /* 现在分配pbuf链的尾部 */

    /*记住第一个pbuf,以便在下一次迭代中进行链接 */
  r = p;
  /* 待分配的剩余长度 */
  rem_len = length - p->len;
    /* 还有没有剩余的pbufs需要分配?*/
  while (rem_len > 0) {
    q = (struct pbuf*)memp_malloc(MEMP_PBUF_POOL);
    q->type = type;
    q->flags = 0;
    q->next = NULL;
    /*使以前的pbuf指向这个pbuf */
    r->next = q;
     /* 设置这个pbuf和下一个pbuf的总长度 */
    q->tot_len = (u16_t)rem_len;
    /*这个pbuf的长度是池子的大小,除非是较小尺寸的尾巴 */
    q->len = min((u16_t)rem_len, PBUF_POOL_BUFSIZE_ALIGNED);
    q->payload = (void*)((u8_t*)q + SIZEOF_STRUCT_PBUF);
    q->ref = 1;
    /* 计算要分配的剩余长度 */
    rem_len -= q->len;
    /*记住这个pbuf,以便在下一次迭代中进行链接 */
    r = q;
  }
  /* end of chain */
  /*r->next = NULL;*/
}

6.7 pbuf_free数据包释放

  • pbuf的释放函数pbuf_free()会将pbuf链表中的pbuf->ref-1,当它为0时就会释放
  • pbuf_free()的参数应该是pbuf链表表首指针

6.8 其他pbuf函数

6.8.1 pbuf_realloc

  • 在pbuf链表尾部释放一定空间
  • 将数据包的长度修改为给定值

6.8.2 pbuf_header

调整pbuf->payload的指针,方便各层对数据包的首部操作

6.8.3 pbuf数据拷贝

  • pbuf_take:从pbuf数据区拷贝
  • pbuf_copy:从pbuf数据区拷贝到PBUF_RAM的pbuf中
  • pbuf_chain:将两个pbuf连接在一个链表
  • pbuf_ref:ref+1

6.9 网卡中的pbuf

6.9.1 网卡发送数据

  • 网卡发送数据的函数 low_level_output
  • 数据发送的过程
    • 用户把数据发送给LwIP内核
    • LwIP内核经过TCP、IP等等层层封装数据到pbuf数据包中
    • 如果数据多,用pbuf链表存储
    • 通过一个以太网帧发出去
  • low_level_output
    • 这个函数应该完成数据包的实际传输
    • 数据包被包含在传递给该函数的pbuf中
      • 这个 pbuf 可能是链式的
    • netif
      • 该ethernetif的lwip网络接口结构
    • p
      • 要发送的MAC数据包
      • 例如,包括MAC地址和类型的IP数据包

裸机函数:

static err_t low_level_output(struct netif* netif, struct pbuf* p) {
    buffer = (uint8_t*)(enet_desc_information_get(dma_current_txdesc, TXDESC_BUFFER_1_ADDR));

    /* 从pbufs拷贝数据到驱动的buf中 */
    for (q = p; q != NULL; q = q->next) {
        memcpy((uint8_t*)&buffer[framelength], q->payload, q->len);
        framelength = framelength + q->len;
    }

    /* 通过驱动的DMA把数据发送出去 */
    ENET_NOCOPY_FRAME_TRANSMIT(framelength);
}

有OS的函数:

static err_t low_level_output(struct netif* netif, struct pbuf* p) {
    struct pbuf* q;
    uint8_t* buffer;
    uint16_t framelength = 0;
    ErrStatus reval = ERROR;

    SYS_ARCH_DECL_PROTECT(sr);

    if (xSemaphoreTake(gtx_semaphore, LOWLEVEL_OUTPUT_WAITING_TIME)) {
        SYS_ARCH_PROTECT(sr);

        while ((uint32_t)RESET != (dma_current_txdesc->status & ENET_TDES0_DAV)) { }
        buffer = (uint8_t*)(enet_desc_information_get(dma_current_txdesc, TXDESC_BUFFER_1_ADDR));

        for (q = p; q != NULL; q = q->next) {
            memcpy((uint8_t*)&buffer[framelength], q->payload, q->len);
            framelength = framelength + q->len;
        }
        /* 给DMA的传输描述符 */
        reval = ENET_NOCOPY_FRAME_TRANSMIT(framelength);

        SYS_ARCH_UNPROTECT(sr);
        /* 给出信号并退出 */
        xSemaphoreGive(gtx_semaphore);
    }

    if (SUCCESS == reval) {
        return ERR_OK;
    } else {
        while (1) {
        }
    }
}

6.9.2 网卡接收数据

  • low_level_input
    • 从网卡接收一个数据包
    • 将数据包封装在pbuf中递交给上层
  • 裸机
      static struct pbuf* low_level_input(struct netif* netif) {
          len = enet_desc_information_get(dma_current_rxdesc, RXDESC_FRAME_LENGTH);
          buffer = (uint8_t*)(enet_desc_information_get(dma_current_rxdesc, RXDESC_BUFFER_1_ADDR));
          /* 从LwIP的内存池分配一个pbuf链表 */
          p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL);
          /* 拷贝接收数据帧到pbuf链表 */
          if (p != NULL) {
              for (q = p; q != NULL; q = q->next) {
                  memcpy((uint8_t*)q->payload, (u8_t*)&buffer[l], q->len);
                  l = l + q->len;
              }
          }
          /* 设置DMA,准备下一帧数据的接收 */
          ENET_NOCOPY_FRAME_RECEIVE();
      }
    
  • 有OS
      static struct pbuf* low_level_input(struct netif* netif) {
          struct pbuf* p = NULL, * q;
          uint32_t l = 0;
          u16_t len;
          uint8_t* buffer;
          /* 获得数据包的大小,并将其放入 "len "变量。*/
          len = enet_desc_information_get(dma_current_rxdesc, RXDESC_FRAME_LENGTH);
          buffer = (uint8_t*)(enet_desc_information_get(dma_current_rxdesc, RXDESC_BUFFER_1_ADDR));
    
          if (len > 0) {
              /* 从Lwip内存池中分配一个pbuf链的pbufs */
              p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL);
          }
    
          if (p != NULL) {
              for (q = p; q != NULL; q = q->next) {
                  memcpy((uint8_t*)q->payload, (u8_t*)&buffer[l], q->len);
                  l = l + q->len;
              }
          }
    
          ENET_NOCOPY_FRAME_RECEIVE();
    
          return p;
      }
    

6.9.3 ethernetif_input函数

  • ethernetif_input
    • 它调用low_level_input,获取网卡数据包
      • low_level_input只是从网卡接收数据,没有把数据交给上层
    • 有OS时
      • ethernetif_input作为一个线程,由OS周期调用
    • 裸机
      • 自己周期调用
  • ethernetif_input被周期性的调用,负责把数据发送给上层
    • 通过网卡的netif->input,把pbuf交给上层
  • 裸机
      err_t ethernetif_input(struct netif* netif) {
    
          /* 把接收数据包放入新的pbuf中 */
          p = low_level_input(netif);
          /* LwIP栈的入口点 */
          err = netif->input(p, netif);
          return err;
      }
    

    这里netif->inputnetif_add()函数中赋值

    netif_add(&g_mynetif, &ipaddr, &netmask, &gw, NULL, &ethernetif_init, &ethernet_input);
      netif->input = ethernet_input;
    
    • ehternet_input
      • ip4_input(p, netif)
        • ip4_debug_print(p)
        • ip_data.current_input_netif = inp;
        • 根据数据类型发送给上层
          • udp_input(p, inp)
          • tcp_input(p, inp);
          • icmp_input(p, inp);
  • 有OS
      static struct netif* low_netif = NULL;
      void ethernetif_input(void* pvParameters) {
          struct pbuf* p;
          SYS_ARCH_DECL_PROTECT(sr);
    
          for (;; ) {
              if (pdTRUE == xSemaphoreTake(grx_semaphore, LOWLEVEL_INPUT_WAITING_TIME)) {
              TRY_GET_NEXT_FRAME:
                  SYS_ARCH_PROTECT(sr);
                  p = low_level_input(low_netif);
                  SYS_ARCH_UNPROTECT(sr);
    
                  if (p != NULL) {
                      if (ERR_OK != low_netif->input(p, low_netif)) {
                          pbuf_free(p);
                      } else {
                          goto TRY_GET_NEXT_FRAME;
                      }
                  }
              }
          }
      }
    

    这里的low_netif在low_level_init()函数中赋值

      static void low_level_init(struct netif* netif) {
          ...
          low_netif = netif;
          ...
      }
    

    low_level_init()由ethernetif_init()调用

      err_t ethernetif_init(struct netif* netif) {
        netif->name[0] = IFNAME0;
        netif->name[1] = IFNAME1;
    
        netif->output = etharp_output;
        netif->linkoutput = low_level_output;
    
        /* initialize the hardware */
        low_level_init(netif);
      }
    

    而ethernetif_init()由net_add()调用

      netif_add(&netif, &ipaddr, &netmask, &gw, NULL, &ethernetif_init, &tcpip_input) {
        ...
        init = ethernetif_init;
        if (init(netif) != ERR_OK) {
          return NULL;
        }
      }
    

7 无OS的移植

7.1 LwIP移植文件

  • LwIP源代码中的src目录
  • LwIP的功能目录
    • LwIP/app
    • LwIP/core
    • LwIP/netif
    • LwIP/arch:底层接口相关文件
  • 头文件
    • LwIP/arch/lwipopts.h
      • 配置LwIP参数
    • LwIP/arch/cc.h
      • 处理器相关的变量类型
      • 数据结构
      • 字节对齐宏
    • LwIP/arch/perf.h
      • 性能
      • 系统统计与测量

7.2 移植网卡驱动

  • 网卡驱动放在ethernetif.c文件中
  • 模板:contrib-2.1.0\examples\ethernetif
  • 放到LwIP/arch目录下

7.3 LwIP的时基

  • LwIP内核与OS一样,也是通过时钟驱动的。有了时钟,LwIP就可以处理各种定时问题
  • LwIP通过sys_now()来获取时钟,用户需要实现它
  • cortex-M有sysTicks()这个接口,那么我们把这个CPU的时钟给sys_now()即可
  • 如果sysTicks()不是1ms,需要转换,让sys_now()是1ms变化一次

7.4 协议栈的初始化

  • 初始化协议栈 lwip_stack_init()
  • 配置主机ip地址、子网掩码、网关
    • IP4_ADDR()
    • ip地址要和路由器在同一子网
    • 为了让路由器可以转发数据,网关就填路由器地址
  • 挂载网卡
    • netif_add()
    • netif_set_default()

7.5 获取数据包

  • 用开发板获取数据包的方式有中断和轮询
    • 轮询方式
      • main中周期查询
      • 交给上层协议处理
    • 中断方式
      • 网卡收到数据包
      • 触发中断
      • CPU去处理
  • 通过网卡初始化的时候设置ETH_RXINTERRUPT_MODEETH_RXPOLLING_MODE来实现

轮询方式

int main(void) {
  // 板级外设初始化
  BspInit();
  // LwIP协议栈初始化
  LwIP_Init();

  while(1) {
	// 调用网卡接收函数
	ethernetif_input(&gnetif);

	// 处理LwIP中定时事件
	sys_check_timeoutxs();
  }
}
  • 通过PC来ping开发板,可以ping通
  • 通过arp -a显示主机的arp列表
    • 开发板的IP地址和MAC地址都显示其中

中断方式

中断是当网卡接收到一个数据包,就触发中断,进入相应的中断处理程序

int main(void) {
  BspInit();
  LwIP_Init();

  while(1) {
	if (flag) {
	  flag = 0;
	  ethernetif_input(&gnetif);
	}
	sys_check_timeouts();
  }
}

void ETH_IRQHandler(void) {
  HAL_ETH_IRQHandler(&heth);
}

void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth) {
  flag = 1;
}

有OS的移植

  • 在OS下, LwIP以独立的线程运行。用户通过信号量、互斥量、消息队列(邮箱)和LwIP通信。
  • 为了让LwIP可以适应各种OS,所以它提供了与OS相关的接口,由用户自己去移植

在LwIP中添加OS

  1. 在功能中添加FreeRTOS
    • 新建FreeRTOS/srcFreeRTOS/port
    • FreeRTOS/src放置源文件
    • port放置MemMang/RVDS/ARM_CM4文件夹
    • MemMang/heap_4.c是FreeRTOS的内存管理源码
  2. 头文件
    • FreeRTOS/include
    • FreeRTOS/port/RVDS/ARM_CM4
    • FreeRTOSConfig.h
  3. FreeRTOS的心跳时钟
    • 定时功能
    • 任务调度
    • SysTick中断服务就是心跳时钟

头文件的配置

  • lwipopts.h
    • NO_SYS = 0表明使用OS
    • LWIP_NETCONN = 1和LWIP_SOCKET=1表明使用NetConn和Socket两种API
  • sys_arch.h/c
    • LwIP移植的核心就是用户根据使用的OS为LwIP内核提供相应的功能
      • 这些功能都写在sys_arch.c文件中
    • 比如邮箱(消息队列)、互斥量、信号量
    • 这些IPC通信机制是LwIP和用户程序通讯的功能保障
  • 邮箱
    • 用户与协议栈内部是通过邮箱通讯的
      • LwIP的邮箱本质是指向数据的指针
    • 用户通过API把指针给内核
    • 内核处理数据
      • 也通过邮箱把指向数据的指针返回给用户
  • LwIP通过邮箱处理数据的过程
    • LwIP初始化之后会创建一个tcpip_thread的线程,这就是LwIP协议栈内核
    • 它会阻塞在邮箱上
    • 当网卡有数据或者应用程序有数据后(邮箱上有数据),LwIP处理并返回数据,之后再阻塞
  • LwIP信号量与互斥量
    • 给LwIP内核提供同步与互斥的功能
    • 用户调用LwIP上层API发送数据
      • API把数据给内核
      • API申请信号量
        • 但信号量没有多的,因此阻塞用户线程
      • 内核通过网卡发送完数据
      • 内核释放信号量
      • 用户API获取信号量,继续运行用户线程

网卡底层的编写

  • 有OS的情况下,我们把网卡的接收函数作为一个单独的线程运行。
  • 当网卡接到数据后,就交给内核线程去处理。
  • 网卡在初始化的时候就创建这个网卡接收线程

  • 网络数据接收线程与网卡接收中断的同步
    • 网卡接收到数据产生中断
    • 中断处理程序只是释放一个信号量
    • 网络数据接收线程获取到信号,开始处理数据
    • 处理完成后,继续阻塞在信号量上
  • 网卡初始化程序
    • 创建网络数据接收线程
    • 创建信号标

协议栈的初始化

  • 移植涉及方面:
    • 底层
    • OS接口
    • 协议栈初始化
  • 协议栈初始化
    • 初始化网卡
    • 创建tcpip_thread线程
  • tcpip_init
    • lwip_init 初始化内核
    • 创建tcpip_mbox邮箱
      • 接收底层和上层传递的消息
    • 创建tcpip_thread线程
      • LwIP作为独立线程运行

9 LwIP究竟是什么

9.1:网卡接收数据的流程

tcpip

  • 网卡eth接收到数据后产生一个中断
    • 我们在enet控制配置时
      // 启动网卡中断
      nvic_irq_enable(ENET_IRQn, 2, 0);
      // 启动了DMA中断
      enet_interrupt_enable(ENET_DMA_INT_NIE);
      enet_interrupt_enable(ENET_DMA_INT_RIE);
      
    • 当网卡接收到数据时,在中断服务程序ENET_IRQHandler中释放接收互斥锁,唤醒LwIP任务
      if (SET == enet_interrupt_flag_get(ENET_DMA_INT_FLAG_RS)) {
          /* 释放接收互斥锁,唤醒LwIP任务 */
          xSemaphoreGiveFromISR(grx_semaphore, &xHigherPriorityTaskWoken);
      }
      
  • 中断释放信号标
  • 阻塞在信号标上的接收数据线程ethernetif_input将数据封装成消息msg,并投递到mbox邮箱中
  • LwIP内核线程接收消息,通过ethernet_input对消息进行解析
    • 如果是ARP消息,更新ARP列表
    • 如果是IP消息,交给IP层处理

9.2:内核超时处理

  • 超时处理是TCP/IP协议中的重要部分
    • LwIP的应用
      • ARP缓存表刷新
      • IP分包的超时重传
      • 连接超时
    • LwIP相关文件
      • timeouts.c/h
    • LwIP使用软件提供超时机制,只需要用户给一个准确的时基

9.2.1:sys_timeo结构体与超时链表

  • LwIP将所有的超时事件都挂在超时链表上
  • 超时事件由sys_timeo结构体来描述
  • 这个结构体记录了
    • 下一个超时事件的指针next
    • 超时时间
    • 超时后的执行函数
    • 函数参数

9.2.2:注册超时事件

timeout

  • 通过sys_timeout()函数来注册超时事件
  • 根据超时时间的升序,插入新注册的事件
  • 对于循环超时事件
    • 第一次通过lwip_cyclic_timer()注册到循环超时链表中
    • 每次超时处理后,再调用sys_timeout_abs()重新注册

9.2.3:超时检查

  • LwIP的超时处理机制
    • sys_check_timeout()函数是裸机时使用的超时处理函数
      • 它会检查超时链表上的第1个timeo结构
        • 没有超时,直接退出
        • 超时
          • 调用超时处理函数
          • 删除此事件
          • 遍历后面的事件
          • 直到没有遇到超时
    • tcpip_timeouts_mbox_fetch()函数
      • 等待消息到来
      • 等待中如果有超时,调用sys_check_timeouts()函数处理超时
        • 处理完后,再等待下一个超时时间

9.3:tcpip_thread线程

  • tcpip_thread线程就是对接收数据的处理
    • 等待消息
      • 没有消息continue,继续等待
      • 有消息,调用tcpip_thread_handle_msg处理消息
  • tcpip_thread_handle_msg
    • 根据消息的类型进行相应处理
    • TCPIP_MSG_API,执行API:msg->msg.api_msg.function
    • TCPIP_MSG_INPKT,交给ARP层处理msg->msg.inp.input_fn
    • ……

9.4:LwIP中的消息

数据包消息和API消息是常用的两种消息类型

9.4.1:消息结构

  • LwIP的消息有多种类型,它通过tcpip_msg_type来描述
  • LwIP使用tcpip_msg来描述消息
    • tcpip_msg_type表示消息类型
    • msg共用体表示消息的具体内容
  • tcpip_thread根据消息的类型进行不同的处理

9.4.2:数据包消息

  • LwIP有多种消息,因此有对应的产生该类型消息的函数
  • 函数通过tcpip_input产生消息并将之投递到邮箱tcpip_mbox中
  • tcpip_thread会从邮箱中获取消息进行处理
    • 内部是 tcpip_inpkt
    • 最终调用ethernet_input处理数据包
  • OS与消息处理线程隔开
    • 接收线程与OS线程互不干扰

input

9.4.3:API消息

API消息的结构体用api_msg来表示


9.5:揭开LwIP神秘的面纱

  1. PHY芯片的初始化
  2. 以太网控制器的初始化
  3. LwIP的初始化
  4. 自己的应用程序