蔣俊,鐘偉勝
(1.長(zhǎng)沙 410081;2.江蘇省淮安技師學(xué)院)
蔣?。ㄗ杂陕殬I(yè)者),從事嵌入式開(kāi)發(fā)6年,擅長(zhǎng)μC/OS-II和LWIP,熟悉DSP和ARM開(kāi)發(fā),自主開(kāi)發(fā)過(guò)GUI庫(kù),精通電能計(jì)量、測(cè)量、諧波分析;鐘偉勝(講師),主要研究方向?yàn)橛?jì)算機(jī)網(wǎng)絡(luò)。
現(xiàn)有一嵌入式設(shè)備具備網(wǎng)絡(luò)通信功能,它要求設(shè)計(jì)成支持多臺(tái)數(shù)據(jù)采集器同時(shí)進(jìn)行通信,如圖1所示。多種原因(價(jià)格,功耗和尺寸)的限制導(dǎo)致該嵌入式設(shè)備的處理能力和存儲(chǔ)空間有限,因此選用μC/OS-II操作系統(tǒng)和LWIP協(xié)議棧。在通信過(guò)程中,數(shù)據(jù)采集器充當(dāng)客戶端,嵌入式設(shè)備是服務(wù)器,顯然,需要將該嵌入式設(shè)備設(shè)計(jì)成并發(fā)服務(wù)器,另外為節(jié)省內(nèi)存,需要設(shè)計(jì)代理線程模式。
圖1 多臺(tái)采集器與嵌入式設(shè)備通信
一般來(lái)說(shuō)OS都支持動(dòng)態(tài)生成線程(或進(jìn)程),μC/OS-II也不例外,對(duì)于程序員來(lái)說(shuō),要處理4方面的問(wèn)題:線程的正文、優(yōu)先級(jí)、堆??臻g和檢測(cè)堆棧占用率。
正文(text)即線程的執(zhí)行代碼,經(jīng)常組織成一個(gè)無(wú)限循環(huán)的函數(shù),更具普遍規(guī)律的是,在網(wǎng)絡(luò)開(kāi)發(fā)中多個(gè)線程的正文都是同一個(gè)函數(shù),因?yàn)檫@些線程基本上完成相同的任務(wù),差異只是連接的主機(jī)不同。
μC/OS-II中的優(yōu)先級(jí)具有十分重要的意義,不僅影響該線程的調(diào)度,而且在整個(gè)系統(tǒng)中它是唯一的ID,可以分配一段ID號(hào)給動(dòng)態(tài)線程。
最重要的工作是堆棧的處理,當(dāng)生成一個(gè)線程時(shí)需要給它分配一段內(nèi)存充當(dāng)堆棧,當(dāng)刪除該線程時(shí)需要回收這段內(nèi)存。
根據(jù)線程個(gè)數(shù)與每個(gè)線程堆棧大小定義一塊內(nèi)存區(qū),然后使用OS提供的OSMemCreate()生成動(dòng)態(tài)內(nèi)存區(qū),每當(dāng)生成一個(gè)線程時(shí),調(diào)用OSMemGet()來(lái)獲取一塊內(nèi)存,將該內(nèi)存的句柄保存;刪除一個(gè)線程時(shí),調(diào)用OSMemPut()將該內(nèi)存回收。這里有一個(gè)問(wèn)題,怎么查找被刪除線程的堆棧呢?其實(shí)它是根據(jù)該線程的優(yōu)先級(jí)號(hào)來(lái)完成的。
一個(gè)線程可以調(diào)用OSTaskDel()來(lái)刪除自己,那么它可以回收自己的堆棧嗎?事實(shí)上,這樣做很危險(xiǎn)!當(dāng)一個(gè)線程沒(méi)有完全刪除時(shí),它是依賴堆棧來(lái)運(yùn)行的,如果此時(shí)回收了堆??臻g,可能會(huì)帶來(lái)致命的錯(cuò)誤。假設(shè)一個(gè)線程回收自己的堆棧且該內(nèi)存立即被別的線程使用,那么當(dāng)它還想使用這個(gè)堆棧執(zhí)行最后一些工作時(shí),堆棧里的有效數(shù)據(jù)已經(jīng)被破壞了,這可能會(huì)帶來(lái)程序的崩潰,而且這種錯(cuò)誤很難查找。
一個(gè)安全可行的辦法是,一個(gè)線程向創(chuàng)建者發(fā)送消息——請(qǐng)回收我的堆??臻g,然后刪除自己,注意這2個(gè)動(dòng)作必然是“原子的”,否則上述的錯(cuò)誤就不可避免了。μC/OS-II在刪除線程的函數(shù) OSTaskDel()中提供鉤子函數(shù)App_TaskDelHook(),并且調(diào)用該鉤子函數(shù)時(shí)中斷被關(guān)閉,從而保證原子操作,可以在該函數(shù)中發(fā)送消息。
最后,一個(gè)強(qiáng)大的嵌入式系統(tǒng)需要檢測(cè)線程的堆棧占用率,以防止堆棧溢出帶來(lái)的內(nèi)存錯(cuò)誤。引入動(dòng)態(tài)線程后,線程個(gè)數(shù)是未知的,該如何檢測(cè)堆棧占用率呢?這個(gè)工作可以交給OS來(lái)完成,因?yàn)樗盍私饽男┚€程已經(jīng)被創(chuàng)建(通過(guò)查看OS_TCB來(lái)完成),可以簡(jiǎn)單地調(diào)用OSTaskStkChk()遍歷所有優(yōu)先級(jí)號(hào),對(duì)于已創(chuàng)建線程會(huì)返回堆棧使用數(shù)據(jù),不存在的線程會(huì)返回錯(cuò)誤信息(如該線程不存在)。[1]
我們先來(lái)解決一個(gè)理論問(wèn)題:為什么網(wǎng)絡(luò)通信多連接需要設(shè)計(jì)并發(fā)線程,用一個(gè)線程來(lái)查詢并處理多連接是否可以呢?回答這個(gè)問(wèn)題需要看看處理一個(gè)網(wǎng)絡(luò)連接的線程到底在干什么。在LWIP協(xié)議棧中,當(dāng)線程調(diào)用netconn_recv()等待連接主機(jī)的報(bào)文時(shí)會(huì)被阻塞在郵箱recvmbox上,直到接收成功該線程才進(jìn)行下一步處理。阻塞在OS中是通過(guò)切換CPU來(lái)實(shí)現(xiàn)的,阻塞時(shí)間內(nèi)該線程什么也干不了,查詢并處理多連接是不可能的。因此,一個(gè)線程處理一個(gè)網(wǎng)絡(luò)連接,這種模式最自然,也最科學(xué)。
這里有一個(gè)主線程負(fù)責(zé)偵聽(tīng)網(wǎng)絡(luò),每建立一個(gè)網(wǎng)絡(luò)連接時(shí)創(chuàng)建一個(gè)新線程并傳遞網(wǎng)絡(luò)連接句柄,新線程開(kāi)始處理對(duì)應(yīng)網(wǎng)絡(luò)連接上的所有事務(wù),直到該網(wǎng)絡(luò)連接斷開(kāi)時(shí)才刪除自己,并通知主線程回收堆棧空間,整個(gè)工作過(guò)程如圖2所示。
圖2 多線程與并發(fā)連接
結(jié)合LWIP看上述過(guò)程的實(shí)現(xiàn),主線程阻塞在netconn_accept()上,每接收一個(gè)新連接句柄struct netconn*p_stNetConn,就會(huì)創(chuàng)建一個(gè)新線程并傳遞該句柄。新線程將阻塞在netconn_recv()上,每接收一個(gè)網(wǎng)絡(luò)數(shù)據(jù)包struct netbuf*p_stNetBuf后進(jìn)行處理,新線程通過(guò)查詢ERR_IS_FATAL(p_stNetConn->err)來(lái)得知網(wǎng)絡(luò)連接是否有效,如果無(wú)效,則刪除自己并通知主線程回收堆??臻g。[3]
從圖1中可以看出,無(wú)論多少個(gè)數(shù)據(jù)采集器,嵌入式設(shè)備的數(shù)據(jù)都是一致的,即數(shù)據(jù)與客戶端連接個(gè)數(shù)無(wú)關(guān);另外,每個(gè)數(shù)據(jù)采集器與設(shè)備通信的操作是相同的(一個(gè)優(yōu)秀架構(gòu)會(huì)保證通信操作的同構(gòu)性)。因此,引入代理線程設(shè)計(jì)模式,至少具備2方面的優(yōu)點(diǎn):
①節(jié)省內(nèi)存。假設(shè)解析通信協(xié)議需要N字節(jié)內(nèi)存,如果新建M個(gè)線程,那么采用代理線程將節(jié)省N×(M-1)字節(jié)內(nèi)存,除代理線程外其他都不需要解析協(xié)議,這對(duì)于嵌入式系統(tǒng)寶貴內(nèi)存來(lái)說(shuō)是一個(gè)極大的優(yōu)勢(shì)。
② 避免競(jìng)態(tài)。一份數(shù)據(jù)如果被多個(gè)線程共享,那必定會(huì)帶來(lái)令人頭痛的競(jìng)態(tài)問(wèn)題,設(shè)計(jì)者不得不花費(fèi)大量精力來(lái)保證線程安全;采用代理線程后,該數(shù)據(jù)只被一個(gè)線程操作,從源頭避免共享,降低設(shè)計(jì)復(fù)雜度。[2]
圖3描述了代理線程的工作原理,當(dāng)客戶端Client_i向線程Thread_i發(fā)起通信請(qǐng)求時(shí),線程把該請(qǐng)求委托給Proxy-Thread來(lái)完成,Proxy-Thread解析該委托任務(wù)并回應(yīng)客戶端Client_i。同時(shí),也顯示了這種設(shè)計(jì)模式的缺點(diǎn),需要線程之間通信和少量的時(shí)間開(kāi)銷。
圖3 代理線程通信原理
在多線程設(shè)計(jì)中不得不提的是時(shí)序問(wèn)題,它可以清晰地反映線程之間是如何交互的,OS是如何調(diào)度線程的以及系統(tǒng)的運(yùn)行軌跡。在本設(shè)計(jì)中,委托線程與代理線程的交互如圖4所示,每當(dāng)委托線程提交任務(wù)后它就被阻塞,直到代理線程處理完該任務(wù)才解除阻塞;另外代理線程負(fù)荷最重,它占用大部分的CPU資源。
圖4 線程交互時(shí)序圖
委托線程與代理線程之間的通信接口該怎樣設(shè)計(jì)呢?從需求出發(fā),代理線程完成委托線程的任務(wù)需要以下資源:網(wǎng)絡(luò)連接句柄、接收數(shù)據(jù)包指針、同步信號(hào)量和委托線程私有數(shù)據(jù)存儲(chǔ)區(qū)。發(fā)送回應(yīng)數(shù)據(jù)包必須提供網(wǎng)絡(luò)連接句柄;要解析協(xié)議必須依賴接收數(shù)據(jù)包;當(dāng)代理線程完成任務(wù)后需要同步委托線程,這里將使用信號(hào);客戶端往往需要設(shè)置委托線程,數(shù)據(jù)將保存在它的私有數(shù)據(jù)存儲(chǔ)區(qū)。數(shù)據(jù)結(jié)構(gòu)的設(shè)計(jì)如圖5所示。
圖5 線程通信接口
在上述通信接口設(shè)計(jì)中有SemProtect用于保護(hù)線程的私有數(shù)據(jù)單元,這個(gè)信號(hào)量有存在的必要嗎?以圖6中沒(méi)有設(shè)置信號(hào)保護(hù)的情況為例,這將直接導(dǎo)致一個(gè)錯(cuò)誤:當(dāng)Proxy需要通過(guò)連接句柄主動(dòng)發(fā)送數(shù)據(jù)時(shí),委托線程搶奪了CPU,并將該連接句柄置為無(wú)效,等Proxy再次獲得CPU時(shí),它并不知道該句柄已經(jīng)無(wú)效了,發(fā)送數(shù)據(jù)將會(huì)導(dǎo)致LWIP協(xié)議棧紊亂。
圖6 沒(méi)有信號(hào)量保護(hù)導(dǎo)致錯(cuò)誤
解決這個(gè)問(wèn)題的辦法就是原子操作,即檢測(cè)線程有效與使用連接句柄發(fā)送數(shù)據(jù)不能被打斷,信號(hào)量能夠勝任這種場(chǎng)合。圖7和圖8表明,無(wú)論P(yáng)roxy先還是后,獲取信號(hào)量都能避免上述狀況下錯(cuò)誤的發(fā)生。
圖7 Proxy先獲取信號(hào)量
圖8 Proxy后獲取信號(hào)量
本文重點(diǎn)研究了基于μC/OS-II和LWIP的嵌入式系統(tǒng)下并發(fā)服務(wù)器和代理線程的實(shí)現(xiàn)模式,它具備網(wǎng)絡(luò)多連接(數(shù)目?jī)H依賴內(nèi)存大?。┖蜆O大節(jié)省內(nèi)存的優(yōu)勢(shì),深入探究線程同步和網(wǎng)絡(luò)開(kāi)發(fā)陷阱,對(duì)于嵌入式系統(tǒng)網(wǎng)絡(luò)開(kāi)發(fā)具備實(shí)用價(jià)值。
論文中涉及的技術(shù)方法已在嵌入式產(chǎn)品上驗(yàn)證成功,該產(chǎn)品軟件基于μC/OS-II V2.86和 LWIP V1.3.2,硬件基于LPC1768處理器和以太網(wǎng)口,實(shí)踐證明論文中的方法穩(wěn)定、可行。
[1]Jean J Labrosse.嵌入式實(shí)時(shí)操作系統(tǒng)μC/OS-II[M].邵貝貝,等譯.北京:北京航空航天大學(xué)出版社,2007.
[2]David E.Simon嵌入式系統(tǒng)軟件教程[M].陳向群,等譯.北京:機(jī)械工業(yè)出版社,2005.
[3]Adam Dunkels.Design and Implementation of the LWIP TCP/IP Stack.Swedish Institute of Computer Science,2001.