Linux内核与网络兼职,Linux内核分析 - 网络[九]:邻居表

 2023-09-25 阅读 17 评论 0

摘要:内核版本:2.6.34 这部分的重点是三个核心的数据结构-邻居表、邻居缓存、代理邻居表,以及NUD状态转移图。 总的来说,要成功添加一条邻居表项,需要满足两个条件:1. 本机使用该表项;2. 对方主机进行了确认。同时,表项的添加引
内核版本: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://blog.csdn.net/wangpengqi/article/category/1488621或者http://blog.csdn.net/wangpengqi/article/details/9262211,作为开始处理ARP报文的起点。

return NF_HOOK(NFPROTO_ARP, NF_ARP_IN, skb, dev, NULL, arp_process);

Linux内核与网络兼职?

arp_process()
    这个函数开始对报文进行处理,首先会从skb中取出arp报头部分的信息,如sha, sip, tha, tip等,这部分可查阅代码,这里略过。ARP不会查询环路地址和组播地址,因为它们没有对应的mac地址,因此遇到这两类地址,直接退出。

[cpp] view plaincopy
  1. if (ipv4_is_loopback(tip) || ipv4_is_multicast(tip))  
  2.  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状态的表项。

[cpp] view plaincopy
  1. if (state & NUD_REACHABLE) {  
  2.  if (time_before_eq(now,  
  3.    neigh->confirmed + neigh->parms->reachable_time)) {  
  4.   NEIGH_PRINTK2("neigh %p is still alive.\n", neigh);  
  5.   next = neigh->confirmed + neigh->parms->reachable_time;  
  6.  } else if (time_before_eq(now,  
  7.    neigh->used + neigh->parms->delay_probe_time)) {  
  8.   NEIGH_PRINTK2("neigh %p is delayed.\n", neigh);  
  9.   neigh->nud_state = NUD_DELAY;  
  10.   neigh->updated = jiffies;  
  11.   neigh_suspect(neigh);  
  12.   next = now + neigh->parms->delay_probe_time;  
  13.  } else {  
  14.   NEIGH_PRINTK2("neigh %p is suspected.\n", neigh);  
  15.   neigh->nud_state = NUD_STALE;  
  16.   neigh->updated = jiffies;  
  17.   neigh_suspect(neigh);  
  18.   notify = 1;  
  19.  }  
  20. }  

       下图是对上面表项处于NUD_REACHABLE状态时,定时器到期后3种情形的示意图: 

      当neigh处于NUD_DELAY状态时,根据NUD的状态转移图,它有二种转移可能,分别对应下面二个条件语句。
         -如果超时,期间收到对方报文,迁移至NUD_REACHABLE,记录下次检查时间到next;
         -如果超时,期间未收到对方的报文,迁移至NUD_PROBE,记录下次检查时间到next。
      在NUD_STALE->NUD_PROBE中间还插入NUD_DELAY状态,是为了减少ARP包的数目,期望在定时时间内会收到对方的确认报文,而不必再进行地址解析。

[cpp] view plaincopy
  1. else if (state & NUD_DELAY) {  
  2.  if (time_before_eq(now,  
  3.    neigh->confirmed + neigh->parms->delay_probe_time)) {  
  4.   NEIGH_PRINTK2("neigh %p is now reachable.\n", neigh);  
  5.   neigh->nud_state = NUD_REACHABLE;  
  6.   neigh->updated = jiffies;  
  7.   neigh_connect(neigh);  
  8.   notify = 1;  
  9.   next = neigh->confirmed + neigh->parms->reachable_time;  
  10.  } else {  
  11.   NEIGH_PRINTK2("neigh %p is probed.\n", neigh);  
  12.   neigh->nud_state = NUD_PROBE;  
  13.   neigh->updated = jiffies;  
  14.   atomic_set(&neigh->probes, 0);  
  15.   next = now + neigh->parms->retrans_time;  
  16.  }  
  17. }   

        当neigh处于NUD_PROBE或NUD_INCOMPLETE状态时,记录下次检查时间到next,因为这两种状态需要发送ARP解析报文,它们过程的迁移依赖于ARP解析的进程。

[cpp] view plaincopy
  1. else {  
  2.  /* NUD_PROBE|NUD_INCOMPLETE */  
  3.  next = now + neigh->parms->retrans_time;  
  4. }  

        经过定时器超时后的状态转移,如果neigh处于NUD_PROBE或NUD_INCOMPLETE,则会发送ARP报文,先会检查报文发送的次数,如果超过了限度,表明对方主机没有回应,则neigh进入NUD_FAILED,被释放掉。

