Lwip 内存管理

Lwip 用它自己的动态内存分配方式替代了标准C的malloc(); 它可以使用MEM_POOL的方式分配内存,即开辟几个不同尺寸的缓冲池,给数据分配合适的缓冲区来存放,就类似于使用数组的方式,可能会比较浪费空间,但是可以有效避免内存碎片产生, 这在第二部分讲。两种方式分别在mem.c memp.c里面实现,下面先总结一般的内存堆分配方式。 (一) 内存堆动态分配方式 首先是内存对齐,这里用到的MCU是按4字节对齐的。这一语句功能是将addr 调整成最靠近addr的且能被4整除的值,其实就是(addr+3)&(~(u32)(0x03)),+3之后有余数的自然就进位了然后再舍掉余数。其实也可以用 addr = (addr&0x03)?(addr+1):addr; 这样应该也可以,更容易理解一些,但是没有上面的简便。 这里申请了一个内存堆ram_heap, 大小包括整个MEM_SIZE + 两个STRUCT_MEM的大小 + 4, 假设申请了MEM_SIZE 为8k。 内存初始化。ram 指向了ram_heap开头, 并且在开头初始化了一个struct mem。 ram_end 指向了MEM_SIZE之后的第一个STRUCT_MEM, 这里不知道是不是bug,我觉得应该指向&ram[MEM_SIZE_ALIGNED+SIZEOF_STRUCT_MEM]; 没时间了,以后再慢慢研究。 lfree最开始也指向了开头。 ->next 等这些参数都是相对地址,相对ram的,方便之后用数组的方式访问。 内存分配函数。首先对齐size,并且size不能太小或者太大,太小容易产生过多的碎片导致后期内存不足。 然后搜索heap,找到适合的块。lfree指向未分配的内存位置,刚开始它是在开头。 ptr=lfree-ram 即是lfree相对ram的位置,即已经用掉的内存大小。 ptr mem 先指向lfree所指的位置,当这块内存未被使用并且剩下的内存大于size时,…略复杂,待续… (二) 内存池的动态分配 内存池顾名思义就是一个个划分好的池子,不像内存堆一样东西乱堆。 pbuf是协议栈里最常用的数据类型,它有多种类型的存储方式,如下: 我们从pbuf入手研究一下MEM_POOL内存池的编写方式。 pbuf_alloc(PBUF_POOL)调用的是: p = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL); 我们来看看MEMP_PBUF_POOL是如何声明的: memp_std.h里定义了各种类型POOL. memp.c 定义了 几个数组来保存每个pool的元素大小和数量。定义一个memp_memory[]内存块保存所有的pool。 […]

methods of handling multiple connections as lwip

这里分享一些一个服务handle多client连接的经验。 (1) 多线程/进程方法 每accept并创建一个新连接之后,就create一个task来处理这个连接的事务。linux使用fork()来创建分支线程。freertos 不支持fork方法,只能创建新的线程来管理连接。此方法内存开销较大。 (2) non-blocking socket 和 select 此方法由单线程处理并发事务,即最原始的轮询方式。在系统不支持fork()的情况下经常用到。首先,accpet/recv函数都必须的non-blocking的,需立即返回,否则其它任务就得不到轮询。这种模式在跑单片机裸机程序时经常是这样的,另外在labview里面也经常是这样的轮询的方式。这个例程可以在lwip 1.4.1 contrib下app demo:chargen 里可以学习到。这种方式会占用很多cpu资源,这样的任务优先级要放低,并设置一定的轮询间隔(类比在labview里,while循环通常会插入一个等待时间)。否则 freertos 中低优先级任务就得不到运行,同优先级的任务也只能分时间片轮流运行。 这里简单总结下 select方法。 协议栈内核在每次有accept 或 recv事件到达后都会调用 event_callback(); 增加一个事件记录。在上层接收后都会减掉一个记录。select 函数通过查询socket的对应event来置位fd_set对应的比特位。本质就类似查询一个计数信号量。 (3) 使用 raw api, 基于事件触发(基于回调)方式。 此方式适合编写只进行简单处理的应用,每次接收到包就会调用相应的回调函数。多连接是由协议栈的active_pcb_lists直接管理,每个连接都创建属于自己的state argument。如果需要运算量比较大占用时间较长的服务则不适合此方式,会影响的协议栈对其它服务的响应速度。 后记,从事务的角度来看,一个tcp连接代表一个session,一句udp 请求也代表一个session。一个session从接收到返回就是一条事务线。若不能一气呵成,则就必须用一条线程去管理它,或者说维护一个工作现场,等待它的后台任务完成再回来应答这个事务。一条线程就对应一个事务现场,多任务操作系统就是在不同的工作现场的中交替工作,模拟多个人工作的情况,每一个工作现场都保存的对应工作事务的进展情况和所有需要用到的工具。 或者还有一种做法,就是一件事做完一部分后丢给后台,让它处理完之后调用我给它的办法帮我把剩下的事处理完,节省了任务之间信息传递和任务切换带来的开销。 其实一个session对应一个任务确实有点浪费,我想一般handle几百上千万个连接的服务应该不会这样做的,每个连接只需将它当前的状态变量连同数据包传给每个部门来做就可以了,毕竟每个连接的状态的数据量不是很多,没必要开辟一个专门的任务去Handle.

