1、LWIP的結構
lwip是瑞典計算機科學院(SICS)的Adam Dunkels 開發的一個小型開源的TCP/IP協議棧。實現的重點是在保持TCP協議主要功能的基礎上減少對RAM 的占用。
LWIP(Light weight internet protocol)的主要模塊包括:配置模塊、初始化模塊、NetIf模塊、mem(memp)模塊、netarp模塊、ip模塊、udp模塊、icmp 模塊、igmp模塊、dhcp模塊、tcp模塊、snmp模塊等。下面主要對我們需要關心的協議處理進行說明和梳理。
配置模塊:
配置模塊通過各種宏定義的方式對系統、子模塊進行了配置。比如,通過宏,配置了mem管理模塊的參數。該配置模塊還通過宏,配置了協議棧所支持的協議簇,通過宏定制的方式,決定了支持那些協議。主要的文件是opt.h。
初始化模塊:
初始化模塊入口的文件為tcpip.c,其初始化入口函數為: void tcpip_init(void (* initfunc)(void *), void *arg)
該入口通過調用lwip_init()函數,初始化了所有的子模塊,并啟動了協議棧管理進程。同時,該函數還帶有回調鉤子及其參數。可以在需要的地方進行調用。
協議棧數據分發管理進程負責了輸入報文的處理、超時處理、API函數以及回調的處理,原型如下:
static void tcpip_thread(void *arg)
NetIf模塊:
Netif模塊為協議棧與底層驅動的接口模塊,其將底層的一個網口設備描述成協議棧的一個接口設備(net interface)。該模塊的主要文件為netif.c。其通過鏈表的方式描述了系統中的所有網口設備。
Netif的數據結構描述了網口的參數,包括IP地址、MAC地址、link狀態、網口號、收發函數等等參數。一個網口設備的數據收發主要通過該結構進行。
Mem(memp)模塊:
Mem模塊同一管理了協議棧使用的內容緩沖區,并管理pbuf結構以及報文的字段處理。主要的文件包括mem.c、memp.c、pbuf.c。
netarp模塊:
netarp模塊是處理arp協議的模塊,主要源文件為etharp.c。其主要入口函數為: err_t ethernet_input(struct pbuf *p, struct netif *netif)
該入口函數通過判斷輸入報文p的協議類型來決定是按照arp協議進行處理還是將該報文提交到IP協議。如果報文是arp報文,該接口則調用etharp_arp_input,進行arp請求處理。
如果是ip報文,該接口就調用etharp_ip_input進行arp更新,并調用ip_input接口,將報文提交給ip層。
在該模塊中,創建了設備的地址映射arp表,并提供地址映射關系查詢接口。同時還提供了arp報文的發送接口。如下
err_t etharp_output(struct netif *netif, struct pbuf *q, struct ip_addr *ipaddr)
該接口需要注冊到netif的output字段,ip層在輸出報文時,通過該接口獲取目標機的MAC地址,組合最終報文后,由該接口調用底層設備的驅動接口發送數據。
在etharp_output接口中,判斷報文類型,如果是廣播包或者組播包,就調用etharp_send_ip(組裝目標mac和源mac)接口,etharp_send_ip調用netif結構中的設備驅動注冊的linkoutput鉤子函數發送最終報文。如果是單播包,etharp_output接口就調用etharp_query進行ip地址和MAC地址的映射,來獲取到目標機的MAC地址。并在etharp_query中調用etharp_send_ip來發送最終組合報文。
ip模塊:
ip模塊實現了協議的ip層處理,主要文件為ip.c。其主要入口函數為: err_t ip_input(struct pbuf *p, struct netif *inp)
該接口通過判斷輸入報文的協議類型,將其輸入到相應的上層協議模塊中去。比如,將udp報文送到udp_input。
該模塊另外一個接口是輸入函數,原型如下:
err_t ip_output(struct pbuf *p, struct ip_addr *src, struct ip_addr *dest, u8_t ttl, u8_t
tos, u8_t proto)
該接口通過路由表或者傳輸ip后,調用netif的output字段函數鉤子發送報文。
udp模塊:
udp模塊實現了udp協議層的協議處理,主要文件為udp.c。該模塊通過PCB控制塊將應用端口跟應用程序做了綁定。在接收到新報文時,分析其對應的PCB,找到對應的處理鉤子,進行應用的處理。主要入口函數為:
void udp_input(struct pbuf *p, struct netif *inp) 該模塊負責輸出的接口如下:
err_t udp_send(struct udp_pcb *pcb, struct pbuf *p)
該模塊負責將一個PCB跟一個本地端口進行綁定的接口如下:
err_t udp_bind(struct udp_pcb *pcb, struct ip_addr *ipaddr, u16_t port) 該模塊負責將一個PCB跟一個遠端端口綁定的接口如下:
err_t udp_connect(struct udp_pcb *pcb, struct ip_addr *ipaddr, u16_t port)
igmp模塊:
igmp模塊負責分組管理。其主要的接口函數如下:
void igmp_input(struct pbuf *p, struct netif *inp, struct ip_addr *dest) 該接口負責IGMP協議報文的處理,比如分析當前報文是請求還是應答。 err_t igmp_joingroup(struct ip_addr *ifaddr, struct ip_addr *groupaddr) 該接口將一個網口加入一個組。
err_t igmp_leavegroup(struct ip_addr *ifaddr, struct ip_addr *groupaddr) 該接口將一個網口從一個組中移出。
dhcp模塊:
dhcp模塊用于獲取設備ip地址的相關信息。其處理入口主要有這么幾個:dpch的啟動、dpch的接收報文處理以及定時器模塊的處理。
主要的接口原型如下:
err_t dhcp_start(struct netif *netif)
該接口用于設備啟動dhcp模塊,主要是客戶端的功能。該模塊實現設備dhcp描述結構生成,并將dhcp的端口綁定到udp協議中,以及將本dhcp模塊跟遠端服務器端口進行綁定。最后啟動dhcp申請。
static void dhcp_recv(void *arg, struct udp_pcb *pcb, struct pbuf *p, struct ip_addr *addr, u16_t port)
該接口為一個注冊接口,用于dhcp報文接收。在start dhcp時,該接口通過dhcp的udp pcb注冊到udp協議層。Udp進行報文處理后,根據端口調用該注冊接口。該接口中,實現dhcp報文的協議處理。
Void dhcp_fine_tmr() Void dhcp_coarse_tmr()
這兩個函數接口實現了dhcp的相關超時處理監控。上面一個用于請求應答超時處理。下面一個用于地址租用情況的到期處理。
從源碼分析看,上述的接口在應用lwip的協議棧時,需要重點關注。對于小內存應用的場合,該協議棧的內存管理以及pbuf應用部分需要自行改寫。
2、lwip特征
(1)支持多網絡接口下的IP轉發;
(2)支持ICMP協議;
(3)包括實驗性擴展的UDP(用戶數據報協議);
(4)包括阻塞控制、RTT 估算、快速恢復和快速轉發的TCP(傳輸控制協議);
(5)提供專門的內部回調接口(Raw API),用于提高應用程序性能;
(6)可選擇的Berkeley接口API (在多線程情況下使用) 。
(7)在最新的版本中支持ppp
(8) 新版本中增加了的IP fragment的支持。
(9) 支持DHCP協議,動態分配ip地址.
3、LWIP的協議流程
下面這張圖比較清楚的描述了lwip的報文處理流程,呵呵,借用一下。不過,其對netif-》output描述不夠。從代碼看,該output實際是arp層的輸出,最后通過arp層調用netif中的底層輸出接口發送報文。
四、LwIP的API的實現
LwIP的API的實現主要有兩部分組成:一部分駐留在用戶進程中,一部分駐留在TCP/IP協議棧進程中。這兩個部分間通過操作系統模擬層提供的進程通信機制(IPC)進行通信,從而完成用戶進程與協議棧間的通信,IPC包括共享內存、消息傳遞和信號量。通常盡可能多的工作在用戶進程內的API部分實現,例如運算量及時間開銷大的工作;TCP/IP協議棧進程中的API部分只完成少量的工作,主要是完成進程間的通訊工作。兩部分API之間通過共享內存傳遞數據,對于共享內存區的描述是采用和pbuf類似的結構來實現。綜上,可以用簡單的一段話來描述這種API實現的機制:API 函數庫中處理網絡連接的函數駐留在 TCP/IP 進程中。位于應用程序進程中的API函數使用郵箱這種通訊協議向駐留在TCP/IP 進程中的API函數傳遞消息。這個消息包括需要協議棧執行的操作類型及相關參數。駐留在 TCP/IP 進程中的API函數執行這個操作并通過消息傳遞向應用程序返回操作結果。
很早以前描述過結構pbuf,它是協議棧內部用來描述數據包的一種方式。這里介紹數據結構netbuf,它是API用來描述數據的一種方式。netbuf是基于pbuf來實現的,其結構如下所示,很明顯,它就是包含了幾個典型結構的指針。
struct netbuf {
struct pbuf *p, *ptr;
struct ip_addr *addr;
u16_t port;
};
注意,這里的netbuf只是相當于一個數據頭,而真正保存數據的還是字段p指向的pbuf鏈表。字段ptr也是指向該netbuf的pbuf鏈表,但與p的區別在于:p一直指向pbuf鏈表中的第一個pbuf結構,而ptr則不然,它可能指向鏈表中的其他位置,源文檔里面把它描述為fragment pionter,與該指針調整密切相關的函數是netbuf_next和netbuf_first。還有兩個字段addr和port分別表示發出netbuf數據端的IP地址和端口號,這兩個字段實際用處似乎不大,API定義了宏netbuf_fromaddr和netbuf_fromport分別用于返回某個netbuf結構中這兩個字段的值。
與netbuf相關的處理函數很多,在源文檔15.2節中對每個函數也有了詳細的說明,這里說說比較重要的幾個。netbuf_new用于分配一個新的netbuf結構,注意這里只是一個頭部結構,而真正需要的存儲數據區域是在函數netbuf_alloc中分配的,同理函數netbuf_delete用于刪除一個netbuf結構,同時函數pbuf_free會被調用,用以刪除數據區域的空間。以上這幾個函數使用的簡單例子如下:
struct netbuf *buf; // 申明指針
buf = netbuf_new(); // 申請新的netbuf
netbuf_alloc(buf, 200); // 為netbuf分配200 字節的空間
。。。。 // 使用buf做相關事情
netbuf_delete(buf); // 刪除buf
講過了API的內存管理,再來講具體的API函數。與前面的對應,API函數由兩部分組成,分別在文件api_lib.c和api_msg.c中。前者包括了用戶程序直接調用的API接口函數,后者包括了與協議棧進程通信的API函數,用戶程序不可直接調用。這兩部分API函數之間通過郵箱傳遞的消息進行通信。這里又要涉及到兩個重要的數據結構:API應用接口部分提供給上層應用以描述一個網絡連接的數據結構netconn,以及描述兩部分API函數間傳遞的消息結構的api_msg。
應用程序要使用API函數建立一個連接連接,首先應該建立一個netconn的結構來描述這個連接的各種屬性。netconn結構如下,其中去掉了不必要的編譯選項和注釋。
struct netconn {
enum netconn_type type; // 連接的類型,包括TCP, UDP等
enum netconn_state state; // 連接的狀態
union { // 共用體,內核用來描述某個連接
struct ip_pcb *ip;
struct tcp_pcb *tcp;
struct udp_pcb *udp;
struct raw_pcb *raw;
} pcb;
err_t err; // 該連接最近一次發生錯誤的編碼
sys_sem_t op_completed; // 用于兩部分API間同步的信號量
sys_mbox_t recvmbox; // 接收數據的郵箱
sys_mbox_t acceptmbox; // 服務器用來接受外部連接的郵箱
int socket; // 該字段只在socket實現中使用
u16_t recv_avail; //
struct api_msg_msg *write_msg; // 對數據不能正常處理時,保存信息
int write_offset; // 同上,表示已經處理數據的多少
netconn_callback callback; // 回調函數,在發生與該netconn相關的事件時可以調用
};
write_msg是api_msg_msg類型的指針,該結構后續講到;netconn_callback是API定義的一種函數指針類型,其定義為:
typedef void (* netconn_callback)(struct netconn *, enum netconn_evt, u16_t len);
關鍵字typedef的功能是定義新的類型。這里它用于定義一種netconn_callback的類型,這種類型為指向某種函數的指針,且這種函數有三個類型的輸入參數,返回類型為void。在這定義以后就可以像使用int,char一樣使用netconn_callback,也即它也表示一種數據類型了。所以上面的callback字段表示一個函數指針,當把某個函數名賦給該字段后,該字段就記錄了這個函數的起始地址。
netconn的其他字段后面用到時再詳解。接下來看看兩部分API函數間傳遞的消息結構的api_msg:
struct api_msg {
void (* function)(struct api_msg_msg *msg); // 函數指針
struct api_msg_msg msg; // 函數執行時需要的參數
}
字段function是一個函數指針,通常當消息被構造時,該字段被填充為api_msg.c中的某個與協議棧接口的API函數,然后該消息被投遞到協議棧進程中進行處理,協議棧進程解析出并執行該函數,這樣應用程序與協議棧之間的通信就完成了。字段msg中包含了這個函數執行時需要的所有參數,api_msg_msg是個枚舉類型,所以對于不同的function,msg有著不同的結構。api_msg_msg結構如下所示:
struct api_msg_msg {
struct netconn *conn; // 與消息相關的某個連接
union {
struct netbuf *b; // 函數do_send的參數
struct { // 函數do_newconn的參數
u8_t proto;
} n;
struct { // 函數do_bind和do_connect的參數
struct ip_addr *ipaddr;
u16_t port;
} bc;
struct { // 函數do_getaddr的參數
struct ip_addr *ipaddr;
u16_t *port;
u8_t local;
} ad;
struct { // 函數do_write的參數
const void *dataptr;
int len;
u8_t apiflags;
} w;
struct { // 函數do_recv的參數
u16_t len;
} r;
} msg;
};
這個結構體只包含了兩個字段:描述連接信息的conn和枚舉類型msg。在api_msg_msg中保存conn字段是必須的,因為conn結構中包含了與該連接相關的信箱和信號量等信息,協議棧進程要用這些信息來完成與應用進程間的同步與通信。枚舉類型msg的各個成員與調用它的函數密切相關。因此在構建一個消息時,API應首先填充消息的function字段,并針對特定的function種類往api_msg_msg的枚舉字段寫入參數,函數function被協議棧解析執行時,直接按照自己已定的格式取參數。這意味著,對于構造消息的API函數和解析消息的function函數,它們對參數個數、結構的認識是預先約定的,不能有其他變數。這一點體現出LwIP呆板僵硬的一面。
評論