[cpp] view plaincopy
  1. if ((neigh->nud_state & (NUD_INCOMPLETE | NUD_PROBE)) &&  
  2.  atomic_read(&neigh->probes) >= neigh_max_probes(neigh)) {  
  3.  neigh->nud_state = NUD_FAILED;  
  4.  notify = 1;  
  5.  neigh_invalidate(neigh);  
  6. }  

        检查完后,如果还未超过限度,则会发送ARP报文,neigh->ops->solicit在创建表项neigh时被赋值,一般是arp_solicit,并且增加探测计算neigh->probes。

[cpp] view plaincopy
  1. if (neigh->nud_state & (NUD_INCOMPLETE | NUD_PROBE)) {  
  2.  struct sk_buff *skb = skb_peek(&neigh->arp_queue);  
  3.  /* keep skb alive even if arp_queue overflows */  
  4.  if (skb)  
  5.   skb = skb_copy(skb, GFP_ATOMIC);  
  6.  write_unlock(&neigh->lock);  
  7.  neigh->ops->solicit(neigh, skb);  
  8.  atomic_inc(&neigh->probes);  
  9.  kfree_skb(skb);  
  10. }  

      实际上,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状态。

[cpp] view plaincopy
  1. tbl->parms.base_reachable_time = 30 HZ  

     当初始化邻居表时,添加了neigh_periodic_work工作
     neigh_table_init() -> neigh_table_init_no_netlink():

[cpp] view plaincopy
  1. INIT_DELAYED_WORK_DEFERRABLE(&tbl->gc_work, neigh_periodic_work);  

        当neigh_periodic_work执行时,首先计算到达时间(reachable_time),其中要注意的是

[cpp] view plaincopy
  1. p->reachable_time = neigh_rand_reach_time(p->base_reachable_time);  
  2. unsigned long neigh_rand_reach_time(unsigned long base)  
  3. {  
  4.  return (base ? (net_random() % base) + (base >> 1) : 0);  
  5. }  

        因此,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内仍未被引用过,则会从邻居表中清除。

