趙成青,李宥謀,劉永斌,王 濤
(西安郵電大學,陜西 西安 710000)
LWIP是瑞典計算機科學院(SICS)的Adam Dunkels開發的用于嵌入式系統的開源TCP/IP協議棧[1]。LWIP的含義是輕量級的TCP/IP協議,專注于減少資源消耗。嵌入式網絡傳輸系統由于成本資源的限制,往往采用簡化的TCP/IP協議。文中通過研究、分析常用的嵌入式網絡協議棧LWIP的結構,在物理層和應用層提出了提高系統傳輸效率的改進方法。
在小型嵌入式系統中,LWIP的實現基于上層協議已明確知道下層協議所使用的數據結構的特點[2]。它會假設各層間的部分數據結構和實現原理在其他層是可見的。在數據包遞交過程中,各層協議可以通過指針直接指向數據包中其他層次的字段。所以上層可直接使用取地址計算得到下層中的數據,這不僅使整個協議棧對數據包的操作更加靈活,而且避免了LWIP協議棧內部數據遞交時的復制。但是,這僅僅是在LWIP協議棧內部實現數據的零拷貝。在物理網卡向協議棧傳遞數據時和協議棧向應用程序傳遞數據時,還是存在兩次消耗較大的數據拷貝過程。所以,文中提出在網卡接收數據時讓LWIP內核存儲區指針直接指向物理網卡的寄存器地址的方法,避免了物理網卡數據到LWIP協議棧緩沖區數據的拷貝[3]。在應用層,提出了利用μcos操作系統的郵箱機制,避免了多個外部應用程序和協議棧內核交互時的數據拷貝[4],從而實現了從物理層到應用層真正的數據零拷貝,并且提高了系統的并發性。
在嵌入式系統中應用比較廣泛的是MicroChip公司的ENC28J60網卡。在網卡驅動函數接收數據包時,網卡驅動函數首先向網卡發送數據包傳送指令,此時網卡會把BNRY(邊界寄存器)處的一個網卡格式的數據包數據一次性全部發送到DMA端口,此時網卡驅動函數會在DMA端口讀取所有字節數據后,網卡會自動將BNRY(邊界寄存器)值調整為下一個數據包地址,以準備下一次讀取所有字節數據。然后將接收到的數據封裝成LWIP熟悉的格式并且寫到緩沖區中,這個過程涉及到數據到拷貝[5]。在ENC28J60網卡中接收緩沖器由一個硬件管理的循環FIFO構成。ERXST表示接收緩沖區起始地址,ERXNDH表示接收緩沖區結束地址[6]。如圖1所示,通過把網卡相關的寄存器映射到LWIP內核內存空間,就可以直接對網卡寄存器進行操作,避免了物理網卡到LWIP內核空間的數據拷貝。然后封裝成LWIP內核能夠識別的pbuf類型的數據包結構,在LWIP內核中由low_level_input()函數完成這個功能。通過ehternetif_input()函數解析該數據包的類型,然后將該數據包指針遞交給相應的上層。
在網卡接收數據時,需要申請一個數據包,然后將網卡中的數據填入數據包中。發送數據包時,協議棧的某層中會申請一個pbuf,并將相應的數據裝入到數據區域,同時相關的協議首部信息也會被填寫到pbuf的預留數據區域中[7]。數據包申請函數有兩個重要參數,一個是想申請的數據包pbuf類型,另一個重要參數是該數據包是在協議棧中哪一層被申請的,分配函數會根據這個層次的不同,在pbuf數據區域前為相應的協議預留出首部空間,這就是offset值。總的來說,LWIP定義了四個層次,當數據包申請時,所處的層次不同,會導致預留空間的offset值不同[8]。層次定義時通過一個枚舉類型pbuf_layer:
Typedef enum{
PBUF_TRANSPORT,//傳輸層
PBUF_IP,//網絡層
PBUF_LINK,//鏈路層
PBUF_RAW,//原始層
}pbuf_layer;
上面的結構體中除了定義枚舉類型pbuf_layer來表示各個網絡協議層的名稱外,還定義了兩個宏: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的數據包管理函數pbuf.c中,首先根據數據包申請時傳入的協議層參數,計算需要在pbuf數據區簽預留的長度offset值,然后根據pbuf的類型進行實際申請。Pbuf_pooll類型申請最復雜[9],因為可能需要幾個pool連接在一起,以此來滿足用戶的空間需求。
限于篇幅,對LWIP內存分配機制不做深入研究;pbuf_ref和pbuf_rom類型申請最簡單,它們只是在內存MEMEP_PBUF中分配一個pbuf結構空間,然后初始化相關字段,注意這兩種類型的payload指針需要用戶自行設置,通常在調用完函數pbuf_alloc后,調用者需要將payload指向某個數據區。
在原始層以太網驅動中:
P=pbuf_alloc(PBUF_RAW,recvlen,PBUF_RAM);
這個調用語句申請了一個PBUF_RAM類型的pbuf,且其申請的協議層為PBUF_RAW,所以pbuf_alloc函數不會在數據區前預留出任何首部空間;通過使用p->payload,就可以實現對pbuf中數據區的讀取或者寫入操作了。
在傳輸層TCP層:
P=pbuf_alloc(PBUF_RAW,recvlen,PBUF_RAM);
它告訴數據包分配函數使用PBUF_RAM類型的pbuf,且數據前應該預留一部分的首部空間。由于這里是PBUF_TRANSPORT層,所以預留空間將有54個字節,即TCP首部長度的20個字節、IP數據包首部長度的20個字節以及以太網幀首部長度的14字節。當數據包往下層遞交,各層協議就直接操作這些預留空間的數據,以實現數據首部的填寫,這樣就避免了數據的拷貝。
協議棧API實現時,也為用戶提供了數據包管理函數,可以完成數據包內存申請、釋放、數據拷貝等任務。無論是UDP還是TCP連接,當協議棧接收到數據包后,會將數據封裝在一個netbuf中,并遞交給應用程序[10]。在發送數據時,不同類型的連接將導致不同的數據處理方式。對于TCP連接,內核會根據用戶提供待發送數據的起始數據和長度,自動將數據封裝在合適的數據包中,然后放入發送隊列;對于UDP,用戶需要手動將數據封裝在netbuf中,通過調用發送函數,內核直接發送數據包中的數據段。
應用程序使用netbuf結構來描述、組裝數據包,該結構只是對內核pbuf的簡單封裝,是用戶應用程序和協議棧共享的。外部應用程序可以使用該結構來管理發送數據、接收數據的緩沖區。netbuf是基于pbuf實現的,其結構如以下代碼所示:
Struct netbuf{
Struct pbuf *p,*ptr;
Ip_addr_t *addr;
U16_t port;
}
其中,netbuf相當于一個數據首部,保存數據的字段是p,它指向pbuf鏈表首部,ptr指向鏈表中的其他位置,addr表示IP地址,port表示端口號。
netbuf是應用程序描述待發送數據和已接收數據的基本結構,引入netbuf結構看似會讓應用程序更加繁雜,但實際上內核為應用程序提供了API,通過共享一個netbuf結構(如圖2所示),兩部分API就能實現對數據包的共同處理,避免了數據拷貝。

