內核版本:2.6.34
這部分的重點是三個核心的數據結構-鄰居表、鄰居緩存、代理鄰居表,以及NUD狀態轉移圖。
總的來說,要成功添加一條鄰居表項,需要滿足兩個條件:1. 本機使用該表項;2. 對方主機進行了確認。同時,表項的添加 引入了NUD(Neighbour Unreachability Detection)機制,從創建NUD_NONE到可用NUD_REACHABLE需要經歷一系列狀態轉移,而根 據達到兩個條件順序的不同,可以分為兩條路線:
先引用再確認- NUD_NONE -> NUD_INCOMPLETE -> NUD_REACHABLE
先確認再引用- NUD_NONE -> NUD_STALE -> NUD_DELAY -> NUD_PROBE -> NUD_REACHABLE
下面還是從接收函數入手,當匹配號協議號是0x0806,會調用ARP模塊的接收函數arp_rcv()。
arp_rcv() ARP接收函數
首先是對arp協議頭進行檢查,比如大小是否足夠,頭部各數值是否正確等,這裡略過代碼,直 接向下看。每個協議處理都一樣,如果被多個協議占有,則拷貝一份。
if ((skb = skb_share_check(skb, GFP_ATOMIC)) == NULL) goto out_of_mem;
NEIGH_CB(skb)實際就是skb->cb,在skb聲明為u8 char[48],它用作每個協議模塊的私有數據 區(control buffer),每個協議模塊可以根據自身需求在其中存儲私有數據。而arp模塊就利用了它存儲控制結構neighbour_cb ,它聲明如下,占8字節。這個控制結構在代理ARP中使用工作隊列時會發揮作用,sched_next代表下次被調度的時間,flags是 標志。
memset(NEIGH_CB(skb), 0, sizeof(struct neighbour_cb)); struct neighbour_cb { unsigned long sched_next; unsigned int flags; };
函數最後調用arp_process,其間插入netfilter(關於netfilter,參見前篇:http://hi.csdn.net/link.php? url=http://blog.csdn.net%2Fqy532846454),作為開始處理ARP報文的起點。
return NF_HOOK(NFPROTO_ARP, NF_ARP_IN, skb, dev, NULL, arp_process);
arp_process()
這個函數開始對報 文進行處理,首先會從skb中取出arp報頭部分的信息,如sha, sip, tha, tip等,這部分可查閱代碼,這裡略過。ARP不會查詢 環路地址和組播地址,因為它們沒有對應的mac地址,因此遇到這兩類地址,直接退出。
if (ipv4_is_loopback(tip) || ipv4_is_multicast(tip)) goto out;
如果收到的是重復地址檢測報文,並且本機占用了檢測了地址,則調用arp_send發送響應。對於重復地址 檢測報文(ARP報文中源IP為全0),它所帶有的鄰居表項信息還沒通過檢測,此時緩存它顯然沒有意義,也許下一刻就有其它主機 聲明它非法,因此,重復地址檢測報文中的信息不會加入鄰居表中。
if (sip == 0) { if (arp->ar_op == htons(ARPOP_REQUEST) && inet_addr_type(net, tip) == RTN_LOCAL && !arp_ignore(in_dev, sip, tip)) arp_send(ARPOP_REPLY, ETH_P_ARP, sip, dev, tip, sha, dev->dev_addr, sha); goto out; }
下面要處理的地址解析報文,並且要解析的地址在路由表中存在
if (arp->ar_op == htons (ARPOP_REQUEST) && ip_route_input(skb, tip, sip, 0, dev) == 0)
第一種情況,如果要解析的是本機地址,則調用neigh_event_ns() ,並根據查到的鄰居表項n發送ARP響應報文。這裡neigh_event_ns的功能是在arp_tbl中查找是否已含有對方主機的地址信息, 如果沒有,則進行創建,然後會調用neigh_update來更新狀態。收到對方主機的請求報文,會導致狀態遷移到NUD_STALE。
if (addr_type == RTN_LOCAL) { …… if (!dont_send) { n = neigh_event_ns(&arp_tbl, sha, &sip, dev); if (n) { arp_send(ARPOP_REPLY,ETH_P_ARP,sip,dev,tip,sha,dev->dev_addr,sha); neigh_release(n); } } goto out; }
#NUD_INCOMPLETE也遷移到NUD_STALE,作何解釋?
第二種情況,如果要解析的不是本機地址,則要判斷是否支持 轉發,是否支持代理ARP(代理ARP是陸由器的功能,因此能轉發是先決條件),如果滿足條件,那麼按照代理ARP流程處理。首先 無論如何,主機得通了存在這樣一個鄰居,因此要在在arp_tbl中查找並(如果不存在)創建相應鄰居表項;然後,對於代理ARP, 這個流程實際上會執行兩遍,第一遍走else部分,第二遍走if部分。第一次的else代碼段會觸發定時器,通過定時器引發報文重 新執行arp_process函數,並走if部分。
-第一遍的else部分:調用pneigh_enqueue()將報文skb加入tbl->proxy_queue隊 列,同時設置NEIGH_CB(skb)的值,具體可看後見的代理表項處理。
-第二遍的if部分,發送ARP響應報文,行使代理ARP的功 能。
else if (IN_DEV_FORWARD(in_dev)) { if (addr_type == RTN_UNICAST && (arp_fwd_proxy(in_dev, dev, rt) || arp_fwd_pvlan(in_dev, dev, rt, sip, tip) || pneigh_lookup(&arp_tbl, net, &tip, dev, 0))) { n = neigh_event_ns(&arp_tbl, sha, &sip, dev); if (n) neigh_release(n); if (NEIGH_CB(skb)->flags & LOCALLY_ENQUEUED || skb->pkt_type == PACKET_HOST || in_dev->arp_parms->proxy_delay == 0) { arp_send(ARPOP_REPLY,ETH_P_ARP,sip,dev,tip,sha,dev->dev_addr,sha); } else { pneigh_enqueue(&arp_tbl, in_dev->arp_parms, skb); in_dev_put(in_dev); return 0; } goto out; } }
補充:neigh_event_ns()與neigh_release()配套使用並不代表創建後又被釋放,neigh被釋放的條件是neigh- >refcnt==0,但neigh創建時的refcnt=1,而neigh_event_ns會使refcnt+1,neigh_release會使-1,此時refcnt的值還是1, 只有當下次單獨調用neigh_release時才會被釋放。
查找是否已存在這樣一個鄰居表項。如果ARP報文是發往本機的響應報文 ,那麼neigh會更新為NUD_REACHABLE狀態;否則,維持原狀態不變。#個人認為,這段代碼是處理 NUD_INCOMPLETE/NUD_PROBE/NUD_DELAY向NUD_REACHABLE遷移的,但如果一台主機A發送一個對本機的ARP響應報文,那麼會導致 neigh從NUD_NONE直接遷移到NUD_REACHABLE,當然,按照正常流程,一個ARP響應報文肯定是由於本機發送了ARP請求報文,那樣 neigh已經處於NUD_INCOMPLETE狀態了。
n = __neigh_lookup(&arp_tbl, &sip, dev, 0); if (n) { int state = NUD_REACHABLE; int override; override = time_after(jiffies, n->updated + n->parms->locktime); if (arp->ar_op != htons(ARPOP_REPLY) || skb->pkt_type != PACKET_HOST) state = NUD_STALE; neigh_update(n, sha, state, override ? NEIGH_UPDATE_F_OVERRIDE : 0); neigh_release(n); }
實際上,arp_process是接收到ARP報文的處理函數,它涉及到的是鄰居表項在收到arp請求和響應的情況,下圖反映 了arp_process中所涉及的狀態轉移:收到arp請求,NUD_NONE -> NUD_STALE;收到arp響應, NUD_INCOMPLETE/NUD_DELAY/NUD_PROBE -> NUD_REACHABLE。根據之前分析,我認為還存在NUD_NONE -> NUD_REACHABLE和 NUD_INCOMPLETE -> NUD_STALE的轉移,作何解釋?
NUD狀態
每個鄰居表項在生效前都要經歷一系列的狀態遷移,每個狀態都有不同的含義,在前面已經多次提到了NUD狀態。要添加一條有 效的鄰居表項,有效途徑有兩條:
先引用再確認- NUD_NONE -> NUD_INCOMPLETE -> NUD_REACHABLE
先確認再引用 - NUD_NONE -> NUD_STALE -> NUD_DELAY -> NUD_PROBE -> NUD_REACHABLE
其中neigh_timer_handler定時器 、neigh_periodic_work工作隊列會異步的更改NUD狀態,neigh_timer_handler用於NUD_INCOMPLETE, NUD_DELAY, NUD_PROBE, NUD_REACHABLE狀態;neigh_periodic_work用於NUD_STALE。注意neigh_timer_handler是每個表項一個的,而 neigh_periodic_work是唯一的,NUD_STALE狀態的表項沒必要單獨使用定時器,定期檢查過期就可以了,這樣大大節省了資源。
neigh_update則專門用於更新表項狀態,neigh_send_event則是解析表項時的狀態更新,能更新表項的函數很多,這裡不一 一列出。
neigh_timer_handler 定時器函數
當neigh處於NUD_INCOMPLETE, NUD_DELAY, NUD_PEOBE, NUD_REACHABLE時會添 加定時器,即neigh_timer_handler,它處理各個狀態在定時器到期時的情況。
當neigh處於NUD_REACHABLE狀態時,根據NUD 的狀態轉移圖,它有三種轉移可能,分別對應下面三個條件語句。neigh->confirmed代表最近收到來自對應鄰居項的報文時 間,neigh->used代表最近使用該鄰居項的時間。
-如果超時,但期間收到對方的報文,不更改狀態,並重置超時時間為 neigh->confirmed+reachable_time;
-如果超時,期間未收到對方報文,但主機使用過該項,則遷移至NUD_DELAY狀態, 並重置超時時間為neigh->used+delay_probe_time;
-如果超時,且既未收到對方報文,也未使用過該項,則懷疑該項可 能不可用了,遷移至NUD_STALE狀態,而不是立即刪除,neigh_periodic_work()會定時的清除NUD_STALE狀態的表項。
if (state & NUD_REACHABLE) { if (time_before_eq(now, neigh->confirmed + neigh->parms->reachable_time)) { NEIGH_PRINTK2("neigh %p is still alive.\n", neigh); next = neigh->confirmed + neigh->parms->reachable_time; } else if (time_before_eq(now, neigh->used + neigh->parms->delay_probe_time)) { NEIGH_PRINTK2("neigh %p is delayed.\n", neigh); neigh->nud_state = NUD_DELAY; neigh->updated = jiffies; neigh_suspect(neigh); next = now + neigh->parms->delay_probe_time; } else { NEIGH_PRINTK2("neigh %p is suspected.\n", neigh); neigh->nud_state = NUD_STALE; neigh->updated = jiffies; neigh_suspect(neigh); notify = 1; } }
下圖是對上面表項處於NUD_REACHABLE狀態時,定時器到期後3種情形的示意圖:
當neigh處於 NUD_DELAY狀態時,根據NUD的狀態轉移圖,它有二種轉移可能,分別對應下面二個條件語句。
-如果超時,期間收到對方報 文,遷移至NUD_REACHABLE,記錄下次檢查時間到next;
-如果超時,期間未收到對方的報文,遷移至NUD_PROBE,記錄下次 檢查時間到next。
在NUD_STALE->NUD_PROBE中間還插入NUD_DELAY狀態,是為了減少ARP包的數目,期望在定時時間內會收 到對方的確認報文,而不必再進行地址解析。
else if (state & NUD_DELAY) { if (time_before_eq(now, neigh->confirmed + neigh->parms->delay_probe_time)) { NEIGH_PRINTK2("neigh %p is now reachable.\n", neigh); neigh->nud_state = NUD_REACHABLE; neigh->updated = jiffies; neigh_connect(neigh); notify = 1; next = neigh->confirmed + neigh->parms->reachable_time; } else { NEIGH_PRINTK2("neigh %p is probed.\n", neigh); neigh->nud_state = NUD_PROBE; neigh->updated = jiffies; atomic_set(&neigh->probes, 0); next = now + neigh->parms->retrans_time; } }
當neigh處於NUD_PROBE或NUD_INCOMPLETE狀態時,記錄下次檢查時間到next,因為這兩種狀態需要發送ARP解析報文,它們過 程的遷移依賴於ARP解析的進程。
else { /* NUD_PROBE|NUD_INCOMPLETE */ next = now + neigh->parms->retrans_time; }
經過定時器超時後的狀態轉移,如果neigh處於NUD_PROBE或NUD_INCOMPLETE,則會發送ARP報文,先會檢查報文發送 的次數,如果超過了限度,表明對方主機沒有回應,則neigh進入NUD_FAILED,被釋放掉。
if ((neigh->nud_state & (NUD_INCOMPLETE | NUD_PROBE)) && atomic_read(&neigh->probes) >= neigh_max_probes(neigh)) { neigh->nud_state = NUD_FAILED; notify = 1; neigh_invalidate(neigh); }
檢查完後,如果還未超過限度,則會發送ARP報文,neigh->ops->solicit在創建表項neigh時被賦值,一般是 arp_solicit,並且增加探測計算neigh->probes。
if (neigh->nud_state & (NUD_INCOMPLETE | NUD_PROBE)) { struct sk_buff *skb = skb_peek(&neigh->arp_queue); /* keep skb alive even if arp_queue overflows */ if (skb) skb = skb_copy(skb, GFP_ATOMIC); write_unlock(&neigh->lock); neigh->ops->solicit(neigh, skb); atomic_inc(&neigh->probes); kfree_skb(skb); }
實際上,neigh_timer_handler處理啟用了定時器狀態超時的情況,下圖反映了neigh_timer_handler中所涉及的狀態 轉移,值得注意的是NUD_DELAY -> NUD_REACHABLE的狀態轉移,在arp_process中也提到過,收到arp reply時會有表項狀態 NUD_DELAY -> NUD_REACHABLE。它們兩者的區別在於arp_process處理的是arp的確認報文,而neigh_timer_handler處理的是 4層的確認報文。
neigh_periodic_work NUD_STALE狀態的定時函數
當neigh處於NUD_STALE狀態時,此時它等待一段 時間,主機引用到它,從而轉入NUD_DELAY狀態;沒有引用,則轉入NUD_FAIL,被釋放。不同於NUD_INCOMPLETE、NUD_DELAY、 NUD_PROBE、NUD_REACHABLE狀態時的定時器,這裡使用的異步機制,通過定期觸發neigh_periodic_work()來檢查NUD_STALE狀態 。
tbl->parms.base_reachable_time = 30 HZ
當初始化鄰居表時,添加了neigh_periodic_work工作
neigh_table_init() -> neigh_table_init_no_netlink():
INIT_DELAYED_WORK_DEFERRABLE(&tbl- >gc_work, neigh_periodic_work);
當neigh_periodic_work執行時,首先計算到達時間(reachable_time),其中要 注意的是
p->reachable_time = neigh_rand_reach_time(p->base_reachable_time); unsigned long neigh_rand_reach_time(unsigned long base) { return (base ? (net_random() % base) + (base >> 1) : 0); }
因此,reachable_time實際取值是1/2 base ~ 2/3 base,而base = base_reachable_time,當表項處於 NUD_REACHABLE狀態時,會啟動一個定時器,時長為reachable_time,即一個表項在不被使用時存活時間是1/2 base_reachable_time ~ 2/3 base_reachable_time。
然後它會遍歷整個鄰居表,每個hash_buckets的每個表項,如果在 gc_staletime內仍未被引用過,則會從鄰居表中清除。
for (i = 0 ; i <= tbl->hash_mask; i++) { np = &tbl->hash_buckets[i]; while ((n = *np) != NULL) { ….. if (atomic_read(&n->refcnt) == 1 && (state == NUD_FAILED || time_after(jiffies, n->used + n->parms->gc_staletime))) { *np = n->next; n->dead = 1; write_unlock(&n->lock); neigh_cleanup_and_release(n); continue; } …… }
在工作最後,再次添加該工作到隊列中,並延時1/2 base_reachable_time開始執行,這樣,完成了 neigh_periodic_work工作每隔1/2 base_reachable_time執行一次。
schedule_delayed_work(&tbl->gc_work, tbl- >parms.base_reachable_time >> 1);
neigh_periodic_work定期執行,但要保證表項不會剛添加就被 neigh_periodic_work清理掉,這裡的策略是:gc_staletime大於1/2 base_reachable_time。默認的,gc_staletime = 30, base_reachable_time = 30。也就是說,neigh_periodic_work會每15HZ執行一次,但表項在NUD_STALE的存活時間是30HZ,這樣 ,保證了每項在最差情況下也有(30 - 15)HZ的生命周期。
neigh_update 鄰居表項狀態更新
如果新狀態是非有效(! NUD_VALID),那麼要做的就是刪除該表項:停止定時器neigh_del_timer,設置neigh狀態nud_state為新狀態new。除此之外,當 是NUD_INCOMPLETE或NUD_PROBE狀態時,可能有暫時因為地址沒有解析而暫存在neigh->arp_queue中的報文,而現在表項更新 到NUD_FAILED,即解析無法成功,那麼這麼暫存的報文也只能被丟棄neigh_invalidate。
if (!(new & NUD_VALID)) { neigh_del_timer(neigh); if (old & NUD_CONNECTED) neigh_suspect(neigh); neigh->nud_state = new; err = 0; notify = old & NUD_VALID; if ((old & (NUD_INCOMPLETE | NUD_PROBE)) && (new & NUD_FAILED)) { neigh_invalidate(neigh); notify = 1; } goto out; }
中間這段代碼是對比表項的地址是否發生了變化,略過。#個人認為NUD_REACHABLE狀態時,新狀態為NUD_STALE是在 下面這段代碼裡面除去了,因為NUD_REACHABLE狀態更好,不應該回退到NUD_STALE狀態。但是當是NUD_DELAY, NUD_PROBE, NUD_INCOMPLETE時仍會被更新到NUD_STALE狀態,對此很不解???
else { if (lladdr == neigh->ha && new == NUD_STALE && ((flags & NEIGH_UPDATE_F_WEAK_OVERRIDE) || (old & NUD_CONNECTED))) new = old; }
新舊狀態不同時,首先刪除定時器,如果新狀態需要定時器,則重新設置定時器,最後設置表項neigh為新狀態new。
if (new != old) { neigh_del_timer(neigh); if (new & NUD_IN_TIMER) neigh_add_timer(neigh, (jiffies + ((new & NUD_REACHABLE) ? neigh->parms->reachable_time : 0))); neigh->nud_state = new; }
如果鄰居表項中的地址發生了更新,有了新的地址值lladdr,那麼更新表項地址neigh->ha,並更新與此表項相關 的所有緩存表項neigh_update_hhs。
if (lladdr != neigh->ha) { memcpy(&neigh->ha, lladdr, dev->addr_len); neigh_update_hhs(neigh); if (!(new & NUD_CONNECTED)) neigh->confirmed = jiffies - (neigh->parms->base_reachable_time << 1); notify = 1; }
如果表項狀態從非有效(!NUD_VALID)遷移到有效(NUD_VALID),且此表項上的arp_queue上有項,表明之前有報文因為 地址無法解析在暫存在了arp_queue上。此時表項地址解析完成,變為有效狀態,從arp_queue中取出所有待發送的報文skb,發 送出去n1->output(skb),並清空表項的arp_queue。
if (!(old & NUD_VALID)) { struct sk_buff *skb; while (neigh->nud_state & NUD_VALID && (skb = __skb_dequeue(&neigh->arp_queue)) != NULL) { struct neighbour *n1 = neigh; write_unlock_bh(&neigh->lock); /* On shaper/eql skb->dst->neighbour != neigh :( */ if (skb_dst(skb) && skb_dst(skb)->neighbour) n1 = skb_dst(skb)->neighbour; n1->output(skb); write_lock_bh(&neigh->lock); } skb_queue_purge(&neigh->arp_queue); }
neigh_event_send
當主機需要解析地址,會調用neigh_resolve_output,主機引用表項明顯會涉及到表項的NUD狀 態遷移,NUD_NONE->NUD_INCOMPLETE,NUD_STALE->NUD_DELAY。
neigh_event_send -> __neigh_event_send
只 處理nud_state在NUD_NONE, NUD_STALE, NUD_INCOMPLETE狀態時的情況:
if (neigh->nud_state & (NUD_CONNECTED | NUD_DELAY | NUD_PROBE)) goto out_unlock_bh;
不處於NUD_STALE和NUD_INCOMPLETE狀態,則只能是NUD_NONE。此時主機要用到該鄰居表項(注 意是通過neigh_resolve_output進入的),但還沒有,因此要通過ARP進行解析,並且此時沒有收到對方發來的任何報文,要進行 的ARP是廣播形式。
在發送ARP報文時有3個參數- ucast_probes, mcast_probes, app_probes,分別代表單播次數,廣播次數 ,app_probes比較特殊,一般情況下為0,當使用了arpd守護進程時才會設置它的值。如果已經收到過對方的報文,即知道了對 方的MAC-IP,ARP解析會使用單播形式,次數由ucast_probes決定;如果未收到過對方報文,此時ARP解析只能使用廣播形式,次 數由mcasat_probes決定。
當mcast_probes有值時,neigh進入NUD_INCOMPLETE狀態,設置定時器,注意此時neigh_probes(表 示已經進行探測的次數)初始化為ucast_probes,目的是只進行mcast_probes次廣播;當mcast_probes值為0時(表明當前配置不 允許解析),neigh進入NUD_FAILED狀態,被清除。
if (!(neigh->nud_state & (NUD_STALE | NUD_INCOMPLETE))) { if (neigh->parms->mcast_probes + neigh->parms->app_probes) { atomic_set(&neigh->probes, neigh->parms->ucast_probes); neigh->nud_state = NUD_INCOMPLETE; neigh->updated = jiffies; neigh_add_timer(neigh, now + 1); } else { neigh->nud_state = NUD_FAILED; neigh->updated = jiffies; write_unlock_bh(&neigh->lock); kfree_skb(skb); return 1; } }
當neigh處於NUD_STALE狀態時,根據NUD的狀態轉移圖,主機引用到了該鄰居表項,neigh轉移至NUD_DELAY狀態,設 置定時器。
else if (neigh->nud_state & NUD_STALE) { NEIGH_PRINTK2("neigh %p is delayed.\n", neigh); neigh->nud_state = NUD_DELAY; neigh->updated = jiffies; neigh_add_timer(neigh, jiffies + neigh->parms->delay_probe_time); }
當neigh處於NUD_INCOMPLETE狀態時,需要發送ARP報文進行地址解析,__skb_queue_tail(&neigh->arp_queue, skb) 的作用就是先把要發送的報文緩存起來,放到neigh->arp_queue鏈表中,當完成地址解析,再從neigh->arp_queue取出報 文,並發送出去。
if (neigh->nud_state == NUD_INCOMPLETE) { if (skb) { if (skb_queue_len(&neigh->arp_queue) >= neigh->parms->queue_len) { struct sk_buff *buff; buff = __skb_dequeue(&neigh->arp_queue); kfree_skb(buff); NEIGH_CACHE_STAT_INC(neigh->tbl, unres_discards); } __skb_queue_tail(&neigh->arp_queue, skb); } rc = 1; }
鄰居表的操作
neigh_create 創建鄰居表項
首先為新的鄰居表項struct neighbour分配空間,並做一些初始化 。傳入的參數tbl就是全局量arp_tbl,分配空間的大小是tbl->entry_size,而這個值在聲明arp_tbl時初始化為sizeof (struct neighbour) + 4,多出的4個字節就是key值存放的地方。
n = neigh_alloc(tbl);
拷貝key(即IP地址) 到primary_key,而primary_key就是緊接neighbour的4個字節,看下struct neighbor的聲明 - u8 primary_key[0];設置n- >dev指向接收到報文的網卡設備dev。
memcpy(n->primary_key, pkey, key_len); n->dev = dev;
哈希表是犧牲空間換時間,保證均勻度很重要,一旦某個表項的值過多,鏈表查找會降低性能。因 此當表項數目entries大於初始分配大小hash_mask+1時,執行neigh_hash_grow將哈希表空間倍增,這也是內核使用哈希表時常 用的方法,可變大小的哈希表。
if (atomic_read(&tbl->entries) > (tbl->hash_mask + 1)) neigh_hash_grow(tbl, (tbl->hash_mask + 1) << 1);
通過pkey和dev計算哈希值,決定插入tbl- >hash_buckets的表項。
hash_val = tbl->hash(pkey, dev) & tbl->hash_mask;
搜索tbl- >hash_buckets[hash_val]項,如果創建的新ARP表項已存在,則退出;否則將其n插入該項的鏈表頭。
for (n1 = tbl->hash_buckets[hash_val]; n1; n1 = n1->next) { if (dev == n1->dev && !memcmp(n1->primary_key, pkey, key_len)) { neigh_hold(n1); rc = n1; goto out_tbl_unlock; } } n->next = tbl->hash_buckets[hash_val]; tbl->hash_buckets[hash_val] = n;
附一張創建ARP表項並插入到hash_buckets的圖:
neigh_lookup 查找 ARP表項
查找函數很簡單,以IP地址和網卡設備(即pkey和dev)計算哈希值hash_val,然後在tbl->hash_buckets查找相應 項。
hash_val = tbl->hash(pkey, dev); for (n = tbl->hash_buckets[hash_val & tbl->hash_mask]; n; n = n->next) { if (dev == n->dev && !memcmp(n->primary_key, pkey, key_len)) { neigh_hold(n); NEIGH_CACHE_STAT_INC(tbl, hits); break; } }
代理ARP
代理ARP的相關知識查閱google。要明確代理ARP功能是針對陸由器的(或者說是具有轉發功能的主機)。開 啟ARP代理後,會對查詢不在本網段的ARP請求包回應。
回到之前的arp_process代碼,處理代理ARP的情況,這實際就是進行 代理ARP的條件,IN_DEV_FORWARD是支持轉發,RTN_UNICAST是與路由直連,arp_fwd_proxy表示設備支持代理行為, arp_fwd_pvlan表示支持代理同設備進出,pneigh_lookup表示目的地址的代理。這兩種arp_fwd_proxy和arp_fwd_pvlan都只是網 卡設備的一種性質,pneigh_lookup則是一張代理鄰居表,它的內容都是手動添加或刪除的,三種策略任一一種滿足都可以進行 代理ARP。
else if (IN_DEV_FORWARD(in_dev)) { if (addr_type == RTN_UNICAST && (arp_fwd_proxy(in_dev, dev, rt) || arp_fwd_pvlan(in_dev, dev, rt, sip, tip) || pneigh_lookup(&arp_tbl, net, &tip, dev, 0)))
pneigh_lookup 查找或添加代理鄰居表項[proxy neighbour]
以[pkey=tip, key_len=4]計算hash值,執行__pneigh_lookup_1在phash_buckets中查找。
u32 hash_val = pneigh_hash(pkey, key_len); n = __pneigh_lookup_1(tbl->phash_buckets[hash_val], net, pkey, key_len, dev);
如果在phash_buckets中查 找到,或者不需要創建新表項,則函數返回,此時它的功能僅僅是lookup。
if (n || !creat) goto out;
而當傳入參數create=1時,則它的功能不僅是lookup,還會在表項不存在時create。同neighbour結構一樣 ,鍵值pkey存儲在pneigh結構的後面,這樣當pkey變化時,修改十分容易。創建操作很直觀,為pneigh和pkey分配空間,初始化 些變量,最後插入phash_buckets。
n = kmalloc(sizeof(*n) + key_len, GFP_KERNEL); …… write_pnet(&n->net, hold_net(net)); memcpy(n->key, pkey, key_len); …… n->next = tbl->phash_buckets[hash_val]; tbl->phash_buckets[hash_val] = n;
pneigh_enqueue 將報文加入代理隊列
首先計算下次調度的時間,這是一 個隨機值,記錄到sched_next中;設置flags|=LOCALLY_ENQUEUED表明報文是本地加入的。
unsigned long sched_next = now + (net_random() % p->proxy_delay); …… NEIGH_CB(skb)->sched_next = sched_next; NEIGH_CB(skb)->flags |= LOCALLY_ENQUEUED;
然後將報文加入proxy_queue,並設置定時器proxy_timer,下次超 時時間為剛計算的值sched_next,這樣,下次超時時就會處理proxy_queue隊列中的報文。
__skb_queue_tail (&tbl->proxy_queue, skb); mod_timer(&tbl->proxy_timer, sched_next);
這裡的tbl當然是arp_tbl,它的proxy_timer是在初始化時設置 的arp_init() -> neigh_table_init_no_netlink()中:
setup_timer(&tbl->proxy_timer, neigh_proxy_process, (unsigned long)tbl);
neigh_proxy_process 代理ARP的定時器
skb_queue_walk_safe如同 for循環一樣,它遍歷proxy_queue,一個個取出其中的報文skb,查看報文的調度時間sched_next與當前時間now的差值。
如 果tdif<=0則表明調度時間已到或已過,報文要被處理了,從proxy_queue上取出該報文,調用tbl->proxy_redo重新發送 報文,tbl->proxy_redo也是在arp初始化時賦值的,實際上就是arp_process()函數。結合上面的分析,它會執行 arp_process中代理ARP處理的else部分,發送響應報文。
如果tdif>0則表明調度時間還未到,else if部分的功能就是記 錄下最近要過期的調度時間到sched_next。
skb_queue_walk_safe(&tbl->proxy_queue, skb, n) { long tdif = NEIGH_CB(skb)->sched_next - now; if (tdif <= 0) { struct net_device *dev = skb->dev; __skb_unlink(skb, &tbl->proxy_queue); if (tbl->proxy_redo && netif_running(dev)) tbl->proxy_redo(skb); else kfree_skb(skb); dev_put(dev); } else if (!sched_next || tdif < sched_next) sched_next = tdif; }
重新設置proxy_timer的定時器,下次超時時間為剛剛記錄下的最近要調度的時間sched_next + 當前時間jiffies。
del_timer(&tbl->proxy_timer); if (sched_next) mod_timer(&tbl->proxy_timer, jiffies + sched_next);
以一張簡單的圖來說明ARP代理的處理過程,過程 一是入隊列等待,過程二是出隊列發送。不立即處理ARP代理請求報文的原因是為了性能,收到報文後會啟動定時器,超時時間 是一個隨機變量,保證了在大量主機同時進行此類請求時不會形成太大的負擔。
鄰居表緩存
鄰居 表緩存中存儲的就是二層報頭,如果緩存的報頭正好被用到,那麼直接從鄰居表緩存中取出報文就行了,而不用再額外的構造報 頭,加快了協議棧的響應速度。
neigh_hh_init 創建新的鄰居表緩存
當發送報文時,如果還沒有對方主機MAC地址,則調 用neigh_resove_output進行地址解析,此時會判斷dst->hh為NULL時,就會調用neigh_hh_init創建鄰居表緩存,加速下次的 報文發送。
首先在鄰居表項所鏈的所有鄰居表緩存項n->hh匹配協議號protocol,找到,則說明已有緩存,不必再創建, neigh_hh_init會直接返回;未找到,則會創建新的緩存項hh。
for (hh = n->hh; hh; hh = hh->hh_next) if (hh->hh_type == protocol) break;
下面代碼段創建了新的緩存項hh,並初始化了hh的內容,其中dev->header_ops->cache會賦值hh- >hh_data,即[SRCMAC, DSTMAC, TYPE]。如果賦值失敗,釋放掉剛才分配的hh;如果賦值成功,將hh鏈入n->hh的鏈表, 並根據NUD狀態賦值hh->hh_output。
if (!hh && (hh = kzalloc(sizeof(*hh), GFP_ATOMIC)) != NULL) { seqlock_init(&hh->hh_lock); hh->hh_type = protocol; atomic_set(&hh->hh_refcnt, 0); hh->hh_next = NULL; if (dev->header_ops->cache(n, hh)) { kfree(hh); hh = NULL; } else { atomic_inc(&hh->hh_refcnt); hh->hh_next = n->hh; n->hh = hh; if (n->nud_state & NUD_CONNECTED) hh->hh_output = n->ops->hh_output; else hh->hh_output = n->ops->output; } }
最後,創建成功的hh,陸由緩存dst->hh指向新創建的hh。
if (hh) { atomic_inc(&hh->hh_refcnt); dst->hh = hh; }
從hh的創建過程可以看出,通過鄰居表項neighbour的緩存hh可以遍歷所有的與neighbour相關的緩存(即目的MAC相同 ,但協議不同);通過dst的緩存hh只能指向相關的一個緩存(盡管dst->hh->hh_next也許有值,但只會使用dst->hh)。
這裡解釋了為什麼neighbour和dst都有hh指針指向緩存項,可以這麼說,neighbour指向的hh是全部的,dst指向的hh是特定 一個。兩者的作用:在發送報文時查找完陸由表找到dst後,會直接用dst->hh,得到以太網頭;而當遠程主機MAC地址變更時 ,通過dst->neighbour->hh可以遍歷所有緩存項,從而全部更改,而用dst->hh得一個個查找,幾乎是無法完成的。可 以這麼說,dst->hh是使用時用的,neigh->hh是管理時用的。
neigh_update_hhs 更新緩存項
更新緩存項更新的實際就是緩存項的MAC地址。比如當收到一個報文,以它源IP為鍵值在 鄰居表中查找到的neighbour表項的n->ha與報文源MAC值不同時,說明對方主機的MAC地址發生了變更,此時就需要更新所有 以舊MAC生成的hh為新MAC。
鄰居表項是以IP為鍵值查找的,因此通過IP可以查找相關的鄰居表項neigh,前面說過neigh- >hh可以遍歷所有以之相關的緩存項,所以遍歷它,並調用update函數。以以太網卡為例,update = neigh->dev- >header_ops->cache_update ==> eth_header_cache_update,而eth_header_cache_update函數就是用新的MAC地址覆 蓋hh->data中的舊MAC地址。
neigh_update_hhs函數也說明了neighbour->hh指針的作用。
for (hh = neigh ->hh; hh; hh = hh->hh_next) { write_seqlock_bh(&hh->hh_lock); update(hh, neigh->dev, neigh->ha); write_sequnlock_bh(&hh->hh_lock); }
補充:緩存項hh的生命期從創建時起,會一直持續到鄰居表項被刪除,也就是調用neigh_destroy時,刪除neigh->hh指向 的所有緩存項。
參考:《Understanding Linux Network Internals》