1、LwIP結構體netif
網絡接口(如以太網接口)是硬件接口,網絡接口又可以稱之為網卡 , LwIP 是軟件,那么怎么讓硬件與軟件無縫連接起來呢?而且, 網卡又有多種多樣,怎么能讓 LwIP 使用同樣的軟件能兼容不同的硬件呢?LwIP 使用一個數據結構——netif 來描述一個網卡, 但是由於網卡是直接與硬件打交道的,硬件不同則處理基本是不同的, 所以必須由用戶提供最底層接口函數, LwIP 提供統一的接口,但是底層的實現需要用戶自己去完成,比如網卡的初始化, 網卡的收發數據,當 LwIP 底層得到了網絡的數據之后,才會傳入內核中去處理;同理, LwIP 內核需要發送一個數據包的時候,也需要調用網卡的發送函數,這樣子才能把數據從硬件接口到軟件內核無縫連接起來。
LwIP 中的 ethernetif.c 文件即為底層接口的驅動的模版,用戶為自己的網絡設備實現驅動時應參照此模塊做修改。 ethernetif.c 文件中的函數通常為與硬件打交道的底層函數,當有數據需要通過網卡接收或者發送數據的時候就會被調用,經過 LwIP 協議棧內部進行處理后,從應用層就能得到數據或者可以發送數據。
簡單來說, netif 是 LwIP 抽象出來的網卡, LwIP 協議棧可以使用多個不同的接口,而ethernetif.c 文件則提供了 netif 訪問各種不同的網卡,每個網卡有不同的實現方式, 用戶只需要修改 ethernetif.c 文件即可。
在單網卡中,這個 netif 結構體只有一個,可能還有人會問,那么一個設備中有多個網卡怎么辦,很簡單, LwIP 會將每個用 netif 描述的網卡連接成一個鏈表(單向鏈表),該鏈表就記錄每個網卡的 netif。 屏蔽硬件接口的差異,完成了對不同網卡的抽象,因此了解netif 結構體是移植 LwIP 的關鍵。
//netif 數據結構 struct netif { #if !LWIP_SINGLE_NETIF struct netif *next; /* 指向 netif 鏈表中的下一個 */ (1) #endif /* 網絡字節中的 IP 地址、子網掩碼、默認網關配置 */ #if LWIP_IPV4 ip_addr_t ip_addr; ip_addr_t netmask; ip_addr_t gw; (2) #endif /* LWIP_IPV4 */ netif_input_fn input; /* 此函數由網絡設備驅動程序調用,將數據包傳遞到 TCP/IP 協議棧。對於以太網物理層,這通常是 ethernet_input()*/ (3) #if LWIP_IPV4 netif_output_fn output; /* 此函數由 IP 層調用,在接口上發送數據包。通常這個功能,首先解析硬件地址,然后發送數據包。對於以太網物理層,這通常是 etharp_output() */ (4) #endif /* LWIP_IPV4 */ netif_linkoutput_fn linkoutput; /* 此函數由 ethernet_output()調用,當需要在網卡上發送一個數據包時。底層硬件輸出數據函數,一般是調用自定義函數 low_level_output*/ (5) #if LWIP_NETIF_STATUS_CALLBACK netif_status_callback_fn status_callback; /*當 netif 狀態設置為 up 或 down 時調用此函數*/ (6) #endif /* LWIP_NETIF_STATUS_CALLBACK */ #if LWIP_NETIF_LINK_CALLBACK netif_status_callback_fn link_callback; /* 當 netif 鏈接設置為 up 或 down 時,將調用此函數 */ (7) #endif /* LWIP_NETIF_LINK_CALLBACK */ #if LWIP_NETIF_REMOVE_CALLBACK netif_status_callback_fn remove_callback; /* 當 netif 被刪除時調用此函數 */ (8) #endif /* LWIP_NETIF_REMOVE_CALLBACK */ void *state; /* 此字段可由設備驅動程序設置並指向設備的狀態信息。主要是將網卡的某些私有數據傳遞給上層,用戶可以自由發揮,也可以不用。 */ (9) #ifdef netif_get_client_data void* client_data[LWIP_NETIF_CLIENT_DATA_INDEX_MAX + LWIP_NUM_NETIF_CLIENT_DATA]; #endif #if LWIP_NETIF_HOSTNAME const char* hostname; /* 這個 netif 的主機名, NULL 也是一個有效值 */ #endif /* LWIP_NETIF_HOSTNAME */ #if LWIP_CHECKSUM_CTRL_PER_NETIF u16_t chksum_flags; #endif /* LWIP_CHECKSUM_CTRL_PER_NETIF*/ u16_t mtu; /** 最大傳輸單位(以字節為單位),對於以太網一般設為 1500 */ (10) u8_t hwaddr[NETIF_MAX_HWADDR_LEN]; /** 此網卡的鏈路層硬件地址 */ (11) u8_t hwaddr_len; /** 硬件地址長度,對於以太網就是 MAC 地址長度,為 6 字節 */ (12) u8_t flags; /* 網卡狀態信息標志位,是很重要的控制字段,它包括網卡功能使能、廣播使能、 ARP 使能等等重要控制位。 */ (13) /* 字段用於保存每一個網卡的名字。用兩個字符的名字來標識網絡接口使用的設備驅動的種類,名字由設備驅動來設置並且應該反映通過網卡表示的硬件的種類。比如藍牙設備( bluetooth)的網卡名字可以是 bt, 而 IEEE 802.11b WLAN 設備的名字就可以是 wl,當然設置什么名字用戶是可以自由發揮的,這並不影響用戶對網卡的使用。當然,如果兩個網卡具有相同的網絡名字,我們就用 num 字段來區分相同類別的不同網卡*/ char name[2]; (14) u8_t num; /* 用來標示使用同種驅動類型的不同網卡 */ (15) #if MIB2_STATS u8_t link_type; /* 連接類型 */ u32_t link_speed; /* 連接速度 */ u32_t ts; /* 最后一次更改的時間戳 */ struct stats_mib2_netif_ctrs mib2_counters; #endif /* MIB2_STATS */ #if LWIP_IPV4 && LWIP_IGMP netif_igmp_mac_filter_fn igmp_mac_filter; /** 可以調用此函數來添加或刪除多播中的條目以太網 MAC 的過濾表。 */ #endif /* LWIP_IPV4 && LWIP_IGMP */ #if LWIP_NETIF_USE_HINTS struct netif_hint *hints; #endif /* LWIP_NETIF_USE_HINTS */ #if ENABLE_LOOPBACK /* List of packets to be queued for ourselves. */ struct pbuf *loop_first; struct pbuf *loop_last; #if LWIP_LOOPBACK_MAX_PBUFS u16_t loop_cnt_current; #endif /* LWIP_LOOPBACK_MAX_PBUFS */ #endif /* ENABLE_LOOPBACK */ };
(1):LwIP 使用鏈表來管理同一設備的多個網卡。在 netif.c 文件中定義兩個全局指針: struct netif *netif_list 和 struct netif *netif_default,其中 netif_list 就是網卡鏈表指針,指向網卡鏈表的首節點(第一個網卡) ,后者表示默認情況下(有多網口時)使用哪個網卡。 next 字段指向下一個 netif 結構體指針,在一個設備中有多個網卡時, 才使用該字段。 (2):ip_addr 字段記錄的是網絡中的 IP 地址, netmask 字段記錄的是子網掩碼, gw 記錄的是網關地址,這些字段是用於描述網卡的網絡地址屬性。IP 地址必須與網卡對應,即設備擁有多少個網卡那就必須有多少個 IP 地址;子網掩碼可以用來判斷某個 IP 地址與當前網卡是否處於同一個子網中, IP 在發送數據包的時候會選擇與目標 IP 地址處於同一子網的網卡來發送;網關地址在數據包的發送、轉發過程非常重要,如果要向不屬於同一子網的主機(主機目標 IP 地址與網卡不屬於同一子網)發送一個數據包,那么 LwIP 就會將數據包發送到網關中,網關設備會對該數據包進行正確的轉發,除此之外,網關還提供很多高級功能,如 DNS, DHCP 等。 (3):input 是一個函數指針, 指向一個函數,該函數由網絡設備驅動程序調用,將數據包傳遞到 TCP/IP 協議棧(IP 層) 。 對於以太網物理層,這通常是ethernet_input(), 參數為 pbuf 和 netif 類型,其中 pbuf 為接收到的數據包。 (4):output 也是一個函數指針, 指向一個函數, 此函數由 IP 層調用,在接口上發送數據包。 用戶需要編寫該函數並使 output 指向它, 通這個函數的處理步驟是首先解析硬件地址,然后發送數據包。對於以太網物理層, 該函數通常是 etharp_output(),參數為 pbuf、 netif 和 ip_addr 類型,其中, ipaddr 代表要將該數據包發送到的地址,但不一定是數據包最終到到達的 IP 地址,比如,要發送 IP 數據報到一個並不在本網絡的主機上,該數據包要被發送到一個路由器上,這里的 ipaddr 就是路由器 IP 地址。 (5):linkoutput 字段和 output 類似,也需要用戶自己實現一個函數, 但只有兩個參數,它是由 ARP 模塊調用的, 一般是自定義函數 low_level_output()。 當需要在網卡上發送一個數據包時,該函數會被 ethernet_output()函數調用。 (6):當 netif 狀態設置為 up 或 down 時, 將調用此函數。 (7):當 netif 連接設置為 up 或 down 時,將調用此函數。 (8):當 netif 被刪除時調用此函數。 (9):此字段可由設備驅動程序設置並指向設備的狀態信息。主要是將網卡的某些私有數據傳遞給上層,用戶可以自由發揮,也可以不用。 (10):最大傳輸單位(以字節為單位),對於以太網一般為 1500,在 IP層發送數據的時候, LwIP 會使用該字段決定是否需要對數據包進行分片處理,為什么是在IP 層進行分片處理?因為鏈路層不提供任何的差錯處理機制,如果在網卡中接收的數據包不滿足網卡自身的屬性,那么網卡可能就會直接丟棄該數據包,也可能在底層進行分包發送,但是這種分包在 IP 層看來是不可接受的,因為它打亂了數據的結構,所以只能由 IP層進行分片處理。 (11):此網卡的鏈路層硬件地址。 (12):硬件地址長度,對於以太網就是 MAC 地址長度,為 6 字節 (13):網卡狀態信息標志位,是很重要的控制字段,它包括網卡功能使能、廣播使能、 ARP 使能等等重要控制位。 (14):name 字段用於保存每一個網卡的名字。用兩個字符的名字來標識網卡使用的設備驅動的種類,名字由設備驅動來設置並且應該反映通過網卡表示的硬件的種類。比如藍牙設備(bluetooth)的網卡名字可以是 bt,而 IEEE 802.11b WLAN 設備的名字就可以是 wl,當然設置什么名字用戶是可以自由發揮的,這並不影響用戶對網卡的使用。當然,如果兩個網卡具有相同的網絡名字,我們就用 num 字段來區分相同類別的不同網卡。 (15):用來標識使用同種驅動類型的不同網卡。
2、 netif結構體的使用
首先我們需要根據我們的網卡定義一個 netif 結構體變量 struct netif gnetif, 我們首先要把網卡掛載到 netif_list 鏈表上才能使用,因為 LwIP 是通過鏈表來管理所有的網卡,所有第一步是通過 netif_add()函數將我們的網卡掛載到 netif_list 鏈表上, netif_add()函數具體見代碼清單 4-2。
struct netif *netif_add(struct netif *netif,const ip4_addr_t *ipaddr,const ip4_addr_t *netmask, const ip4_addr_t *gw,void *state, netif_init_fn init, netif_input_fn input) { LWIP_ASSERT_CORE_LOCKED(); if (ipaddr == NULL) { ipaddr = ip_2_ip4(IP4_ADDR_ANY); } if (netmask == NULL) { netmask = ip_2_ip4(IP4_ADDR_ANY); } if (gw == NULL) { gw = ip_2_ip4(IP4_ADDR_ANY); } /*清空主機 IP 地址、子網掩碼、網關等字段信息*/ /* reset new interface configuration state */ ip_addr_set_zero_ip4(&netif->ip_addr); ip_addr_set_zero_ip4(&netif->netmask); ip_addr_set_zero_ip4(&netif->gw); netif->output = netif_null_output_ip4; NETIF_SET_CHECKSUM_CTRL(netif, NETIF_CHECKSUM_ENABLE_ALL); netif->mtu = 0; netif->flags = 0; memset(netif->client_data, 0, sizeof(netif->client_data)); /*根據傳遞進來的參數填寫網卡 state、 input 等字段的相關信息*/ /* remember netif specific state information data */ netif->state = state; netif->num = netif_num; netif->input = input; NETIF_RESET_HINTS(netif); /*調用網卡設置函數 netif_set_addr()設置網卡 IP 地址、子網掩碼、網關等信息*/ netif_set_addr(netif, ipaddr, netmask, gw); (3) /*通過傳遞進來的回調函數 init()進行網卡真正的初始化操作, 所以該函數是由用戶實現的, 對於不同網卡就使用不一樣的初始化, 而此處是以太網, 則該回調函數一般為 ethernetif_init()*/ /* call user specified initialization function for netif */ if (init(netif) != ERR_OK) { return NULL; } { struct netif *netif2; int num_netifs; do { if (netif->num == 255) { netif->num = 0; } num_netifs = 0; for(netif2 = netif_list; netif2 != NULL; netif2 = netif2->next) { num_netifs++; if (netif2->num == netif->num) { netif->num++; break; } } } while (netif2 != NULL); } if (netif->num == 254) { netif_num = 0; } else { netif_num = (u8_t)(netif->num + 1); /8*初始化網卡成功,則遍歷當前設備擁有多少個網卡,並為當前網卡分配唯一標識 num*/ } /將當前網卡插入 netif_list 鏈表中*/ /* add this netif to the list */ netif->next = netif_list; netif_list = netif; mib2_netif_added(netif); ip4_addr_debug_print(NETIF_DEBUG, ipaddr); ip4_addr_debug_print(NETIF_DEBUG, netmask); ip4_addr_debug_print(NETIF_DEBUG, gw); netif_invoke_ext_callback(netif, LWIP_NSC_NETIF_ADDED, NULL); return netif; }
在使用之前需要進行初始化主機 IP 地址、子網掩碼、網關等,並且在調用 netif_add()函數之后會觸發 netif_init_fn 的回調函數。
總之一句話,在開始使用 LwIP 協議棧的時候,我們就需要將網卡底層移植完成,才能開始使用,而移植的第一步,就是將網絡進行初始化,並且設置該網卡為默認網卡,讓LwIP 能通過網卡進行收發數據。
3、與 netif 相關的底層函數
每個 netif 接口都需要一個底層接口文件提供訪問硬件的支持, 而 LwIP 作者將這種支持做成一個框架供我們參考,如 ethernetif.c 文件就是實現為一個框架的形式,我們在移植的時候只需要根據實際的網卡特性完善這里面的函數即可。框架中的函數名、參數等都已經實現,我們只需往里面填充完善即可,當然,網卡的驅動與這些函數名字我們也可以進行修改,只要 LwIP 內核能正確識別網卡中的功能即可,為了方便,我們還是使用 LwIP 作者提供的框架進行移植操作,當一個設備使用了多個網卡的時候,那就需要編寫多個不同的網卡驅動。
與網卡驅動密切相關的函數有三個,分別是:
static void low_level_init(struct netif *netif); static err_t low_level_output(struct netif *netif, struct pbuf *p); static struct pbuf * low_level_input(struct netif *netif);
low_level_init()為網卡初始化函數,它主要完成網卡的復位及參數初始化,根據實際的網卡屬性進行配置 netif 中與網卡相關的字段,例如網卡的 MAC 地址、長度,最大發送單元等。
low_level_output()函數為網卡的發送函數, 它主要將內核的數據包發送出去,數據包采用 pbuf 數據結構進行描述,該數據結構是一個比較復雜的數據結構。
low_level_input()函數為網卡的數據接收函數,該函數會接收一個數據包,為了內核易於對數據包的管理,該函數必須將接收的數據封裝成 pbuf 的形式。
除此之外,還有兩個函數也與網卡與關系, 分別是:
err_t ethernetif_init(struct netif *netif); void ethernetif_input(void *pParams);
ethernetif_init()函數是在上層管理網卡 netif 的到時候會被調用的函數,如使用netif_add()添加網卡的時候,就會調用 ethernetif_init()函數對網卡進行初始化,其實該函數的最終調用的初始化函數就是 low_level_init()函數,我們目前只有一個網卡,就暫時不用對該函數進行改寫,直接使用即可,它內部會將網卡的 name、 output、 linkoutput 等字段進行初始化, 這樣子就能將內核與網卡無縫連接起來。
ethernetif_input()函數的主要作用就是調用 low_level_input()函數從網卡中讀取一個數據包,然后解析該數據包的類型是屬於 ARP 數據包還是 IP 數據包,再將包遞交給上層,在無操作系統的時候 ethernetif_input()就是一個可以直接使用的函數,已經無需我們自己去修改,內核會周期性處理該接收函數。而在多線程操作系統的時候,我們一般會將其改寫成一個線程的形式,可以周期性去調用 low_level_input()網卡接收函數;也可以使用中斷的形式去處理,當這個線程將在尚未接收到數據包的時候,處於阻塞狀態,當收到數據包的時候,中斷利用操作系統的 IPC 通信機制來喚醒線程去處理接收到的數據包,並將數據包遞交上層,這樣子的效率會更加高效,事實上我們也是這樣子處理的。