μC/OS-II和LWIP的并发服务器与代理线程设计模式

2014-06-25 02:46蒋俊钟伟胜
单片机与嵌入式系统应用 2014年12期
关键词:网络连接堆栈线程

蒋俊,钟伟胜

(1.长沙 410081;2.江苏省淮安技师学院)

蒋俊(自由职业者),从事嵌入式开发6年,擅长μC/OS-II和LWIP,熟悉DSP和ARM开发,自主开发过GUI库,精通电能计量、测量、谐波分析;钟伟胜(讲师),主要研究方向为计算机网络。

引 言

现有一嵌入式设备具备网络通信功能,它要求设计成支持多台数据采集器同时进行通信,如图1所示。多种原因(价格,功耗和尺寸)的限制导致该嵌入式设备的处理能力和存储空间有限,因此选用μC/OS-II操作系统和LWIP协议栈。在通信过程中,数据采集器充当客户端,嵌入式设备是服务器,显然,需要将该嵌入式设备设计成并发服务器,另外为节省内存,需要设计代理线程模式。

图1 多台采集器与嵌入式设备通信

1 基于μC/OS-II动态生成线程

一般来说OS都支持动态生成线程(或进程),μC/OS-II也不例外,对于程序员来说,要处理4方面的问题:线程的正文、优先级、堆栈空间和检测堆栈占用率。

正文(text)即线程的执行代码,经常组织成一个无限循环的函数,更具普遍规律的是,在网络开发中多个线程的正文都是同一个函数,因为这些线程基本上完成相同的任务,差异只是连接的主机不同。

μC/OS-II中的优先级具有十分重要的意义,不仅影响该线程的调度,而且在整个系统中它是唯一的ID,可以分配一段ID号给动态线程。

最重要的工作是堆栈的处理,当生成一个线程时需要给它分配一段内存充当堆栈,当删除该线程时需要回收这段内存。

根据线程个数与每个线程堆栈大小定义一块内存区,然后使用OS提供的OSMemCreate()生成动态内存区,每当生成一个线程时,调用OSMemGet()来获取一块内存,将该内存的句柄保存;删除一个线程时,调用OSMemPut()将该内存回收。这里有一个问题,怎么查找被删除线程的堆栈呢?其实它是根据该线程的优先级号来完成的。

一个线程可以调用OSTaskDel()来删除自己,那么它可以回收自己的堆栈吗?事实上,这样做很危险!当一个线程没有完全删除时,它是依赖堆栈来运行的,如果此时回收了堆栈空间,可能会带来致命的错误。假设一个线程回收自己的堆栈且该内存立即被别的线程使用,那么当它还想使用这个堆栈执行最后一些工作时,堆栈里的有效数据已经被破坏了,这可能会带来程序的崩溃,而且这种错误很难查找。

一个安全可行的办法是,一个线程向创建者发送消息——请回收我的堆栈空间,然后删除自己,注意这2个动作必然是“原子的”,否则上述的错误就不可避免了。μC/OS-II在删除线程的函数 OSTaskDel()中提供钩子函数App_TaskDelHook(),并且调用该钩子函数时中断被关闭,从而保证原子操作,可以在该函数中发送消息。

最后,一个强大的嵌入式系统需要检测线程的堆栈占用率,以防止堆栈溢出带来的内存错误。引入动态线程后,线程个数是未知的,该如何检测堆栈占用率呢?这个工作可以交给OS来完成,因为它最了解哪些线程已经被创建(通过查看OS_TCB来完成),可以简单地调用OSTaskStkChk()遍历所有优先级号,对于已创建线程会返回堆栈使用数据,不存在的线程会返回错误信息(如该线程不存在)。[1]

2 基于LWIP的并发服务器

我们先来解决一个理论问题:为什么网络通信多连接需要设计并发线程,用一个线程来查询并处理多连接是否可以呢?回答这个问题需要看看处理一个网络连接的线程到底在干什么。在LWIP协议栈中,当线程调用netconn_recv()等待连接主机的报文时会被阻塞在邮箱recvmbox上,直到接收成功该线程才进行下一步处理。阻塞在OS中是通过切换CPU来实现的,阻塞时间内该线程什么也干不了,查询并处理多连接是不可能的。因此,一个线程处理一个网络连接,这种模式最自然,也最科学。

这里有一个主线程负责侦听网络,每建立一个网络连接时创建一个新线程并传递网络连接句柄,新线程开始处理对应网络连接上的所有事务,直到该网络连接断开时才删除自己,并通知主线程回收堆栈空间,整个工作过程如图2所示。

图2 多线程与并发连接

