这篇文章本应该在4月就写好的,但是博客评论系统一直没有搭建好,走了很多弯路,现在好了,delay这么久,终于要要补过来了。自建博客:金宝的博客
该文章完全原创,除通用、广泛的知识点外,均为个人总结,如需转载还望备注出处,同时如有错误还请指出,虚心接受。
一、简介
1. 题外话
以这篇文章为第一篇技术文章,一是萌生写博客的契机是换工作,另外就是这篇文章是我在怿星解决的最后一个bug。
问题来源是,跑在基于LwIP+FreeRTOS环境的DoIP,在反复初始化/反初始化时几次之后就会失败了。年初由于任务紧张,检查了下初始化和反初始化函数的流程,改掉了几处可能会出现问题的地方,问题依旧。但是同样的上层处理代码,在windows和linux环境下是没问题的,基本怀疑是LwIP某处不完善引起。一直拖到要离职,终于在离开的最后一天解决了,也算是给在怿星的DoIP协议栈画上一个属于自己的句号。
LwIP 全名为 Light weight IP,意思是轻量化的 TCP/IP 协议, 是瑞典计算机科学院(SICS)的 Adam Dunkels 开发的一个小型开源的 TCP/IP 协议栈。 LwIP 的设计初衷是:用少量的资源消耗(RAM)实现一个较为完整的 TCP/IP 协议栈,其中“完整”主要指的是 TCP 协议的完整性, 实现的重点是在保持 TCP 协议主要功能的基础上减少对 RAM 的占用。此外 LwIP既可以移植到操作系统上运行,也可以在无操作系统的情况下独立运行。
2. 原因
引起该问题的根本原因是,LwIP select函数里如果判断对应的socket没有事件产生(读/写/异常),进行简单处理后则改线程休眠,让出cpu控制权。如果在select休眠期间,进行了close socket的操作,会释放对应的socket pcb(close(socket)是成功的),然后在select休眠结束后,判断该socket资源不存在,则直接退出select函数,但是此时该socket的select_wait标志位没被清除。LwIP在分配socket时(资源都是静态分配的,类似于有一个socket数组,若分配则对应标志位为真),socket是否空闲是会对select_wait该标志位进行判断,所以即使该socket没有被使用,调用socket()函数时也会认为该socket是被占用的,所以几次之后,socket资源被假耗尽。
3. 解决
知道原因后,问题就好解决了。有以下两个解决问题的思路。
1. 更改LwIP源码,对对应的标志位进行判断和清除。该解决方案,如果能够push到LwIP主分支,则是一劳永逸的,否则如果要跟随LwIP官方更新,自己得维护一套代码,并持续merge。
2. 使用者,在使用接口时,做同步。即在select休眠期间不允许进行close socket操作,同时在close socket也不允许进入select函数。所以只要在两个函数之间加上条件判断就好。
考虑到维护成本,最终选择方案2.
二、分析
解决思路在上面已经给出,下面主要想从源码级对问题进行分析。原因中,涉及三个函数,
1. socket函数,即lwip_socket,函数原型如下:
int lwip_socket(int domain, int type, int protocol)
2. close函数,即lwip_close,原型如下:
int lwip_close(int s)
3. select函数, 即lwip_select(),原型如下:
intlwip_select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout)
1. 拓展
LwIP本身提供了类似于bsd socket编程模型,同时也实现了简易版的select函数。
关于socket编程的教程是实在太多了,在这不再重复去描述,socket编程参考链接。辅导过一些人进行socket编程,初学者包括我自己,容易忽略的一点就是,作为server时,listen-socket和accept-socket不是一回事。可以理解为listen-socket窗口,窗口只是负责监听有谁要走通道,走哪个通道,并把真正的通道--accept-socket给到上层。对于其他的,感觉跑跑示例程序,单步走一下,就基本理解了。
在不使用select时,并没有发现socket资源释放不完全的问题。本文不展开讲解lwip select的实现,但是对于select的使用需要稍微展开下,select编程参考链接。关于select本质上是一个同步I/O函数,只不过改同步函数可以同时监控多个"IO"通道,所以也称为多路复用。熟悉了上面的socket编程后,如果需要实现多个socket同时通信的话,就应该给每个socket开一个线程,在负载不是特别高的情况下会显得效率特别低,同时线程太多,就不得不考虑资源竞争的问题,如果竞态条件太多,也容易产生问题(多线程资源竞争问题)。多路复用即是用一个线程监听多个通道(描述符),一旦某个描述符就绪(可读、可写或者异常),就通知程序进行相应的读写操作。上庙的描述,看起来select是异步的,