今天我們來討論LWIP是怎樣來處理與底層硬件,即網卡芯片間的關係的。
爲什麼要首先討論這個問題呢?與許多其他的TCP/IP實現一樣,LWIP也是以分層的協議爲參照來設計實現TCP/IP的。LWIP從邏輯上看分爲四層:鏈路層、網絡層、傳輸層和應用層。注意,雖然LWIP也採用了分層機制,但它沒有在各層之間進行嚴格的劃分,各層協議之間可以進行或多或少的交叉存取,即上層可以意識到下層協議所使用的緩存處理機制。因此各層可以更有效地重用緩衝區。而且,應用進程和協議棧代碼可以使用相同的內存,應用可以直接讀寫內部緩存,因此節省了執行拷貝的開銷。我們將從LWIP的最底層鏈路層起步,開始整個LWIP內部協議之旅。
在LWIP中,是通過一個叫做netif
的網絡結構體來描述一個硬件網絡接口的。這個接口結構比較簡單,下面我們從源代碼結構來分析分析這個結構:
struct netif {
struct netif *next; // 指向下一個netif結構的指針
struct ip_addr ip_addr; // IP地址相關配置
struct ip_addr netmask;
struct ip_addr gw;
err_t (* input)(struct pbuf *p, struct netif *inp); //調用這個函數可以從網卡上取得一個數據包
err_t (* output)(struct netif *netif, struct pbuf *p, // IP層調用這個函數可以向網卡發送一個數據包
struct ip_addr *ipaddr);
err_t (* linkoutput)(struct netif *netif, struct pbuf *p); // ARP模塊調用這個函數向網卡發送一個數據包
void *state; // 用戶可以獨立發揮該指針,用於指向用戶關心的網卡信息
u8_t hwaddr_len; // 硬件地址長度,對於以太網就是MAC地址長度,爲6各字節
u8_t hwaddr[NETIF_MAX_HWADDR_LEN]; //MAC地址
u16_t mtu; // 一次可以傳送的最大字節數,對於以太網一般設爲1500
u8_t flags; // 網卡狀態信息標誌位
char name[2]; // 網絡接口使用的設備驅動類型的種類
u8_t num; // 用來標示使用同種驅動類型的不同網絡接口
};
next字段是指向下一個netif
結構的指針。我們的一個產品可能會有多個網卡芯片,LWIP會把所有網卡芯片的結構體鏈成一個鏈表進行管理,有一個netif_list
的全局變量指向該鏈表的頭部。next
字段就是用於鏈表用。
ip_addr、netmask、gw三個字段用於發送和處理數據包用,分別表示IP地址、子網掩碼和網關地址。前兩個字段在數據包發送時有重要作用,第三個字段似乎沒什麼用。IP地址和網卡設備必須一一對應。如果你連什麼叫IP地址、子網掩碼和它們的作用都不曉得,那你有必要去看看TCP/IP協議詳解卷1第三章。
input字段指向一個函數,這個函數將網卡設備接收到的數據包提交給IP層,使用時將input
指針指向該函數即可,後面將詳細討論這個問題。該函數的兩個參數是pbuf類型和netif
類型的,返回參數是err_t
類型。其中pbuf代表接收到的數據包。
output字段向一個函數,這個函數和具體網絡接口設備驅動密切相關,它用於IP層將一個數據包發送到網絡接口上。用戶需要根據實際網卡編寫該函數,並將output
字段指向該函數。該函數的三個參數是pbuf
類型、netif
類型和ip_addr
類型,返回參數是err_t
類型。其中pbuf
代表要發送的數據包。ipaddr
代表網卡需要將該數據包發送到的地址,該地址應該是接收實際的鏈路層幀的主機的 IP 地址,而不一定爲數據包最終需要到達的IP地址。例如,當要發送 IP信息包到一個並不在本地網絡裏的主機上時,鏈路層幀會被髮送到網絡裏的一個路由器上。在這種情況下,給 output
函數的 IP
地址將是這個路由器的地址。
linkoutput字段和上面的output
基本上是起相同的作用,但是這個函數是在ARP
模塊中被調用的,這裏不贅述了。注意這個函數只有兩個參數。實際上output字段函數的實現最終還是調用linkoutput字段函數將數據包發送出去的。
state字段可以指向用戶關心的關於設備的一些信息,用戶可以自由發揮,也可以不用。hwaddr_len
和hwaddr[]
表示MAC地址長度和MAC地址,一般MAC地址
長度爲6
。
mtu字段表示該網絡一次可以傳送的最大字節數,對於以太網一般設爲1500
,不多說。
flags字段是網卡狀態信息標誌位,是很重要的控制字段,它包括網卡功能使能、廣播使能、ARP使能等等重要控制位。
name[]字段用於保存每一個網絡網絡接口的名字。用兩個字符的名字來標識網絡接口使用的設備驅動的種類,名字由設備驅動來設置並且應該反映通過網絡接口表示的硬件的種類。比如藍牙設備(bluetooth
)的網絡接口名字可以是 bt
,而 IEEE 802.11b WLAN
設備的名字就可以是 wl
,當然設置什麼名字用戶是可以自由發揮的,這並不影響用戶對網絡接口的使用。當然,如果兩個網絡接口具有相同的網絡名字,我們就用num
字段來區分相同類別的不同網絡接口。
到這裏,你可能一頭霧水,太抽象的東西太容易讓人糾結。我們舉個例子來看看一個以太網網卡接口結構是這樣被初始化,還有數據包是如何接收和發送的。先來看初始化過程,源碼:
static struct netif enc28j60; (1)
struct ip_addr ipaddr, netmask, gw; (2)
IP4_ADDR(&gw, 192,168,0,1); (3)
IP4_ADDR(&ipaddr, 192,168,0,60); (4)
IP4_ADDR(&netmask, 255,255,255,0); (5)
netif_init(); (6)
netif_add(&enc28j60, &ipaddr, &netmask, &gw, NULL, ethernetif_init, tcpip_input); (7)
netif_set_default(&enc28j60); (8)
netif_set_up(&enc28j60); (9)
上面的(1)聲明瞭一個netif
結構的變量enc28j60
,由於在我的板子上使用的是網卡芯片enc28j60
,所以我選擇使用了這個名字。(2)聲明瞭三個分別用於暫存IP地址、子網掩碼和網關地址的變量,它們是32位長度的。(3)~ (5)分別是對上述三個地址值的初始化,該過程簡單。
(6)很簡單,它只需初始化上面所述的全局變量netif_list
即可:netif_list = NULL
。
(7)調用netif_add
函數初始化變量enc28j60
,其中比較重要的兩個參數是ethernetif_init
和tcpip_input
,前者是用戶自己定義的底層接口初始化函數,tcpip_input
函數是向IP
層遞交數據包的函數,從前面的講述中可以很明顯的看出,該值會被傳遞給enc28j60
的input
字段。再來看看源碼:
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;
netif->ip_addr.addr = 0; //復位變量enc28j60中各字段的值
netif->netmask.addr = 0;
netif->gw.addr = 0;
netif->flags = 0; //該網卡不允許任何功能使能
netif->state = state; //指向用戶關心的信息,這裏爲NULL
netif->num = netifnum++; //設置num字段,
netif->input = input; //如前所訴,input函數被賦值
netif_set_addr(netif, ipaddr, netmask, gw); //設置變量enc28j60的三個地址
if (init(netif) != ERR_OK) {
//用戶自己的底層接口初始化函數
return NULL;
}
netif->next = netif_list; //將初始化後的節點插入鏈表netif_list
netif_list = netif; // netif_list指向鏈表頭
return netif;
}
上面的初始化函數調用了用戶自己定義的底層接口初始化函數,這裏爲ethernetif_init
,再來看看它的源代碼:
err_t ethernetif_init(struct netif *netif)
{
netif->name[0] = IFNAME0; //初始化變量enc28j60的name字段
netif->name[1] = IFNAME1; // IFNAME在文件外定義的,這裏不必關心它的具體值
netif->output = etharp_output; //IP層發送數據包函數
netif->linkoutput = low_level_output; // //ARP模塊發送數據包函數
low_level_init(netif); //底層硬件初始化函數
return ERR_OK;
}
還有函數調用!low_level_init
函數就是與我們使用的硬件密切相關的函數了。