lwip---(四)網路介面結構

語言: CN / TW / HK

  今天我們來討論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_lenhwaddr[]表示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_inittcpip_input,前者是使用者自己定義的底層介面初始化函式,tcpip_input函式是向IP層遞交資料包的函式,從前面的講述中可以很明顯的看出,該值會被傳遞給enc28j60input欄位。再來看看原始碼:

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函式就是與我們使用的硬體密切相關的函數了。