圖2 用戶緩沖區結構
與BSD相同,LWIP協議棧API也對網絡連接進行了抽象。但它們之間的抽象存在一定的差別:BSD實現了更高級別的抽象,用戶可以像操作文件那樣來操作一個網絡連接;LWIP中,API只能實現較低級別的抽象,用戶操作的僅僅是一個網絡連接,而不是文件。在BSD中,應用程序處理的網絡數據都處于一片連續的存儲區域中,可以使用戶對數據的處理更加方便。在LWIP中,若API使用上述數據存儲機制可能會導致很大的缺陷,因為LWIP中網絡數據都存儲在pbuf中,如果要實現存儲在連續的存儲區的話,需要將所有pbuf數據拷貝到這個連續的存儲中,這將造成數據的拷貝。為了避免數據拷貝以后再遞交給用戶,需要直接操作pbuf的一些方法,而LWIP中恰恰提供了這些方法。比如通過netbuf_next()可以修改數據指針指向下一個數據段,如果返回值為0,表示netbuf中還存在數據段,大于0說明指針已經指向netbuf中的最后一個數據段了,小于0表明netbuf中已經沒有數據段了。當用戶未調用netbuf_next()函數的情況下,ptr和p都默認指向第一個pbuf。通過netbuf_next()對協議棧和應用程序共同緩沖區指針的調整和讀取,避免了應用程序和數據以及內核棧的拷貝。
在單獨運行LWIP時,用戶應用程序和協議棧內核處于同一進程中,用戶程序通過回調的方式進行。這樣,用戶程序和協議棧內核出現了相互制約的關系,因為用戶程序執行的時候,內核一直處于等待狀態,內核需要等待用戶函數返回一個處理結果再繼續執行。如果用戶執行計算量很大,執行時間很長,則協議棧代碼就一直得不到執行,協議棧接收,處理數據包效率會受到直接的影響。最嚴重的結果是,如果發送方速度很快,則協議棧會因為來不及處理而出現丟包的情況。
為了設計多進程外部應用程序,將LWIP移植到μcos操作系統下,讓LWIP內核作為操作系統的一個任務運行[11]。LWIP協議棧設計時,提供了協議棧與操作系統之間函數的接口。協議棧API由兩部分組成。一部分提供給應用程序,一部分提供給協議棧內核。應用程序和協議棧內核通過進程間通信機制進行通信和同步[12]。使用到的進程通信機制包括了以下三種[13]:
(1)郵箱,例如內核郵箱mbox、連接上接收數據的郵箱recvmbox;
(2)信號量,例如op_completed,用于兩部分API同步;
(3)共享內存,例如內核消息結構tcp_msg、API消息內容api_msg等[14]。
兩部分API間的關系如圖3所示。API設計的主要思想是讓應用程序成為一個單獨的進程;而協議棧也成為一個單獨的進程。用戶進程只負責數據的計算等其他工作,協議棧進程僅僅負責通信工作。兩部分進程之間使用三種IPC方式中的郵箱和信號量集,內核進程可以直接將數據遞交到應用程序郵箱中,然后繼續執行,不必阻塞等待,郵箱對于應用程序來說就像一個輸入隊列,提高了系統的實時性[15]。