结合LWIP看上述过程的实现,主线程阻塞在netconn_accept()上,每接收一个新连接句柄struct netconn*p_stNetConn,就会创建一个新线程并传递该句柄。新线程将阻塞在netconn_recv()上,每接收一个网络数据包struct netbuf*p_stNetBuf后进行处理,新线程通过查询ERR_IS_FATAL(p_stNetConn->err)来得知网络连接是否有效,如果无效,则删除自己并通知主线程回收堆栈空间。[3]

3 代理线程设计模式

从图1中可以看出,无论多少个数据采集器,嵌入式设备的数据都是一致的,即数据与客户端连接个数无关;另外,每个数据采集器与设备通信的操作是相同的(一个优秀架构会保证通信操作的同构性)。因此,引入代理线程设计模式,至少具备2方面的优点:

①节省内存。假设解析通信协议需要N字节内存,如果新建M个线程,那么采用代理线程将节省N×(M-1)字节内存,除代理线程外其他都不需要解析协议,这对于嵌入式系统宝贵内存来说是一个极大的优势。

② 避免竞态。一份数据如果被多个线程共享,那必定会带来令人头痛的竞态问题,设计者不得不花费大量精力来保证线程安全;采用代理线程后,该数据只被一个线程操作,从源头避免共享,降低设计复杂度。[2]

图3描述了代理线程的工作原理,当客户端Client_i向线程Thread_i发起通信请求时,线程把该请求委托给Proxy-Thread来完成,Proxy-Thread解析该委托任务并回应客户端Client_i。同时,也显示了这种设计模式的缺点,需要线程之间通信和少量的时间开销。

图3 代理线程通信原理

在多线程设计中不得不提的是时序问题,它可以清晰地反映线程之间是如何交互的,OS是如何调度线程的以及系统的运行轨迹。在本设计中,委托线程与代理线程的交互如图4所示,每当委托线程提交任务后它就被阻塞,直到代理线程处理完该任务才解除阻塞;另外代理线程负荷最重,它占用大部分的CPU资源。

图4 线程交互时序图

委托线程与代理线程之间的通信接口该怎样设计呢?从需求出发,代理线程完成委托线程的任务需要以下资源:网络连接句柄、接收数据包指针、同步信号量和委托线程私有数据存储区。发送回应数据包必须提供网络连接句柄;要解析协议必须依赖接收数据包;当代理线程完成任务后需要同步委托线程,这里将使用信号;客户端往往需要设置委托线程,数据将保存在它的私有数据存储区。数据结构的设计如图5所示。

图5 线程通信接口

在上述通信接口设计中有SemProtect用于保护线程的私有数据单元,这个信号量有存在的必要吗?以图6中没有设置信号保护的情况为例,这将直接导致一个错误:当Proxy需要通过连接句柄主动发送数据时,委托线程抢夺了CPU,并将该连接句柄置为无效,等Proxy再次获得CPU时,它并不知道该句柄已经无效了,发送数据将会导致LWIP协议栈紊乱。

图6 没有信号量保护导致错误

解决这个问题的办法就是原子操作,即检测线程有效与使用连接句柄发送数据不能被打断,信号量能够胜任这种场合。图7和图8表明,无论Proxy先还是后,获取信号量都能避免上述状况下错误的发生。

图7 Proxy先获取信号量

图8 Proxy后获取信号量

结 语

本文重点研究了基于μC/OS-II和LWIP的嵌入式系统下并发服务器和代理线程的实现模式,它具备网络多连接(数目仅依赖内存大小)和极大节省内存的优势,深入探究线程同步和网络开发陷阱,对于嵌入式系统网络开发具备实用价值。

论文中涉及的技术方法已在嵌入式产品上验证成功,该产品软件基于μC/OS-II V2.86和 LWIP V1.3.2,硬件基于LPC1768处理器和以太网口,实践证明论文中的方法稳定、可行。

[1]Jean J Labrosse.嵌入式实时操作系统μC/OS-II[M].邵贝贝,等译.北京:北京航空航天大学出版社,2007.

[2]David E.Simon嵌入式系统软件教程[M].陈向群,等译.北京:机械工业出版社,2005.

[3]Adam Dunkels.Design and Implementation of the LWIP TCP/IP Stack.Swedish Institute of Computer Science,2001.

猜你喜欢
网络连接堆栈线程
基于行为监测的嵌入式操作系统堆栈溢出测试*
基于C#线程实验探究
基于国产化环境的线程池模型研究与实现
个性化设置 Win10 的网络连接信息
运动想象的大尺度动态功能网络连接
基于堆栈自编码降维的武器装备体系效能预测
浅谈linux多线程协作
中小型网络组建技术
一种用于分析MCS-51目标码堆栈深度的方法
基于堆栈的24点游戏解决方案