1.网卡接收数据的流程
我们网卡接收数据基本上就是开发板上eth接收完数据后产生一个中断,然后释放一个信号量通知网卡接收线程去处理这些接收的数据,然后将这些数据封装成信息,投递到tcpip_mbox邮箱中,LWIP内核线程得到这个消息,就对消息进行解析,根据消息中数据包类型进行处理,实际上是调用ethernetif_input()函数决定是否递交到IP层,如果是ARP包,内核就不会递交给IP层,而是更新ARP缓存表,对于IP数据包则递交给IP层去处理,这就是一个数据从网卡到内核的过程.
2.内核超时处理
在LWIP中很多时候都要用到超时处理,例如ARP缓存表项的时间管理,IP分片数据报的重装等待超时,TCP中的建立连接超时,重传超时机制等,因此超时处理的实现是TCP/IP协议栈中一个重要部分,Lwip为每个与外界网络连接的任务都有设定了timeout属性,即等待超时时间,超时处理的相关代码在timeouts.c与timeout.h中.
2.1sys_timeo结构体与超时链表
LWIP通过一个sys_timeo类别的数据结构管理与超时链表相关的所有超时事件.
LWIP使用这个结构体记录下内核中所有被注册的超时事件,这些结构体会以链表的形式一个个连接在超时链表中,而内核中只有一个超时链表,那么怎么对超时链表进行管理呢?
LWIP定义了一个sys_timeo类型的指针next_timeout,并且将next_timeout指向当前内核中链表头部,所有被注册的超时事件都会按照被处理的先后顺序排列在超时链表中.sys_timeo结构体与超时链表源码如下图
(1) 指向下一个超时事件的指针,用于超时链表的连接
(2)当前超时事件的等待时间
(3)指向超时的回调函数,该事件超时后就执行对应的回调函数
(4)向回调函数传入参数
(5)指向超时链表第一个超时事件.
2.2注册超时函数
LWIP虽然使用超时链表进行管理所有的超时事件,那么它首先需要知道有哪些超时事件才能去管理,而这些超时事件就是通过注册的方式被挂载在链表上,简单来说就是这些超时事件要在内核中登记一下,内核才会去处理.
LWIP中注册超时事件的函数是 sys_timeout()
实际上调用的是sys_timeout_abs()函数
(1)根据当前时间计算出超时的时间,然后调用sys_timeout_abs()函数将当前事件插入超时链表
(2)从内存池中申请一个 MEMP_SYS_TIMEOUT 类型内存,保存对应超时事件的相关信息.
(3)填写对应的超时事件信息,超时回调函数,函数参数,超时的时间
(4)如果超时链表中没有超时事件,那么新添加的事件就是链表的第一个
(5)如果新插入的超时事件比链表上第一个事件的时间短,则将新插入的超时事件设置成链表的第一个
(6)否则就是遍历链表,寻找合适的插入节点,超时链表根据超时事件的升序排列
在timeouts.c中,有一个名字为lwip_cyclic_timer的结构,Lwip使用该结构存放了其内部使用的循环超时事件.这些超时事件在Lwip初始化时通过函数sys_timeouts_init()调用定时器注册函数sys_timeout()注册进入超时链表中,lwip_cyclic_timer的结构具体见代码
lwip_cyclic_timer数组中存放了每个周期性的超时事件回调函数及超时时间,在LWIP初始化的时候就将这些事件一个个插入超时链表中,具体如下
在每个 sus_timeo结构体中有着一个成员,函数指针,一直记录的对应的超时回调函数,对于周期性的回调函数,LWIP在他们初始化的时候将他们注册到lwip_cyclic_timer()函数中,每次处理回调函数之后,就调用sys_timeout_abs()函数将其重新注册到超时链表中
2.3超时检查
其实前面的超时处理,在实际开发中,我们用户基本不用考虑的,但是学一下做好铺垫也是很重要,我们既然有超时处理,那肯定我们都可以堆砌进行超时检查并且去处理,LWIP中以下两个函数可以实现对超时的处理
void sys_check_timeouts(void):这是用于裸机的函数,用户需要在裸机应用程序中周期性调用该函数,每次调用的时候LWIP都会检查超时链表上的第一个sys_timo结构体是否到期,如果没有到期,直接退出该函数,否则.执行sys_timeo结构体中对应的超时回调函数,并从链表上删除它,然后继续检查下一个sys_timeo结构体,直到sys_time结构体没有超时才退出.
tcpip_timeouts_mbox_fetch(sys_mbox_t *mbox,void** msg):这个函数在操作系统的线程中循环调用,主要是等待tcpip_mbox信息,是可阻塞的,如果在等待tcpip_mbox的过程中发生超时事件,则会同时执行超时事件处理,即调用超时回调函数.LWIP是这样子处理的,如果已经发生超时,LWIP就会内部调用sys_check_timeouts()函数去检查超时的sys_timeo结构体并调用其对应的回调函数,如果没有发生超时,那就一直等待消息,其等待的事件为下一个超时时间的时间,一举两得.LWIP中tcpIP线程就是靠这种方法,即处理了上层及底层的tcpip_mbox消息,同时处理了所有需要超时处理的事件.
(1)调用sys_timeouts_sleeptime()函数得到距离事件超时的时间并保存在sleeptime变量中.
(2)如果sleeptime为SYS_TIMEOUTS_SLEEPTIME_INFINITE ,表示当前系统没有超时函数,那只要一直等待mbox消息就行了,所以调用sys_arch_mbox_fetch()函数进行等待消息,等待时间是一直等待.
(3)如果sleeptime为0表示已经发生超时了,那就调用sys_check_timeouts()去检查以下到底是哪个事件发生超时并且去处理其超时回调函数.
(4)对于其他时间,LWIP就在等待tcpip_mbox的消息的同时就去处理超时事件,等待tcpip_mbox的消息的时间为sleeptime,然后在时间到达的时候就处理超时事件.如果接收到消息,并且超时时间还没到,那就去处理tcpip_mbox的消息,然后再回来重新计算等待时间sleeptime,如此反复,这样子既不会错过tcpip_mbox的消息,也不会错过超时的事件
3.tcpip_thread线程
Lwip在操作系统的环境下,Lwip内核是作为操作系统的一个线程运行,在协议栈初始化的时候就会创建tcpip_thread线程,那么这个线程如下图所示:
(1)Lwip将函数tcpip_timeouts_mbox_fetch()定义为带参宏TCPIP_MBOX_FETCH,所以在这里就是等待消息并且处理超时事件.
(2)如果没有等到消息就继续等待
(3)等待到消息就对消息进行处理, tcpip_thread_handle_msg(msg);这个函数如下图:
(1)(2) 根据消息中的不同类型,进行不同的处理,对于TCPIP_MSG_API类型,就执行对应的API函数
(3)对于 TCPIP_MSG_INPKT 类型,直接交给ARP层处理
(4)对于 TCPIP_MSG_TIMEOUT类型,表示上层注册一个超时事件,直接执行注册超时事件即可
(5)相反的,对于TCPIP_MSG_UNTIMEOUT,表示上层删除一个超时事件,直接执行删除事件即可
(6)(7) 对于 TCPIP_MSG_CALLBACK 或者是 TCPIP_MSG_CALLBACK_STATIC 类型,表示上层通过回调方式执行一个回调函数,那么执行对应的回调函数即可.
4.LWIP中的信息
主要是讲数据包消息与API消息,整个内核的运作都要依赖他们.
4.1消息结构
从前面的章节,我们知道消息有多种类型,Lwip中消息是有多种结构的,对于不同的消息类型其封装是不一样的,我们上面的tcpip_thread线程是通过tcpip_msg描述消息的,
tcpip_thread线程接收到消息后,根据消息的类型进行不同的处理
Lwip中使用tcpip_msg_type枚举类型定义了系统中可能出现的消息的类型,消息结构msg字段是一个共用体,其中定义了各种消息类型的具体内容,每种类型的消息对应了共用体中的一个字段,其中定义了各种消息类型的具体内容,每种类型的信息对应了共用体中的一个字段,其中注册与删除事件的消息使用了同一个tmo字段.LWIP中的API相关的信息内容很多,不适合直接放在tcpip_msg中,所以Lwip用一个api_msg结构体来描述API消息,在tcpip_msg中只存在指向api_msg结构体的指针.
(1) 消息的类型,目前有 7 种
(2) API消息主要由两部分组成,一部分是用于表示内核执行的API函数,另一部分是执行函数时候的参数,都会被记录在api_msg中
(3)与API消息差不多,也是由两部分组成,一部分是tcpip_api_call_fn类型的函数,另一部分是其对应的形参,此外还有用于同步的信号量.
(4)Inp 用于记录数据包信息的内容,p指向接收到的数据包,netif表示接收到数据包的网卡,input_fn表示输入的函数接口,在tcpip_inpkt进行配置.
(5)cb用于记录回调函数与其对应的形参
(6)tmo用于记录超时相关信息,如超时的时间,如超时回调函数,参数等
4.2数据包信息
对于每种类型的信息,Lwip内核都必须有一个产生与之对应的消息函数,在产生该类型的信息后就将其投递到系统邮箱tcpip_mbox中,这样子tcpip_thread线程就会从邮箱中得到信息并且处理,从而使内核完美运作,
从这个图中我们可以发现对应数据包的消息,是通过tcpip_input()函数对消息进行构造并且投递的,但是真正执行这些操作的函数时tcpip_inpkt(),
(1)调用tcpip_inpkt()函数将ethernet_input()函数作为结构体的一部分传递给内核,然后内核接收到这个数据包就调用该函数.
(2)申请存放消息的内存空间
(3)构造信息,消息的类型就是数据包信息,初始化消息结构中msg共用体的inp字段,p指向数据包
网卡就是对应的网卡,处理的函数就是我们熟悉的ethernet_input()函数
(4)构造信息完成,就调用sys_mbox_trypost进行投递信息,这其实就是对操作系统的API简单封装,如果成功就是ERR_OK
(5)如果投递失败,就释放对应的消息结构空间
总的来说,我们无论是裸机系统 还是 操作系统 都是通过ethernet_input()函数去处理接收到的数据包,只不过在操作系统中,我们分成两个线程处理 ,接收线程只负责接收数据包,构造信息,最后投递,内核线程再去处理 这样子 可以同步进行更加高效.
4.3 API信息
Lwip使用api_msg结构体描述一个API信息的内容
api_msg只包含了三个字段,描述连接信息的com,内核返回的执行结果err,还有mag,
mag是一个共用体,根据不一样的API接口使用不一样的数据结构.在conn中,他保存了当前连接的重要信息,如信号量,邮箱等,lwip_netconn_do_xxx(xxx表示不一样的NETCONN API接口)类型的函数执行需要用这些信息来完成与应用线程的通信与同步,内核执行lwip_netconn_do_xxx类型的函数返回结果会被记录在err中,msg的各个产业记录各个函数执行时需要的详细参数.
我们了解底层的数据包信息,那么同理对于上层的API函数,想要与内核进行数据交互,也是通过Lwip的消息机制,API消息由用户线程发出,与内核进行交互,因为用户的应用程序并不是与内核处于同一线程中,简单来说就是用户使用NETCONN API接口的时候,Lwip会将对应API函数与参数构造成消息造成信息传递到tcpip_thread线程中,然后根据对应的API函数执行对应的操作.
与数据包信息类似,也是有独立的API消息投递函数去处理,那就是netconn_apimsg()函数,在NTCONN API中构造完成数据包,就会调用netconn_apimsg()函数进行投递信息
(1)根据netconn_bind()传递的参数初始化 api_msg结构体.
(2)调用neticonn_apimsg()函数投递这个api_msg结构体,这个函数实际上时调用tcpip_send_msg_wait_sem()函数投递API消息的,并且等待tcpip_thread线程的回应
(3)构造API信息,类型为TCPIP_MSG_API,函数为API对应的函数lwip_netconn_do_bind,将msg的指针指向api_msg结构体
(4)调用sys_mbox_post()函数向内核进行投递信息
(5)同时调用sys_arch_sem_wait()函数等待信息处理完毕
总的来说,用户的应用线程与内核也是相互独立的,依赖操作系统的ICP通信机制进行数据交互与同步(邮箱,信号量等),LWIP提供上层NETCONN API接口,会自动帮我们处理这些事情,只需要我们根据API接口传递正确的参数接口就好
其实这个运作示意图不是最好的,这种运作的方式在每次发送数据的时候,会进行一次线程的调度,这无疑是增大了系统的开销,而将LWIP_TCPIP_CORELOCKNG宏定义设置为1则无需操作系统邮箱与信号量的参与,直接在用户线程中通过回调函数调用对应的处理,当然在这个过程中,内核线程是无法获得互斥量而运行的,因为是通过互斥量进行保护用户线程的处理