(一)ARP之 數據包接收過程 先看一下整個數據流的傳輸過程。 首先etherneti_input()函數 從底層網卡驅動接收到原始數據,若是ip包或者ARP包則調用ethernet_input()。 s32_t ethernetif_input(struct netif *netif) { ...
(一)ARP之 數據包接收過程
先看一下整個數據流的傳輸過程。
-
首先etherneti_input()函數 從底層網卡驅動接收到原始數據,若是ip包或者ARP包則調用ethernet_input()。
s32_t ethernetif_input(struct netif *netif) { struct ethernetif *ethernetif; //網路介面信息結構體,此處無用。 struct eth_hdr *ethhdr; //乙太網幀 頭部結構體指針。 struct pbuf *p; ethernetif = netif->state; p = low_level_input(netif); //!!調用底層函數讀取一個數據包。 if (p == NULL) { return 0; } ethhdr = p->payload; //將ethhdr指針指向數據包中乙太網頭部 switch (htons(ethhdr->type)) { //判斷數據包中的幀類型 ,要大小端轉換。 case ETHTYPE_IP: case ETHTYPE_ARP: /* full packet send to tcpip_thread to process */ if (netif->input(p, netif)!=ERR_OK) //調用netif->input 進行處理。 { pbuf_free(p); //釋放Pbuf p = NULL; } break; default: pbuf_free(p); p = NULL; break; } return 1; }
可見,此函數未作實質性的處理,只是判斷乙太網中幀類型,並調用中netif->input函數指針處理,此處指向的就是 ethernet_input函數。我們來看看ethernet_input函數做了哪些事情。(不停地套娃~)
err_t ethernet_input(struct pbuf *p, struct netif *netif)
{
struct eth_hdr* ethhdr; //乙太網幀頭部結構體指針
u16_t type;
if (p->len <= SIZEOF_ETH_HDR) { //長度校驗,ARP包必須包含在第一個PBUF的數據區。
goto free_and_return;
}
ethhdr = (struct eth_hdr *)p->payload; //乙太網幀指針 指向乙太網幀頭部
type = ethhdr->type; //獲取幀類型
switch (type) {
case PP_HTONS(ETHTYPE_IP):
#if ETHARP_TRUST_IP_MAC
/* update ARP table */
etharp_ip_input(netif, p); //使用IP頭部以及 乙太網頭部MAC 更新 ARP表。
#endif /* ETHARP_TRUST_IP_MAC */
/* skip Ethernet header */
if(pbuf_header(p, -ip_hdr_offset)) { //去掉乙太網頭部
goto free_and_return; //若操作失敗,則釋放pbuf
} else {
/* pass to IP layer */
ip_input(p, netif); //若頭部去除成功,則調用IP輸入處理函數處理數據。
}
break;
case PP_HTONS(ETHTYPE_ARP): //若是ARP數據包
/* pass p to ARP module */
etharp_arp_input(netif, (struct eth_addr*)(netif->hwaddr), p); //調用ARP數據包處理函數。
break;
default:
goto free_and_return;
}
return ERR_OK;
free_and_return:
pbuf_free(p);
return ERR_OK;
}
這個函數也很簡單,就是根據數據包的type 類型進行判斷 是IP包還是ARP包?是IP包則去掉 **乙太網幀頭部**,調用**ip_input** 函數處理,若是ARP包,則調用**etharp_arp_input() **處理數據包。
那我們接下來看看數據包是怎麼處理的呢?
(二)ARP之 數據包處理過程
從之前講述的知識可以瞭解到 etharp_arp_input()函數有兩個功能:
-
若接收到是ARP應答包,則需要根據應答信息更新ARP緩存表。
-
若接收到ARP請求包,則需要:
-
若這個請求包,與本機IP地址不符合,則不需要應答,但是要將請求包中的 IP和MAC加入到自己的緩存表中,以備到時候需要使用。
-
若這個請求包,與本機IP地址符號,除了要將源主機的IP與MAC加入緩存表之外,還要回覆一個應答,告訴本機的MAC地址是多少。
-
好,有了上面的思路,我們具體來看一看代碼是怎麼實現的。
(1)etharp_arp_input()函數分析
細細品味~~
static void etharp_arp_input(struct netif *netif, struct eth_addr *ethaddr, struct pbuf *p)
{
struct etharp_hdr *hdr; //ARP數據包包頭部結構指針。
struct eth_hdr *ethhdr; //乙太網頭部結構體指針
ip_addr_t sipaddr, dipaddr;
u8_t for_us; //指示ARP包是否發送給本機的。
/* drop short ARP packets: we have to check for p->len instead of p->tot_len here
since a struct etharp_hdr is pointed to p->payload, so it musn't be chained! */
if (p->len < SIZEOF_ETHARP_PACKET) { //arp 數據包不能分裝在兩個PBUF中,不然不能用指針來操作內部了。
pbuf_free(p);
return;
}
ethhdr = (struct eth_hdr *)p->payload;
hdr = (struct etharp_hdr *)((u8_t*)ethhdr + SIZEOF_ETH_HDR); //hdr 指向ARP數據包首部
/* RFC 826 "Packet Reception": */
if ((hdr->hwtype != PP_HTONS(HWTYPE_ETHERNET)) || //判斷ARP數據包的合法型,要是不合法,則直接刪除該數據包。
(hdr->hwlen != ETHARP_HWADDR_LEN) ||
(hdr->protolen != sizeof(ip_addr_t)) ||
(hdr->proto != PP_HTONS(ETHTYPE_IP))) {
pbuf_free(p);
return;
}
/* Copy struct ip_addr2 to aligned ip_addr, to support compilers without
* structure packing (not using structure copy which breaks strict-aliasing rules). */
//由於arp 數據包中的IP地址地段並不是位元組對齊的,不能直接使用,需要將IP 拷貝到sipaddr,dipaddr中使用。
IPADDR2_COPY(&sipaddr, &hdr->sipaddr);
IPADDR2_COPY(&dipaddr, &hdr->dipaddr);
//1.判斷請求包是否給我
if (ip_addr_isany(&netif->ip_addr)) { //若本機的網卡IP未配置,那麼肯定不是發給我們的
for_us = 0;
} else {
/* ARP packet directed to us? */
for_us = (u8_t)ip_addr_cmp(&dipaddr, &(netif->ip_addr)); //若配置了本機IP,那比較一下IP是否相同。
}
//2.那麼接下來添加一下ARP緩存表
//若這個ARP包(不論請求還是相應包)是給我們的,那麼更新ARP表。
如果不是給我們的,也要更新ARP表,然後不設置ETHARP_TRY_HARD標誌
etharp_update_arp_entry(netif, &sipaddr, &(hdr->shwaddr),
for_us ? ETHARP_FLAG_TRY_HARD : ETHARP_FLAG_FIND_ONLY);
/* 3.now 處理 ARP數據包 */
switch (hdr->opcode) {
/* 是ARP請求包嗎? */
case PP_HTONS(ARP_REQUEST):
if (for_us) { //是ARP請求包,且是發給我的
hdr->opcode = htons(ARP_REPLY); //改變標誌為為 應答包
IPADDR2_COPY(&hdr->dipaddr, &hdr->sipaddr); //ip地址交換
IPADDR2_COPY(&hdr->sipaddr, &netif->ip_addr);
//設置4個MAC地址欄位
ETHADDR16_COPY(&hdr->dhwaddr, &hdr->shwaddr); //ARP首部中接收端MAC
ETHADDR16_COPY(ðhdr->dest, &hdr->shwaddr);
ETHADDR16_COPY(&hdr->shwaddr, ethaddr);
ETHADDR16_COPY(ðhdr->src, ethaddr);
/* hwtype, hwaddr_len, proto, protolen and the type 這些欄位保持不變 */
/*發送ARP應答包*/
netif->linkoutput(netif, p);
} else if (ip_addr_isany(&netif->ip_addr)) { //如果這個ARP請求包不是給我的,不做任何處理。
} else { //如果這個ARP請求包不是給我的,不做任何處理。
}
break;
case PP_HTONS(ARP_REPLY): //如果是ARP應答包,前面已經處理過了,現在什麼也不用做。
break;
default:
break;
}
/* free ARP packet */
pbuf_free(p);
}
總結一下,這個函數就是實現了上面上述的思路。更新ARP表項的這個動作是放在函數**etharp_update_arp_entry()**中完成的關於這個函數,後續會詳細講解。此處說明一些,標誌位**ETHARP_FLAG_TRY_HARD ** 的含義:
-
ETHARP_FLAG_TRY_HARD :告訴函數無論如何要創建這個地址對的表項,如果所有的ARP表項都用完了,就刪除最老的表項。
-
ETHARP_FLAG_FIND_ONLY:遍歷整個緩存表尋找空閑表項,如果找不到,就不添加了。
(2)ARP緩存表的更新
ARP緩存表的更新是調用etharp_update_arp_entry()來實現的。在介紹這個函數之前要先介紹etharp_find_entry()這個函數。
1. etharp_find_entry()
該函數功能是 尋找 一個與IP地址符合的ARP表項 或者 創建一個新的ARP表項,並返回該表項的索引號
如果 ipaddr 非零,則返回一個處於pending或stable 狀態的表項。
* 若沒有,則返回一個empty表項,且該表項ip地段要設置為ipaddr。這樣etharp_find_entry()函數返回後,調用者需要將empty 狀態,改變為pending狀態。再如果,沒有empty表項呢?那麼根據傳入的關鍵字來做處理,若是ETHART_TRY_HARD,則**刪除**所有表項中年齡最老的那個,建立新表項。
若ipaddr為0,則要返回一個empty狀態的表項。
所有這個過程要怎麼實現呢?要迴圈遍歷3 遍嗎,並不需要。Lwip 一邊判斷表項是否匹配,一邊記錄索引編號最低的empty表項,一邊記錄年齡最大的stable表項,一邊記錄年齡最大且緩存隊列為空的Pending表項,一邊記錄年齡最大且緩存隊列不為空的Pending表項。
static s8_t
etharp_find_entry(ip_addr_t *ipaddr, u8_t flags)
{
s8_t old_pending = ARP_TABLE_SIZE, old_stable = ARP_TABLE_SIZE;
s8_t empty = ARP_TABLE_SIZE;
u8_t i = 0, age_pending = 0, age_stable = 0;
/* oldest entry with packets on queue */
s8_t old_queue = ARP_TABLE_SIZE;
/* its age */
u8_t age_queue = 0;
/**
* a) do a search through the cache, remember candidates
* b) select candidate entry
* c) create new entry
*/
/* a) in a single search sweep, do all of this
* 1) remember the first empty entry (if any)
* 2) remember the oldest stable entry (if any)
* 3) remember the oldest pending entry without queued packets (if any)
* 4) remember the oldest pending entry with queued packets (if any)
* 5) search for a matching IP entry, either pending or stable
* until 5 matches, or all entries are searched for.
*/
for (i = 0; i < ARP_TABLE_SIZE; ++i) {
u8_t state = arp_table[i].state;
/* no empty entry found yet and now we do find one? */
if ((empty == ARP_TABLE_SIZE) && (state == ETHARP_STATE_EMPTY)) {
empty = i;
} else if (state != ETHARP_STATE_EMPTY)
state == ETHARP_STATE_PENDING || state >= ETHARP_STATE_STABLE);
/* if given, does IP address match IP address in ARP entry? */
if (ipaddr && ip_addr_cmp(ipaddr, &arp_table[i].ipaddr)) {
/* found exact IP address match, simply bail out */
return i;
}
/* pending entry? */
if (state == ETHARP_STATE_PENDING) {
/* pending with queued packets? */
if (arp_table[i].q != NULL) {
if (arp_table[i].ctime >= age_queue) {
old_queue = i;
age_queue = arp_table[i].ctime;
}
} else
/* pending without queued packets? */
{
if (arp_table[i].ctime >= age_pending) {
old_pending = i;
age_pending = arp_table[i].ctime;
}
}
/* stable entry? */
} else if (state >= ETHARP_STATE_STABLE) {
{
/* remember entry with oldest stable entry in oldest, its age in maxtime */
if (arp_table[i].ctime >= age_stable) {
old_stable = i;
age_stable = arp_table[i].ctime;
}
}
}
}
}
/* { we have no match } => try to create a new entry */
/* don't create new entry, only search? */
if (((flags & ETHARP_FLAG_FIND_ONLY) != 0) ||
/* or no empty entry found and not allowed to recycle? */
((empty == ARP_TABLE_SIZE) && ((flags & ETHARP_FLAG_TRY_HARD) == 0))) {
return (s8_t)ERR_MEM;
}
/* b) choose the least destructive entry to recycle:
* 1) empty entry
* 2) oldest stable entry
* 3) oldest pending entry without queued packets
* 4) oldest pending entry with queued packets
*
* { ETHARP_FLAG_TRY_HARD is set at this point }
*/
/* 1) empty entry available? */
if (empty < ARP_TABLE_SIZE) {
i = empty;
} else {
/* 2) found recyclable stable entry? */
if (old_stable < ARP_TABLE_SIZE) {
/* recycle oldest stable*/
i = old_stable;
} else if (old_pending < ARP_TABLE_SIZE) {
/* recycle oldest pending */
i = old_pending;
/* 4) found recyclable pending entry with queued packets? */
} else if (old_queue < ARP_TABLE_SIZE) {
/* recycle oldest pending (queued packets are free in etharp_free_entry) */
i = old_queue;
/* no empty or recyclable entries found */
} else {
return (s8_t)ERR_MEM;
}
etharp_free_entry(i);
}
arp_table[i].state == ETHARP_STATE_EMPTY);
/* IP address given? */
if (ipaddr != NULL) {
/* set IP address */
ip_addr_copy(arp_table[i].ipaddr, *ipaddr);
}
arp_table[i].ctime = 0;
return (err_t)i;
}
此函數較為複雜,稍微整理一下流程:
- 對於每個表項,判斷是否是empty,
- 若不是,則表明已經被占用,立即檢查IP是否匹配?
- 若匹配,則返回該索引值
- 若不匹配,判斷是否是pending狀態,且該索引的數據包指針是否為空?該函數試圖分別記錄生存時間最長的有數據緩存或無數據緩存的表項索引。如果一個表項也不是Pending狀態,則判斷是不是stable狀態?
- 對於stable狀態,與pending狀態處理過程類似,該函數試圖記錄生產時間最長的stable的表項索引。
- 對於stable狀態,與pending狀態處理過程類似,該函數試圖記錄生產時間最長的stable的表項索引。
- 若匹配,則返回該索引值
- 若不是,則表明已經被占用,立即檢查IP是否匹配?
2.etharp_updata_arp_entry()
接下來看看這個函數就比較輕鬆了。先看代碼:
tatic err_t
etharp_update_arp_entry(struct netif *netif, ip_addr_t *ipaddr, struct eth_addr *ethaddr, u8_t flags)
{
s8_t i;
if (ip_addr_isany(ipaddr) || //地址有效性驗證,不能為廣播,多播地址建立表項
ip_addr_isbroadcast(ipaddr, netif) ||
ip_addr_ismulticast(ipaddr)) {
return ERR_ARG;
}
/* 查找或新建一個表項 */
i = etharp_find_entry(ipaddr, flags);
/*若不合法,返回 */
if (i < 0) {
return (err_t)i;
}
/* mark it stable */
arp_table[i].state = ETHARP_STATE_STABLE; //將對應的表項狀態改為stable
/* 更新這個表項的網路介面 */
arp_table[i].netif = netif;
/* 更新緩存標誌的MAC地址 */
ETHADDR32_COPY(&arp_table[i].ethaddr, ethaddr);
/* reset time stamp */
arp_table[i].ctime = 0;
/* 該表項中有未發生出去的數據包,則將這些數據包發送出去! */
while (arp_table[i].q != NULL) {
struct pbuf *p;
struct etharp_q_entry *q = arp_table[i].q; //指向緩衝隊列頭部。此結構前文有過介紹。
arp_table[i].q = q->next; //緩衝隊列指針指向下一個結點
p = q->p; //取得緩衝隊列第一個數據包pbuf
memp_free(MEMP_ARP_QUEUE, q); //釋放etharp_q_entry記憶體池空間
etharp_send_ip(netif, p, (struct eth_addr*)(netif->hwaddr), ethaddr); //發送數據包
pbuf_free(p);
}
return ERR_OK;
}
函數的執行流程如下:
首先調用entry_find_entry()找到一個緩衝表項,然後設置表項的相應成員(state,netif,ethaddr,cttime),若這個表項中有未發送的數據,那麼就發送這個數據包。根據返回的arp表項的不同狀態會有以下三種情況:
-
如果是empty狀態的表項,那麼就填充表項的欄位,並設置未stable狀態。
-
如果是pending 狀態的表項,那麼就說明已經有與相同IP地址的MAC地址返回,則改變未Stable狀態。
-
如果是stable 狀態的表項,則只需要將ctime複位即可。