本文是对LwIP的分析记录
内存管理
内存分配策略
- C标准库自带的内存分配策略
- 动态内存池
- 每次只能申请几种固定大小的内存
- 为TCP、IP首申请对应大小的内存
- 系统启动时,初始化几类固定大小的内存块链表
- 动态内存堆
- 宏开关
#define MEM_LIBC_MALLOC 0 #define MEMP_MEM_MALLOC 0 #define MEM_USE_POOLS 0
- 动态内存堆可以和C标准库内存分配共用
- 可同时打开
- 动态内存池不可和C标准库共用
- 一般使用动态内存池和动态内存堆
动态内存池
- 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.h
和opt.h
打开了LWIP_RAW
和LWIP_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->num
个desc->size
大小的内存块串联到desc->tab
上,通过初始化之后的结果如下:
对于&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_malloc 内存分配
- mem_malloc()参数是用户指定大小的内存字节数,如果申请成功则返回内存块的地址,如果内存没有分配成功,则返回NULL
- 该内存并未进行初始化,可能包含任意的随机数据
- 内存堆是一个全局变量,在操作系统的环境中进行申请内存块是不安全的
- 所以LwIP使用互斥量实现了对临界资源的保护
- 在多个线程同时申请或者释放的时候,会因为互斥量的保护而产生延迟
分配后的内存如下:
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);
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
- 专门用于存放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内存池
- 只有第一个pbuf有layer字段
- 协议头
- tot_len是自己和后面的pbuf的中大小
6.5.3 PBUF_ROM和PBUF_REF类型的buf
- 不包含数据区
- PBUF_ROM的数据区在ROM中
- PBUF_REF的数据区在RAM中
- 申请
- memp_malloc
- 大小MEMP_PBUF
- 分配到的pbuf内存示意图
buf:某个类型内存池的一个空闲内存块
数据包可以存储在任意类型的内存池中,也可以存储在混合类型的内存池中
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字节)
- 当数据报往下层递交的时候
- 其他层直接填充对应的协议首部即可
- 无需对数据进行拷贝等操作
- 传输层,内核需要预留54个字节空间
- 数据区为1472字节
- 分配一个PBUF_RAM类型的pbuf
- 根据协议层PBUF_TRANSPORT预留协议头空间
具体实现:
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周期调用
- 裸机
- 自己周期调用
- 它调用low_level_input,获取网卡数据包
- 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->input
在netif_add()
函数中赋值netif_add(&g_mynetif, &ipaddr, &netmask, &gw, NULL, ðernetif_init, ðernet_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);
- ip4_input(p, netif)
- ehternet_input
- 有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, ðernetif_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_MODE
和ETH_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
- 在功能中添加FreeRTOS
- 新建
FreeRTOS/src
和FreeRTOS/port
- FreeRTOS/src放置源文件
- port放置
MemMang/
和RVDS/ARM_CM4
文件夹
MemMang/heap_4.c
是FreeRTOS的内存管理源码
- 新建
- 头文件
FreeRTOS/include
FreeRTOS/port/RVDS/ARM_CM4
FreeRTOSConfig.h
- 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移植的核心就是用户根据使用的OS为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:网卡接收数据的流程
- 网卡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); }
- 我们在enet控制配置时
- 中断释放信号标
- 阻塞在信号标上的接收数据线程ethernetif_input将数据封装成消息msg,并投递到mbox邮箱中
- LwIP内核线程接收消息,通过ethernet_input对消息进行解析
- 如果是ARP消息,更新ARP列表
- 如果是IP消息,交给IP层处理
9.2:内核超时处理
- 超时处理是TCP/IP协议中的重要部分
- LwIP的应用
- ARP缓存表刷新
- IP分包的超时重传
- 连接超时
- LwIP相关文件
- timeouts.c/h
- LwIP使用软件提供超时机制,只需要用户给一个准确的时基
- LwIP的应用
9.2.1:sys_timeo结构体与超时链表
- LwIP将所有的超时事件都挂在超时链表上
- 超时事件由sys_timeo结构体来描述
- 这个结构体记录了
- 下一个超时事件的指针next
- 超时时间
- 超时后的执行函数
- 函数参数
9.2.2:注册超时事件
- 通过sys_timeout()函数来注册超时事件
- 根据超时时间的升序,插入新注册的事件
- 对于循环超时事件
- 第一次通过lwip_cyclic_timer()注册到循环超时链表中
- 每次超时处理后,再调用sys_timeout_abs()重新注册
9.2.3:超时检查
- LwIP的超时处理机制
- sys_check_timeout()函数是裸机时使用的超时处理函数
- 它会检查超时链表上的第1个timeo结构
- 没有超时,直接退出
- 超时
- 调用超时处理函数
- 删除此事件
- 遍历后面的事件
- 直到没有遇到超时
- 它会检查超时链表上的第1个timeo结构
- tcpip_timeouts_mbox_fetch()函数
- 等待消息到来
- 等待中如果有超时,调用sys_check_timeouts()函数处理超时
- 处理完后,再等待下一个超时时间
- sys_check_timeout()函数是裸机时使用的超时处理函数
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线程互不干扰
9.4.3:API消息
API消息的结构体用api_msg来表示
9.5:揭开LwIP神秘的面纱
- PHY芯片的初始化
- 以太网控制器的初始化
- LwIP的初始化
- 自己的应用程序