Lwip 里的信号量、邮箱、线程

这里简单分析总结一下Lwip sys_arch.c 中的信号量、邮箱、进程相关的内容。系统用的是FreeRTOS。 这里创建了两个全局结构体数组来管理semaphores and mailboxes. 有个不明白的地方是,结构体里的buffer是用来作什么的? 这里的semaphore其实用的是长度为1的queue来实现。mbox是长度为size的queue,其中消息对象类型都是void *,由于freertos在把消息送入队列和提取出队列都是复制的方式(应该是类似ringbuf的操作,没深入研究),所以这里为了提高效率,把对象封装一个消息结构体,只传递指针。在作消息传递时要特别注意内存的分配和释放,以免造成内存泄漏。 这里还要总结的一点是,类似于这种xxx_new, xxx_alloc的函数,用来分配对向内存的函数,传递进去的是指针的地址,因为要将指针指向新分配的内存,所以传进去的为指针的地址。 如这样调用: Lwip 的Thread是用FreeRTOS的xTaskCreate()来实现的。这里有个问题, TI的sys_arch.c里的sys_thread_new的实现,又用mem_malloc()分配了栈内存,是不是多此一举了,因为xTaskCreate里会为这个任务分配栈内存。这里似乎没有任何用处。

LwIP TCP Layer Simple analysis

LwIP Version : 1.4.1; 这里分享一些阅读LwIP协议栈的心得。 有个经验: 源代码才是最权威的资料。 若想用raw api编程,可以参考httpd_raw的代码。若涉及进程间通讯,请参考netconn接口,避免在其它进程内调用非Thead-Safe的函数。 下面说说对一些函数及过程的认识。 (1) xxx_accept(void *arg, struct tcp_pcb *pcb, err_t err); 这个函数的参数注意点: 回调原型:pcb->accept(pcb->callback_arg, pcb, err); @*arg: 这里协议栈传递过来的是lpcb,即监听pcb,不要认为和第二个参数相同。因为在tcp_listen_input()里,当有新连接请求到达时会新建一个npcb并将其加入到tcp_active_list里,在初始化npcb时,继承的是lpcb的callback_arg, 而这个callback_arg在新建监听链接时将其赋为lpcb。 @*pcb: 这里是传递进新创建的pcb。在创建APP过程中,需给其分配新的argument, 指定xxx_recv(),xxx_err(), xxx_poll(), xxx_sent()等函数。 (2) struct tcp_pcb *tcp_listen( struct tcp_pcb *pcb); 这里传入了一个original pcb,返回一个lpcb,因为listening状态的pcb 只需包含更少的信息。函数内新分配了一个 lpcb, 拷贝必要信息后,将original pcb释放。 对于SO_REUSE选项,在此函数内对比已经在监听List 中的pcb, 允许本地端口号相同,但本地IP号不同的连接可再次监听。 (3) 服务端口监听过程分析 TCP_LISTEN_BACKLOG 选项 此选项使能 TCP 对监听列表允许监听的数量控制。在tcp_listen_input()函数内, 当收到SYN帧时,此处对比并限制已经pending 状态的数量,即未被上层accept的连接数量。接下来新建立一个npcb, 并加入active_pcbs […]