一、引言
LWIP分為四個層次:鏈路層、網絡層、傳輸層和應用層。運行LWIP的嵌入式設備可以有多個網絡接口:以太網接口、串行鏈路接口、環回接口等。為了實現對所有網絡接口的有效管理,協議棧內部使用了一個名為netif的網絡接口結構來描述各種網絡設備。本章討論的內容包括:
網絡接口管理的作用;
網絡接口結構netif;
二、網絡接口管理
1、數據結構
源文件中 netif.c 和 netif.h 文件實現了與網絡接口結構管理相關的所有數據結構和函數。來看看結構 netif 是怎樣被定義的,如下代碼所示。
————netif.h———————————————— //網絡接口最大物理地址長度,這里定義為以太網網卡 MAC 地址的長度 6 #define NETIF_MAX_HWADDR_LEN 6U //下面幾個宏為網絡接口屬性、狀態相關的宏,主要用於描述 netif 中 flags 字段的各位 #define NETIF_FLAG_UP 0x01U //網絡接口是否已被上層使能 #define NETIF_FLAG_BROADCAST 0x02U //網絡接口是否支持廣播 #define NETIF_FLAG_POINTTOPOINT 0x04U //網絡接口是否屬於點到點連接 #define NETIF_FLAG_DHCP 0x08U //網絡接口是否支持 DHCP 功能 #define NETIF_FLAG_LINK_UP 0x10U //網絡接口的底層鏈路是否已經使能 #define NETIF_FLAG_ETHARP 0x20U //網絡接口是否支持 ARP 功能 #define NETIF_FLAG_IGMP 0x40U //網絡接口是否支持 IGMP 功能 //下面是結構 netif 的定義 struct netif
{ struct netif *next; //指向下一個 netif 結構,在構成鏈表 netif_list 時使用 struct ip_addr ip_addr; //網絡接口的 IP 地址 struct ip_addr netmask; //子網掩碼 struct ip_addr gw; //網關地址 //下面為三個函數指針,調用它們指向的函數就可以完成數據包的發送或接收 err_t (* input)(struct pbuf *p, struct netif *inp); //該函數向 IP 層輸入數據包 err_t (* output)(struct netif *netif, struct pbuf *p,struct ip_addr *ipaddr); //該函數發送 IP 包 err_t (* linkoutput)(struct netif *netif, struct pbuf *p); //該函數實現底層數據包發送 void *state; //該字段用戶可以自由設置,例如用於指向一些底層設備相關的信息 u16_t mtu; //該接口允許的最大數據包長度 u8_t hwaddr_len; //該接口物理地址長度 u8_t hwaddr[NETIF_MAX_HWADDR_LEN]; //該接口的物理地址 u8_t flags; //該接口的狀態、屬性字段 char name[2]; //該接口的名字 u8_t num; //接口的編號 //在接口自輸入使能或者有環回接口的情況下,下面的字段 //用於描述接口發送給自己的數據包 struct pbuf *loop_first; //指向發送給自己的數據包的第一個 pbuf struct pbuf *loop_last; //指向發送給自己的數據包的最后一個 pbuf };
next 字段是指向下一個 netif 結構的指針,前面也說過,一個設備可能會有多個網絡接口(例如路由器),LwIP 會把所有網絡接口的 netif 結構組成一個鏈表進行管理,有一個名為 netif_list 的全局變量指向該鏈表的首部。next 字段就是用於組成鏈表時用。
ip_addr、netmask、gw 三個字段用於描述該網絡接口的網絡地址屬性,依次稱它們為接口 IP地址、子網掩碼和網關地址。IP 地址和網絡接口必須一一對應,即設備有幾個硬件網絡接口,它就得有幾個 IP 地址;子網掩碼可以用來判斷某個目的 IP 地址與當前網絡接口 IP 地址是否處於同一子網中,IP 層會選擇與目的 IP 處於同一子網的網絡接口來發送數據包。gw 字段在數據包的發送、轉發過程中也有重要作用,如果目的 IP 地址與所有網絡接口都不屬於同一子網,LwIP 將會把數據包發送到網關處,因為它認為網關設備會對該 IP 包進行正確的轉發,此外網關也為設備提供了許多高級服務,如 DHCP、DNS 等。
input 字段指向一個函數,這個函數將網絡設備接收到的數據包提交給 IP 層(在以太網中,通常這個函數需要解析以太網數據幀,然后將從中得到的 IP 數據包遞交)。被指向的函數具有兩個參數:一個是 pbuf 類型,代表將要遞交的數據包;另一個為 netif 類型,代表遞交數據包的網絡設備,函數返回值是 err_t 類型。網絡設備初始化時應該向 input 字段注冊相應的輸入函數,后面將詳細討論這個問題。
output 字段指向一個函數,這個函數和具體網絡接口設備驅動密切相關,它用於將 IP 層數據包發送到目的地址處。在 IP 層看來,每個網絡接口都會提供一個這樣的函數供它調用,當 IP 層發送數據包時,它會遍歷 netif_list 鏈表,找出最合適的網絡接口,並調用其注冊的 output 函數發送 數據包。不同的網絡接口需要根據實際接口特性來編寫相關的發送函數,並在初始化時將 output字段指向該函數。就以太網網卡的數據包發送而言,LwIP 在源代碼 etharp.c 文件中實現了一個名為 etharp_output 的函數,該函數就可以直接完成 IP 數據包在以太網中的發送,所以在初始化以太網卡結構時,可以直接將 output 字段指向函數 etharp_output。output 指向函數的三個參數分別是 pbuf類型、netif 類型和 ip_addr 類型,返回值是 err_t 類型。其中 pbuf 代表要發送的 IP 數據包,ipaddr代表數據包的目的 IP 地址
linkoutput 字段和上面的 output 基本上是相似的功能,但是更底層一些,這個函數主要在以太網卡通信中被 ARP 模塊調用,用來完成以太網數據幀的發送,在其他類型的網絡接口中,這個字段的值就沒啥用處了。上面說的函數 etharp_output,它一方面接收 IP 層數據包,另一方面將該數據包封裝為以太網數據幀(填寫物理地址等字段),最后便調用 linkoutput 字段注冊的函數將以太網 數 據 幀 發 送 出 去 。
為了實現對這些數據包的管理,在網絡接口結構 netif 中定義了兩個指針 loop_first 和loop_last,它們分別用於指向數據包 pbuf 鏈表的第一個 pbuf 和最后一個 pbuf。這里的 pbuf 鏈表比較特殊,因為所有數據包的 pbuf 都組織在同一條鏈表上,這就說明一條 pbuf 鏈表上可能存在多個 數據包,這時 pbuf 結構的 next 字段是否為空並不能成為判斷一個數據包結束與否的標志,那在這樣的一條 pbuf 鏈表中,怎樣去將各個數據包的 pbuf 划分開呢?答案在於 pbuf 結構中的 tot_len 字段和 len 字段值,當兩個字段的值相等時,就代表一個數據包的結束。
2、函數實現
向系統注冊一個網絡接口設備的函數 netif_add:
————netif.c———————————————— //定義兩個全局型的 netif 指針 struct netif *netif_list; //系統的全局型 netif 鏈表 struct netif *netif_default; //記錄系統缺省(默認)網絡接口 //函數功能:向 LwIP 內核注冊一個網絡接口結構 //參數 netif:指向一個已分配好的 netif 結構體 //參數 ipaddr:網絡接口的 IP 地址 //參數 netmask:網絡接口子網掩碼 //參數 gw:網關地址 //參數 state:用戶自定義的一些數據信息 //參數 init:網絡接口的初始化函數 //參數 input:網絡接口向 IP 層提交數據包的函數 //返回值:成功注冊的網絡接口結構指針 struct netif *netif_add(struct netif *netif, struct ip_addr *ipaddr, struct ip_addr *netmask, struct ip_addr *gw, void *state, err_t (* init)(struct netif *netif), err_t (* input)(struct pbuf *p, struct netif *netif)) { static u8_t netifnum = 0; //定義靜態變量 netifnum,它記錄網絡接口的編號 //清空 netif 結構的各個字段值 netif>ip_addr.addr = 0; netif>netmask.addr = 0; netif>gw.addr = 0; netif>flags = 0; netif>loop_first = NULL; netif>loop_last = NULL; //填寫結構體各個字段值 netif>state = state; netif>num = netifnum++; //網絡接口編號 netif>input = input; //注冊 input 函數 //調用函數 netif_set_addr 設置網絡接口的三個地址字段值 netif_set_addr(netif, ipaddr, netmask, gw); //調用網絡接口的初始化函數,初始化網絡接口 if (init(netif) != ERR_OK)
{
//初始化失敗,則返回空 return NULL; } //將初始化成功的 netif 結構加入 netif_list 鏈表 netif>next = netif_list; netif_list = netif; //返回 netif 結構指針 return netif; }
netif_add 函數只是簡單的初始化了 netif 結構的幾個字段,然后回調網絡接口定義的初始化函數 init 來完成網絡接口的初始化工作。
IP4_ADDR(&gw, 192,168,1,1); //初始化三個地址 IP4_ADDR(&ipaddr, 192,168,1,37); IP4_ADDR(&netmask, 255,255,255,0); //調用 netif_add 函數 netif_add(&rtl8019_netif, &ipaddr, &netmask, &gw, NULL, ethernetif_init,ethernet_input); netif_set_default(&rtl8019_netif); //設置系統的默認網絡接口為 rtl8019_netif netif_set_up(&rtl8019_netif); //使能網絡接口 rtl8019_netif
在上面這段代碼中,調用 netif_add 函數時,傳遞給它的兩個函數參數是 ethernetif_init 和ethernet_input,其中前者就是網卡初始化函數 ethernetif_init,這是源碼提供者為以太網網卡驅動程序編寫的默認初始化函數,這個函數在第 5 章中也有所提及,這里來看看它的具體實現,源代碼如下所示。ethernet_input 是 ARP 層的一個函數,它的功能是提取以太網幀中的 ARP 地址數據,並將幀中的 IP 數據遞交給 IP 層,關於這個函數在下一章中會講解。
#define IFNAME0 'e' //定義以太網網卡的名字字符 #define IFNAME1 'n' //定義描述網卡用戶信息的結構,該結構無實際用處,源碼作者旨在用該結構來示意 //netif 結構中的 state 字段的用法,該字段可以指向任何用戶關心的信息 struct ethernetif
{ //該結構只包含一個簡單的指針 struct eth_addr *ethaddr; }; //函數參數 netif:網絡接口結構的指針 err_t ethernetif_init(struct netif *netif) { struct ethernetif *ethernetif; ethernetif = mem_malloc(sizeof(struct ethernetif)); //為用戶信息結構申請一個內存堆空間 if (ethernetif == NULL) { //申請失敗,返回內存錯誤 return ERR_MEM; } netif>state = ethernetif; //將 state 字段指向 ethernetif netif>name[0] = IFNAME0; //設置名字字段 netif>name[1] = IFNAME1; netif>output = etharp_output; //注冊 IP 數據包輸出函數,這里使用 ARP 的相關函數 netif>linkoutput = low_level_output; //注冊以太網數據幀輸出函數 //將 ethernetif 結構賦一個用戶關心的值,這里無具體意義 ethernetif>ethaddr = (struct eth_addr *)&(netif>hwaddr[0]); //指向網卡的地址信息 low_level_init(netif); //調用網卡底層初始化函數,這個函數已經講過了 return ERR_OK; //返回成功 }