天天看點

Linux核心分析 - 網絡[九]:鄰居表

核心版本: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的轉移,作何解釋?        

Linux核心分析 - 網絡[九]:鄰居表

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則是解析表項時的狀态更新,能更新表項的函數很多,這裡不一一列出。 

Linux核心分析 - 網絡[九]:鄰居表

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種情形的示意圖: 

Linux核心分析 - 網絡[九]:鄰居表

      當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層的确認封包。 

Linux核心分析 - 網絡[九]:鄰居表

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的圖: 

Linux核心分析 - 網絡[九]:鄰居表

 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代理請求封包的原因是為了性能,收到封包後會啟動定時器,逾時時間是一個随機變量,保證了在大量主機同時進行此類請求時不會形成太大的負擔。 

Linux核心分析 - 網絡[九]:鄰居表

鄰居表緩存

      鄰居表緩存中存儲的就是二層報頭,如果緩存的報頭正好被用到,那麼直接從鄰居表緩存中取出封包就行了,而不用再額外的構造報頭,加快了協定棧的響應速度。

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是管理時用的。 

Linux核心分析 - 網絡[九]:鄰居表

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》

繼續閱讀