圖3 兩部分獨立進程間的通信
全局郵箱mbox在協議棧初始化時建立,用于內核進程tcpip_thread接收消息。內核進程通過共享內存的方式與協議棧的其他各個模塊進行通信,它從郵箱中獲得的是一個指向消息結構的指針。函數tcp_input在內存池中為系統消息結構申請空間,并根據消息類型初始化結構中的相關字段,把內核消息封裝在tcp_msg結構中,最后將消息投遞到系統郵箱中等待內核進程tcpip_thread處理。tcpip_thread使用從郵箱中獲得的指針指定到對應內存地址處讀取消息內容,從而避免了兩個進程間通信的數據的拷貝。
在局域網內,對ARM開發板STM32F103VET6-EV上基于無操作系統和移植了μcos操作系統的LWIP兩種方法編寫的UDP服務器進行數據吞吐能力的測試,以此來估算網卡及整個板子的網絡處理性能及對比無操作系統模擬層和在操作系統模擬層下編寫的UDP服務器性能的差別。
在Windows主機上運行iperf軟件來測試服務器的數據吞吐能力。如圖4(a)所示,在軟件上選擇UDP協議,設置好服務器IP地址(192.168.1.230)和端口號(5000)后,單擊start iperf,軟件開始對服務器性能進行測試。從圖4(a)可以看出,服務器的上下行帶寬都可以維持在9 800 kb/s左右,很接近ENC28J60網卡的處理值上線10 M/s。在操作系統模擬層下基于LWIP零拷貝技術編寫的UDP服務器,板子的網絡處理性能達到最優。從圖4(b)可以看出,基于無操作系統模擬層下編程的服務器在客戶端連續發送大量數據時導致丟包情況,嚴重情況下甚至出現死機的情況。

圖4 UDP性能測試
綜上所述,在應對多個外部應用程序的情況下,無操作系統模擬層的UDP服務器編程,雖然避免了數據的拷貝,但是無法應對多個外部應用程序。所以將LWIP移植到μcos操作系統下,不僅減少了內存開銷,而且能夠應對多個外部應用程序。文中的研究成果已經成功應用于嵌入式網管系統項目并實際運行,不僅提高了基于STM32平臺μcos操作系統下測量儀器代理模塊的傳輸效率,提高了系統的實時性,而且節約了內存開銷。