趙成青,李宥謀,劉永斌,王 濤
(西安郵電大學(xué),陜西 西安 710000)
LWIP是瑞典計算機科學(xué)院(SICS)的Adam Dunkels開發(fā)的用于嵌入式系統(tǒng)的開源TCP/IP協(xié)議棧[1]。LWIP的含義是輕量級的TCP/IP協(xié)議,專注于減少資源消耗。嵌入式網(wǎng)絡(luò)傳輸系統(tǒng)由于成本資源的限制,往往采用簡化的TCP/IP協(xié)議。文中通過研究、分析常用的嵌入式網(wǎng)絡(luò)協(xié)議棧LWIP的結(jié)構(gòu),在物理層和應(yīng)用層提出了提高系統(tǒng)傳輸效率的改進(jìn)方法。
在小型嵌入式系統(tǒng)中,LWIP的實現(xiàn)基于上層協(xié)議已明確知道下層協(xié)議所使用的數(shù)據(jù)結(jié)構(gòu)的特點[2]。它會假設(shè)各層間的部分?jǐn)?shù)據(jù)結(jié)構(gòu)和實現(xiàn)原理在其他層是可見的。在數(shù)據(jù)包遞交過程中,各層協(xié)議可以通過指針直接指向數(shù)據(jù)包中其他層次的字段。所以上層可直接使用取地址計算得到下層中的數(shù)據(jù),這不僅使整個協(xié)議棧對數(shù)據(jù)包的操作更加靈活,而且避免了LWIP協(xié)議棧內(nèi)部數(shù)據(jù)遞交時的復(fù)制。但是,這僅僅是在LWIP協(xié)議棧內(nèi)部實現(xiàn)數(shù)據(jù)的零拷貝。在物理網(wǎng)卡向協(xié)議棧傳遞數(shù)據(jù)時和協(xié)議棧向應(yīng)用程序傳遞數(shù)據(jù)時,還是存在兩次消耗較大的數(shù)據(jù)拷貝過程。所以,文中提出在網(wǎng)卡接收數(shù)據(jù)時讓LWIP內(nèi)核存儲區(qū)指針直接指向物理網(wǎng)卡的寄存器地址的方法,避免了物理網(wǎng)卡數(shù)據(jù)到LWIP協(xié)議棧緩沖區(qū)數(shù)據(jù)的拷貝[3]。在應(yīng)用層,提出了利用μcos操作系統(tǒng)的郵箱機制,避免了多個外部應(yīng)用程序和協(xié)議棧內(nèi)核交互時的數(shù)據(jù)拷貝[4],從而實現(xiàn)了從物理層到應(yīng)用層真正的數(shù)據(jù)零拷貝,并且提高了系統(tǒng)的并發(fā)性。
在嵌入式系統(tǒng)中應(yīng)用比較廣泛的是MicroChip公司的ENC28J60網(wǎng)卡。在網(wǎng)卡驅(qū)動函數(shù)接收數(shù)據(jù)包時,網(wǎng)卡驅(qū)動函數(shù)首先向網(wǎng)卡發(fā)送數(shù)據(jù)包傳送指令,此時網(wǎng)卡會把BNRY(邊界寄存器)處的一個網(wǎng)卡格式的數(shù)據(jù)包數(shù)據(jù)一次性全部發(fā)送到DMA端口,此時網(wǎng)卡驅(qū)動函數(shù)會在DMA端口讀取所有字節(jié)數(shù)據(jù)后,網(wǎng)卡會自動將BNRY(邊界寄存器)值調(diào)整為下一個數(shù)據(jù)包地址,以準(zhǔn)備下一次讀取所有字節(jié)數(shù)據(jù)。然后將接收到的數(shù)據(jù)封裝成LWIP熟悉的格式并且寫到緩沖區(qū)中,這個過程涉及到數(shù)據(jù)到拷貝[5]。在ENC28J60網(wǎng)卡中接收緩沖器由一個硬件管理的循環(huán)FIFO構(gòu)成。ERXST表示接收緩沖區(qū)起始地址,ERXNDH表示接收緩沖區(qū)結(jié)束地址[6]。如圖1所示,通過把網(wǎng)卡相關(guān)的寄存器映射到LWIP內(nèi)核內(nèi)存空間,就可以直接對網(wǎng)卡寄存器進(jìn)行操作,避免了物理網(wǎng)卡到LWIP內(nèi)核空間的數(shù)據(jù)拷貝。然后封裝成LWIP內(nèi)核能夠識別的pbuf類型的數(shù)據(jù)包結(jié)構(gòu),在LWIP內(nèi)核中由low_level_input()函數(shù)完成這個功能。通過ehternetif_input()函數(shù)解析該數(shù)據(jù)包的類型,然后將該數(shù)據(jù)包指針遞交給相應(yīng)的上層。
在網(wǎng)卡接收數(shù)據(jù)時,需要申請一個數(shù)據(jù)包,然后將網(wǎng)卡中的數(shù)據(jù)填入數(shù)據(jù)包中。發(fā)送數(shù)據(jù)包時,協(xié)議棧的某層中會申請一個pbuf,并將相應(yīng)的數(shù)據(jù)裝入到數(shù)據(jù)區(qū)域,同時相關(guān)的協(xié)議首部信息也會被填寫到pbuf的預(yù)留數(shù)據(jù)區(qū)域中[7]。數(shù)據(jù)包申請函數(shù)有兩個重要參數(shù),一個是想申請的數(shù)據(jù)包pbuf類型,另一個重要參數(shù)是該數(shù)據(jù)包是在協(xié)議棧中哪一層被申請的,分配函數(shù)會根據(jù)這個層次的不同,在pbuf數(shù)據(jù)區(qū)域前為相應(yīng)的協(xié)議預(yù)留出首部空間,這就是offset值??偟膩碚f,LWIP定義了四個層次,當(dāng)數(shù)據(jù)包申請時,所處的層次不同,會導(dǎo)致預(yù)留空間的offset值不同[8]。層次定義時通過一個枚舉類型pbuf_layer:
Typedef enum{
PBUF_TRANSPORT,//傳輸層
PBUF_IP,//網(wǎng)絡(luò)層
PBUF_LINK,//鏈路層
PBUF_RAW,//原始層
}pbuf_layer;
上面的結(jié)構(gòu)體中除了定義枚舉類型pbuf_layer來表示各個網(wǎng)絡(luò)協(xié)議層的名稱外,還定義了兩個宏:PBUF_TRANSPORT_HLEN和PBUF_IP_HLEN。前者是典型的TCP報文首部長度,而后者是典型的不帶任何選項字段的IP首部長度。代碼如下所示:
Switch(layer){
Case PBUF_TRANSPORT:
Offset=PBUF_LINK_HLEN+PBUF_IP_HLEN+PBUF_TRASNSPORT_HLEN;
Case PBUF_IP:
Offset=PBUF_LINK_HLEN+PBUF_IP_HLEN;
Case PBUF_LINK:
Offset=PBUF_LINK_HLEN;
Case PBUF_LINK:
Offset=0;
在LWIP的數(shù)據(jù)包管理函數(shù)pbuf.c中,首先根據(jù)數(shù)據(jù)包申請時傳入的協(xié)議層參數(shù),計算需要在pbuf數(shù)據(jù)區(qū)簽預(yù)留的長度offset值,然后根據(jù)pbuf的類型進(jìn)行實際申請。Pbuf_pooll類型申請最復(fù)雜[9],因為可能需要幾個pool連接在一起,以此來滿足用戶的空間需求。
限于篇幅,對LWIP內(nèi)存分配機制不做深入研究;pbuf_ref和pbuf_rom類型申請最簡單,它們只是在內(nèi)存MEMEP_PBUF中分配一個pbuf結(jié)構(gòu)空間,然后初始化相關(guān)字段,注意這兩種類型的payload指針需要用戶自行設(shè)置,通常在調(diào)用完函數(shù)pbuf_alloc后,調(diào)用者需要將payload指向某個數(shù)據(jù)區(qū)。
在原始層以太網(wǎng)驅(qū)動中:
P=pbuf_alloc(PBUF_RAW,recvlen,PBUF_RAM);
這個調(diào)用語句申請了一個PBUF_RAM類型的pbuf,且其申請的協(xié)議層為PBUF_RAW,所以pbuf_alloc函數(shù)不會在數(shù)據(jù)區(qū)前預(yù)留出任何首部空間;通過使用p->payload,就可以實現(xiàn)對pbuf中數(shù)據(jù)區(qū)的讀取或者寫入操作了。
在傳輸層TCP層:
P=pbuf_alloc(PBUF_RAW,recvlen,PBUF_RAM);
它告訴數(shù)據(jù)包分配函數(shù)使用PBUF_RAM類型的pbuf,且數(shù)據(jù)前應(yīng)該預(yù)留一部分的首部空間。由于這里是PBUF_TRANSPORT層,所以預(yù)留空間將有54個字節(jié),即TCP首部長度的20個字節(jié)、IP數(shù)據(jù)包首部長度的20個字節(jié)以及以太網(wǎng)幀首部長度的14字節(jié)。當(dāng)數(shù)據(jù)包往下層遞交,各層協(xié)議就直接操作這些預(yù)留空間的數(shù)據(jù),以實現(xiàn)數(shù)據(jù)首部的填寫,這樣就避免了數(shù)據(jù)的拷貝。
協(xié)議棧API實現(xiàn)時,也為用戶提供了數(shù)據(jù)包管理函數(shù),可以完成數(shù)據(jù)包內(nèi)存申請、釋放、數(shù)據(jù)拷貝等任務(wù)。無論是UDP還是TCP連接,當(dāng)協(xié)議棧接收到數(shù)據(jù)包后,會將數(shù)據(jù)封裝在一個netbuf中,并遞交給應(yīng)用程序[10]。在發(fā)送數(shù)據(jù)時,不同類型的連接將導(dǎo)致不同的數(shù)據(jù)處理方式。對于TCP連接,內(nèi)核會根據(jù)用戶提供待發(fā)送數(shù)據(jù)的起始數(shù)據(jù)和長度,自動將數(shù)據(jù)封裝在合適的數(shù)據(jù)包中,然后放入發(fā)送隊列;對于UDP,用戶需要手動將數(shù)據(jù)封裝在netbuf中,通過調(diào)用發(fā)送函數(shù),內(nèi)核直接發(fā)送數(shù)據(jù)包中的數(shù)據(jù)段。
應(yīng)用程序使用netbuf結(jié)構(gòu)來描述、組裝數(shù)據(jù)包,該結(jié)構(gòu)只是對內(nèi)核pbuf的簡單封裝,是用戶應(yīng)用程序和協(xié)議棧共享的。外部應(yīng)用程序可以使用該結(jié)構(gòu)來管理發(fā)送數(shù)據(jù)、接收數(shù)據(jù)的緩沖區(qū)。netbuf是基于pbuf實現(xiàn)的,其結(jié)構(gòu)如以下代碼所示:
Struct netbuf{
Struct pbuf *p,*ptr;
Ip_addr_t *addr;
U16_t port;
}
其中,netbuf相當(dāng)于一個數(shù)據(jù)首部,保存數(shù)據(jù)的字段是p,它指向pbuf鏈表首部,ptr指向鏈表中的其他位置,addr表示IP地址,port表示端口號。
netbuf是應(yīng)用程序描述待發(fā)送數(shù)據(jù)和已接收數(shù)據(jù)的基本結(jié)構(gòu),引入netbuf結(jié)構(gòu)看似會讓應(yīng)用程序更加繁雜,但實際上內(nèi)核為應(yīng)用程序提供了API,通過共享一個netbuf結(jié)構(gòu)(如圖2所示),兩部分API就能實現(xiàn)對數(shù)據(jù)包的共同處理,避免了數(shù)據(jù)拷貝。
圖2 用戶緩沖區(qū)結(jié)構(gòu)
與BSD相同,LWIP協(xié)議棧API也對網(wǎng)絡(luò)連接進(jìn)行了抽象。但它們之間的抽象存在一定的差別:BSD實現(xiàn)了更高級別的抽象,用戶可以像操作文件那樣來操作一個網(wǎng)絡(luò)連接;LWIP中,API只能實現(xiàn)較低級別的抽象,用戶操作的僅僅是一個網(wǎng)絡(luò)連接,而不是文件。在BSD中,應(yīng)用程序處理的網(wǎng)絡(luò)數(shù)據(jù)都處于一片連續(xù)的存儲區(qū)域中,可以使用戶對數(shù)據(jù)的處理更加方便。在LWIP中,若API使用上述數(shù)據(jù)存儲機制可能會導(dǎo)致很大的缺陷,因為LWIP中網(wǎng)絡(luò)數(shù)據(jù)都存儲在pbuf中,如果要實現(xiàn)存儲在連續(xù)的存儲區(qū)的話,需要將所有pbuf數(shù)據(jù)拷貝到這個連續(xù)的存儲中,這將造成數(shù)據(jù)的拷貝。為了避免數(shù)據(jù)拷貝以后再遞交給用戶,需要直接操作pbuf的一些方法,而LWIP中恰恰提供了這些方法。比如通過netbuf_next()可以修改數(shù)據(jù)指針指向下一個數(shù)據(jù)段,如果返回值為0,表示netbuf中還存在數(shù)據(jù)段,大于0說明指針已經(jīng)指向netbuf中的最后一個數(shù)據(jù)段了,小于0表明netbuf中已經(jīng)沒有數(shù)據(jù)段了。當(dāng)用戶未調(diào)用netbuf_next()函數(shù)的情況下,ptr和p都默認(rèn)指向第一個pbuf。通過netbuf_next()對協(xié)議棧和應(yīng)用程序共同緩沖區(qū)指針的調(diào)整和讀取,避免了應(yīng)用程序和數(shù)據(jù)以及內(nèi)核棧的拷貝。
在單獨運行LWIP時,用戶應(yīng)用程序和協(xié)議棧內(nèi)核處于同一進(jìn)程中,用戶程序通過回調(diào)的方式進(jìn)行。這樣,用戶程序和協(xié)議棧內(nèi)核出現(xiàn)了相互制約的關(guān)系,因為用戶程序執(zhí)行的時候,內(nèi)核一直處于等待狀態(tài),內(nèi)核需要等待用戶函數(shù)返回一個處理結(jié)果再繼續(xù)執(zhí)行。如果用戶執(zhí)行計算量很大,執(zhí)行時間很長,則協(xié)議棧代碼就一直得不到執(zhí)行,協(xié)議棧接收,處理數(shù)據(jù)包效率會受到直接的影響。最嚴(yán)重的結(jié)果是,如果發(fā)送方速度很快,則協(xié)議棧會因為來不及處理而出現(xiàn)丟包的情況。
為了設(shè)計多進(jìn)程外部應(yīng)用程序,將LWIP移植到μcos操作系統(tǒng)下,讓LWIP內(nèi)核作為操作系統(tǒng)的一個任務(wù)運行[11]。LWIP協(xié)議棧設(shè)計時,提供了協(xié)議棧與操作系統(tǒng)之間函數(shù)的接口。協(xié)議棧API由兩部分組成。一部分提供給應(yīng)用程序,一部分提供給協(xié)議棧內(nèi)核。應(yīng)用程序和協(xié)議棧內(nèi)核通過進(jìn)程間通信機制進(jìn)行通信和同步[12]。使用到的進(jìn)程通信機制包括了以下三種[13]:
(1)郵箱,例如內(nèi)核郵箱mbox、連接上接收數(shù)據(jù)的郵箱recvmbox;
(2)信號量,例如op_completed,用于兩部分API同步;
(3)共享內(nèi)存,例如內(nèi)核消息結(jié)構(gòu)tcp_msg、API消息內(nèi)容api_msg等[14]。
兩部分API間的關(guān)系如圖3所示。API設(shè)計的主要思想是讓應(yīng)用程序成為一個單獨的進(jìn)程;而協(xié)議棧也成為一個單獨的進(jìn)程。用戶進(jìn)程只負(fù)責(zé)數(shù)據(jù)的計算等其他工作,協(xié)議棧進(jìn)程僅僅負(fù)責(zé)通信工作。兩部分進(jìn)程之間使用三種IPC方式中的郵箱和信號量集,內(nèi)核進(jìn)程可以直接將數(shù)據(jù)遞交到應(yīng)用程序郵箱中,然后繼續(xù)執(zhí)行,不必阻塞等待,郵箱對于應(yīng)用程序來說就像一個輸入隊列,提高了系統(tǒng)的實時性[15]。
圖3 兩部分獨立進(jìn)程間的通信
全局郵箱mbox在協(xié)議棧初始化時建立,用于內(nèi)核進(jìn)程tcpip_thread接收消息。內(nèi)核進(jìn)程通過共享內(nèi)存的方式與協(xié)議棧的其他各個模塊進(jìn)行通信,它從郵箱中獲得的是一個指向消息結(jié)構(gòu)的指針。函數(shù)tcp_input在內(nèi)存池中為系統(tǒng)消息結(jié)構(gòu)申請空間,并根據(jù)消息類型初始化結(jié)構(gòu)中的相關(guān)字段,把內(nèi)核消息封裝在tcp_msg結(jié)構(gòu)中,最后將消息投遞到系統(tǒng)郵箱中等待內(nèi)核進(jìn)程tcpip_thread處理。tcpip_thread使用從郵箱中獲得的指針指定到對應(yīng)內(nèi)存地址處讀取消息內(nèi)容,從而避免了兩個進(jìn)程間通信的數(shù)據(jù)的拷貝。
在局域網(wǎng)內(nèi),對ARM開發(fā)板STM32F103VET6-EV上基于無操作系統(tǒng)和移植了μcos操作系統(tǒng)的LWIP兩種方法編寫的UDP服務(wù)器進(jìn)行數(shù)據(jù)吞吐能力的測試,以此來估算網(wǎng)卡及整個板子的網(wǎng)絡(luò)處理性能及對比無操作系統(tǒng)模擬層和在操作系統(tǒng)模擬層下編寫的UDP服務(wù)器性能的差別。
在Windows主機上運行iperf軟件來測試服務(wù)器的數(shù)據(jù)吞吐能力。如圖4(a)所示,在軟件上選擇UDP協(xié)議,設(shè)置好服務(wù)器IP地址(192.168.1.230)和端口號(5000)后,單擊start iperf,軟件開始對服務(wù)器性能進(jìn)行測試。從圖4(a)可以看出,服務(wù)器的上下行帶寬都可以維持在9 800 kb/s左右,很接近ENC28J60網(wǎng)卡的處理值上線10 M/s。在操作系統(tǒng)模擬層下基于LWIP零拷貝技術(shù)編寫的UDP服務(wù)器,板子的網(wǎng)絡(luò)處理性能達(dá)到最優(yōu)。從圖4(b)可以看出,基于無操作系統(tǒng)模擬層下編程的服務(wù)器在客戶端連續(xù)發(fā)送大量數(shù)據(jù)時導(dǎo)致丟包情況,嚴(yán)重情況下甚至出現(xiàn)死機的情況。
圖4 UDP性能測試
綜上所述,在應(yīng)對多個外部應(yīng)用程序的情況下,無操作系統(tǒng)模擬層的UDP服務(wù)器編程,雖然避免了數(shù)據(jù)的拷貝,但是無法應(yīng)對多個外部應(yīng)用程序。所以將LWIP移植到μcos操作系統(tǒng)下,不僅減少了內(nèi)存開銷,而且能夠應(yīng)對多個外部應(yīng)用程序。文中的研究成果已經(jīng)成功應(yīng)用于嵌入式網(wǎng)管系統(tǒng)項目并實際運行,不僅提高了基于STM32平臺μcos操作系統(tǒng)下測量儀器代理模塊的傳輸效率,提高了系統(tǒng)的實時性,而且節(jié)約了內(nèi)存開銷。