[cpp] view plaincopy
  1. for (i = 0 ; i <= tbl->hash_mask; i++) {  
  2.  np = &tbl->hash_buckets[i];  
  3.  while ((n = *np) != NULL) {  
  4.   …..  
  5. if (atomic_read(&n->refcnt) == 1 &&  
  6.   (state == NUD_FAILED ||  
  7.   time_after(jiffies, n->used + n->parms->gc_staletime))) {  
  8.   *np = n->next;  
  9.   n->dead = 1;  
  10.   write_unlock(&n->lock);  
  11.   neigh_cleanup_and_release(n);  
  12.   continue;  
  13.  }  
  14.  ……  
  15. }  

      在工作最后,再次添加该工作到队列中,并延时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状态,对此很不解???

ubuntu查看路由表,

[cpp] view plaincopy
  1. else {  
  2.  if (lladdr == neigh->ha && new == NUD_STALE &&  
  3.   ((flags & NEIGH_UPDATE_F_WEAK_OVERRIDE) ||  
  4.   (old & NUD_CONNECTED)))  
  5.   new = old;  
  6. }  

        新旧状态不同时,首先删除定时器,如果新状态需要定时器,则重新设置定时器,最后设置表项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。

[cpp] view plaincopy
  1. if (lladdr != neigh->ha) {  
  2.  memcpy(&neigh->ha, lladdr, dev->addr_len);  
  3.  neigh_update_hhs(neigh);  
  4.  if (!(new & NUD_CONNECTED))  
  5.   neigh->confirmed = jiffies -  
  6.    (neigh->parms->base_reachable_time << 1);  
  7.  notify = 1;  
  8. }  

        如果表项状态从非有效(!NUD_VALID)迁移到有效(NUD_VALID),且此表项上的arp_queue上有项,表明之前有报文因为地址无法解析在暂存在了arp_queue上。此时表项地址解析完成,变为有效状态,从arp_queue中取出所有待发送的报文skb,发送出去n1->output(skb),并清空表项的arp_queue。

[cpp] view plaincopy
  1. if (!(old & NUD_VALID)) {  
  2.  struct sk_buff *skb;  
  3. while (neigh->nud_state & NUD_VALID &&  
  4.      (skb = __skb_dequeue(&neigh->arp_queue)) != NULL) {  
  5.   struct neighbour *n1 = neigh;  
  6.   write_unlock_bh(&neigh->lock);  
  7.   /* On shaper/eql skb->dst->neighbour != neigh :( */  
  8.   if (skb_dst(skb) && skb_dst(skb)->neighbour)  
  9.    n1 = skb_dst(skb)->neighbour;  
  10.   n1->output(skb);  
  11.   write_lock_bh(&neigh->lock);  
  12.  }  
  13.  skb_queue_purge(&neigh->arp_queue);  
  14. }  


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状态时的情况:

[cpp] view plaincopy
  1. if (neigh->nud_state & (NUD_CONNECTED | NUD_DELAY | NUD_PROBE))  
  2.   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状态,被清除。

[cpp] view plaincopy
  1. if (!(neigh->nud_state & (NUD_STALE | NUD_INCOMPLETE))) {  
  2.  if (neigh->parms->mcast_probes + neigh->parms->app_probes) {  
  3.   atomic_set(&neigh->probes, neigh->parms->ucast_probes);  
  4.   neigh->nud_state     = NUD_INCOMPLETE;  
  5.   neigh->updated = jiffies;  
  6.   neigh_add_timer(neigh, now + 1);  
  7.  } else {  
  8.   neigh->nud_state = NUD_FAILED;  
  9.   neigh->updated = jiffies;  
  10.   write_unlock_bh(&neigh->lock);  
  11.   
  12.   kfree_skb(skb);  
  13.   return 1;  
  14.  }  
  15. }  

         当neigh处于NUD_STALE状态时,根据NUD的状态转移图,主机引用到了该邻居表项,neigh转移至NUD_DELAY状态,设置定时器。

[cpp] view plaincopy
  1. else if (neigh->nud_state & NUD_STALE) {  
  2.  NEIGH_PRINTK2("neigh %p is delayed.\n", neigh);  
  3.  neigh->nud_state = NUD_DELAY;  
  4.  neigh->updated = jiffies;  
  5.  neigh_add_timer(neigh, jiffies + neigh->parms->delay_probe_time);  
  6. }  

      当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值存放的地方。

[cpp] view plaincopy
  1. n = neigh_alloc(tbl);  

       拷贝key(即IP地址)到primary_key,而primary_key就是紧接neighbour的4个字节,看下struct neighbor的声明 - u8 primary_key[0];设置n->dev指向接收到报文的网卡设备dev。

[cpp] view plaincopy
  1. memcpy(n->primary_key, pkey, key_len);  
  2. n->dev = dev;  

       哈希表是牺牲空间换时间,保证均匀度很重要,一旦某个表项的值过多,链表查找会降低性能。因此当表项数目entries大于初始分配大小hash_mask+1时,执行neigh_hash_grow将哈希表空间倍增,这也是内核使用哈希表时常用的方法,可变大小的哈希表。

[cpp] view plaincopy
  1. if (atomic_read(&tbl->entries) > (tbl->hash_mask + 1))  
  2.  neigh_hash_grow(tbl, (tbl->hash_mask + 1) << 1);  

       通过pkey和dev计算哈希值,决定插入tbl->hash_buckets的表项。

[cpp] view plaincopy
  1. hash_val = tbl->hash(pkey, dev) & tbl->hash_mask;  

       搜索tbl->hash_buckets[hash_val]项,如果创建的新ARP表项已存在,则退出;否则将其n插入该项的链表头。

[cpp] view plaincopy
  1. for (n1 = tbl->hash_buckets[hash_val]; n1; n1 = n1->next) {  
  2.  if (dev == n1->dev && !memcmp(n1->primary_key, pkey, key_len)) {  
  3.   neigh_hold(n1);  
  4.   rc = n1;  
  5.   goto out_tbl_unlock;  
  6.  }  
  7. }  
  8. n->next = tbl->hash_buckets[hash_val];  
  9. 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。

[cpp] view plaincopy
  1. else if (IN_DEV_FORWARD(in_dev)) {  
  2.  if (addr_type == RTN_UNICAST  &&  
  3.    (arp_fwd_proxy(in_dev, dev, rt) ||  
  4.     arp_fwd_pvlan(in_dev, dev, rt, sip, tip) ||  
  5.     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。

[cpp] view plaincopy
  1. if (n || !creat)  
  2.  goto out;  

        而当传入参数create=1时,则它的功能不仅是lookup,还会在表项不存在时create。同neighbour结构一样,键值pkey存储在pneigh结构的后面,这样当pkey变化时,修改十分容易。创建操作很直观,为pneigh和pkey分配空间,初始化些变量,最后插入phash_buckets。

[cpp] view plaincopy
  1. n = kmalloc(sizeof(*n) + key_len, GFP_KERNEL);  
  2. ……  
  3. write_pnet(&n->net, hold_net(net));  
  4. memcpy(n->key, pkey, key_len);  
  5. ……  
  6. n->next = tbl->phash_buckets[hash_val];  
  7. tbl->phash_buckets[hash_val] = n;  


pneigh_enqueue 将报文加入代理队列
     首先计算下次调度的时间,这是一个随机值,记录到sched_next中;设置flags|=LOCALLY_ENQUEUED表明报文是本地加入的。

[cpp] view plaincopy
  1. unsigned long sched_next = now + (net_random() % p->proxy_delay);  
  2. ……  
  3. NEIGH_CB(skb)->sched_next = sched_next;  
  4. NEIGH_CB(skb)->flags |= LOCALLY_ENQUEUED;  

        然后将报文加入proxy_queue,并设置定时器proxy_timer,下次超时时间为刚计算的值sched_next,这样,下次超时时就会处理proxy_queue队列中的报文。

[cpp] view plaincopy
  1. __skb_queue_tail(&tbl->proxy_queue, skb);  
  2. 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。

[cpp] view plaincopy
  1. skb_queue_walk_safe(&tbl->proxy_queue, skb, n) {  
  2.  long tdif = NEIGH_CB(skb)->sched_next - now;  
  3.   
  4.  if (tdif <= 0) {  
  5.   struct net_device *dev = skb->dev;  
  6.   __skb_unlink(skb, &tbl->proxy_queue);  
  7.   if (tbl->proxy_redo && netif_running(dev))  
  8.    tbl->proxy_redo(skb);  
  9.   else  
  10.    kfree_skb(skb);  
  11.   
  12.   dev_put(dev);  
  13.  } else if (!sched_next || tdif < sched_next)  
  14.   sched_next = tdif;  
  15. }  

        重新设置proxy_timer的定时器,下次超时时间为刚刚记录下的最近要调度的时间sched_next + 当前时间jiffies。

[cpp] view plaincopy
  1. del_timer(&tbl->proxy_timer);  
  2. if (sched_next)  
  3.  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。

[cpp] view plaincopy
  1. for (hh = n->hh; hh; hh = hh->hh_next)  
  2.  if (hh->hh_type == protocol)  
  3.   break;  

        下面代码段创建了新的缓存项hh,并初始化了hh的内容,其中dev->header_ops->cache会赋值hh->hh_data,即[SRCMAC, DSTMAC, TYPE]。如果赋值失败,释放掉刚才分配的hh;如果赋值成功,将hh链入n->hh的链表,并根据NUD状态赋值hh->hh_output。

[cpp] view plaincopy
  1. if (!hh && (hh = kzalloc(sizeof(*hh), GFP_ATOMIC)) != NULL) {  
  2.  seqlock_init(&hh->hh_lock);  
  3.  hh->hh_type = protocol;  
  4.  atomic_set(&hh->hh_refcnt, 0);  
  5.  hh->hh_next = NULL;  
  6.   
  7.  if (dev->header_ops->cache(n, hh)) {  
  8.   kfree(hh);  
  9.   hh = NULL;  
  10.  } else {  
  11.   atomic_inc(&hh->hh_refcnt);  
  12.   hh->hh_next = n->hh;  
  13.   n->hh     = hh;  
  14.   if (n->nud_state & NUD_CONNECTED)  
  15.    hh->hh_output = n->ops->hh_output;  
  16.   else  
  17.    hh->hh_output = n->ops->output;  
  18.  }  
  19. }  

      最后,创建成功的hh,陆由缓存dst->hh指向新创建的hh。

[cpp] view plaincopy
  1. if (hh) {  
  2.  atomic_inc(&hh->hh_refcnt);  
  3.  dst->hh = hh;  
  4. }  

        从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指针的作用。

[cpp] view plaincopy
  1. for (hh = neigh->hh; hh; hh = hh->hh_next) {  
  2.  write_seqlock_bh(&hh->hh_lock);  
  3.  update(hh, neigh->dev, neigh->ha);  
  4.  write_sequnlock_bh(&hh->hh_lock);  
  5. }  

      补充:缓存项hh的生命期从创建时起,会一直持续到邻居表项被删除,也就是调用neigh_destroy时,删除neigh->hh指向的所有缓存项。

参考:《Understanding Linux Network Internals》

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/3/94608.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息